일반적인 오류 및 해결 방법
설치·포트·타입·모듈·Hook·데이터 페칭·Hydration·Vercel 오류를 증상과 실행 위치에 따라 진단합니다.
18장에서는 Next.js 애플리케이션을 개발하고 운영하며 만나는 문제를 진단하는 방법을 차례로 다룹니다. 자주 발생하는 오류의 유형을 먼저 살펴보고, 성능 병목, 브라우저 개발자 도구, 서버 쪽 로깅과 모니터링으로 이어 갑니다. 이 글에서는 설치, 컴파일, 데이터 페칭, 하이드레이션, 배포 단계에서 자주 만나는 오류를 유형별로 정리하고, 원인과 해결 방법을 함께 제시합니다.
오류를 고치기 전에 먼저 오류가 발생한 단계를 구분합니다. 같은 화면이 나오지 않아도 원인은 다를 수 있습니다. 빌드 중에 Cannot find module이 나면 파일, 의존성, 임포트 경로부터 확인하고, 서버 요청 중에 DB 연결이 실패하면 서버 로그와 연결 설정을 확인합니다. 브라우저에서만 map 오류가 나면 그 컴포넌트가 실제로 받은 값부터 확인합니다. 재현한 URL과 동작, 최초 오류의 파일과 줄, 그리고 빌드, 서버, 브라우저 가운데 어디에서 발생했는지를 함께 기록하면, 뒤따르는 오류를 원인으로 착각하는 일이 줄어듭니다.
개발 환경 설정 및 의존성 문제
프로젝트를 시작하거나 의존성을 업데이트할 때 흔히 발생하는 오류들입니다. 이 단계의 오류는 앱 코드가 실행되기 전에 나므로, 코드보다 설치 로그, Node.js 버전, 포트 같은 실행 환경부터 확인합니다. 아래에서는 패키지 설치가 실패하는 경우와, 개발 서버가 포트를 잡지 못하는 경우를 나누어 봅니다.
npm install 또는 yarn install 오류
Error: Cannot find module '...', Error: EACCES: permission denied, node-gyp 관련 오류처럼 설치 단계에서 나는 오류입니다. package.json에 적힌 패키지가 제대로 설치되지 않았거나 node_modules 폴더가 손상된 경우, 특정 디렉터리에 파일을 쓸 권한이 부족한 경우(주로 Linux와 macOS), 프로젝트가 요구하는 Node.js나 패키지 매니저의 버전이 현재 설치된 버전과 다른 경우, 패키지 레지스트리에 접근할 수 없는 네트워크 문제가 대표적인 원인입니다.
먼저 설치 로그에서 실패한 패키지와 프로젝트의 패키지 매니저, 버전을 확인합니다. npm 프로젝트에 일치하는 package-lock.json이 있다면 다음처럼 다시 설치할 수 있습니다.
npm cache verify
npm cinpm ci는 기존 node_modules를 교체하지만 락 파일은 고치지 않습니다. 락 파일과 package.json이 다르면 먼저 의도한 의존성 변경인지 확인합니다. Yarn 프로젝트는 해당 버전의 고정 설치 옵션을 쓰며, npm 캐시 전체 삭제나 락 파일 삭제를 일반적인 해결책으로 삼지 않습니다.
권한 문제라면 오류에 나온 경로와 다음 설정값을 비교해, 캐시 문제인지 전역 설치 경로 문제인지 구분합니다.
npm config get cache
npm config get prefix전역 설치 권한 문제는 Node 버전 관리자를 쓰거나 사용자 소유의 전역 설치 경로를 설정하는 방법을 검토합니다. 공유 시스템 디렉터리의 소유권을 한꺼번에 바꾸지 않습니다.
Node.js 버전도 프로젝트가 지원하는 버전인지 확인합니다. macOS나 Linux에서 nvm을 쓰고 저장소에 검토된 .nvmrc가 있다면 그 버전을 설치하고 선택합니다.
nvm install
nvm use.nvmrc가 없다면 먼저 배포 환경과 일치하는 지원 버전을 정합니다. Windows의 별도 버전 관리자는 사용법이 다릅니다. 마지막으로 네트워크 문제라면 프록시 설정과 VPN 연결 등을 확인하고, 다른 네트워크에서 다시 시도해 봅니다.
포트 충돌(EADDRINUSE)
Error: listen EADDRINUSE: address already in use :::3000은 Next.js 개발 서버(기본 3000번 포트)가 쓰려는 포트를 이미 다른 프로세스가 사용하고 있을 때 발생합니다.
먼저 이미 실행 중인 Next.js 서버가 있다면 그 터미널에서 Ctrl + C를 눌러 종료합니다. 어떤 프로세스가 포트를 쓰는지 모른다면 PID와 프로세스의 소유자, 용도를 확인하고, 종료해도 되는 본인의 개발 서버에만 종료를 적용합니다. macOS와 Linux에서는 다음처럼 확인합니다.
lsof -i :3000 # 3000번 포트를 사용하는 프로세스 ID(PID) 찾기
kill <PID> # 확인한 본인 프로세스에 정상 종료 요청lsof -i :3000의 출력에서 PID 열의 숫자가 프로세스 ID입니다. kill은 따로 신호를 지정하지 않으면 정상 종료를 요청하는 신호를 보내므로, 개발 서버가 정리 작업을 마치고 끝날 기회가 있습니다. Windows의 PowerShell에서는 다음처럼 확인합니다. netstat -ano 출력의 마지막 열이 PID이고, findstr :3000은 그중 :3000이 들어간 줄만 남깁니다.
netstat -ano | findstr :3000 # PID 찾기
Stop-Process -Id <PID> # 해당 PID 종료기존 프로세스를 끄지 않고 npm run dev -- -p 3001이나 yarn dev -p 3001처럼 다른 포트에서 개발 서버를 실행하는 방법도 있습니다.
컴파일 및 런타임 오류
개발 중 코드를 작성하면서 발생하는 일반적인 오류들입니다. 이 오류들은 대개 편집기나 개발 서버의 오류 화면에 파일 이름과 줄 번호가 함께 나오므로, 그 위치에서 출발하는 것이 가장 빠릅니다. 아래에서는 타입 검사가 잡는 TypeScript 오류, 번들러가 파일을 찾지 못하는 모듈 임포트 오류, 린트 규칙이 잡는 Hooks 규칙 위반을 차례로 봅니다.
TypeScript 오류
Property '...' does not exist on type '...', Type 'A' is not assignable to type 'B', Cannot find name '...' 같은 오류는 타입 정의의 불일치, 오타, 잘못된 임포트, 빠진 타입 정의 파일(.d.ts)에서 생깁니다.
먼저 변수, 함수 매개변수, 컴포넌트 Props 등의 타입을 정확하게 정의했는지 확인합니다. 서드파티 라이브러리의 타입 정의가 빠졌다면 npm install --save-dev @types/node @types/react @types/react-dom처럼 @types/ 패키지를 설치합니다. tsconfig.json의 compilerOptions에서 strict 모드와 lib 설정 등이 프로젝트에 맞게 구성되었는지도 확인합니다. VS Code 같은 편집기는 TypeScript 오류를 실시간으로 표시하므로, 오류가 생기는 즉시 고치는 데 도움이 됩니다.
빌드 산출물보다 원본을 먼저 봅니다. 오류가 가리키는 원본 타입과 설정을 고치고, 이미 고친 원본과 오래된 .next 산출물이 어긋나는 경우에만 산출물을 정리하고 다시 빌드합니다. string을 number에 대입하는 실제 타입 오류는 캐시를 지워도 해결되지 않습니다.
모듈 임포트 오류
Module not found: Can't resolve '...' in '...'나 Cannot find module '...'는 잘못된 파일 경로, 대소문자 불일치(특히 Linux 환경에 배포할 때), 실제로 설치되지 않았거나 삭제된 모듈에서 생깁니다.
먼저 임포트하는 파일의 경로가 정확한지, 오타는 없는지 확인하며, 상대 경로(../)와 별칭 경로(@/)를 모두 살펴봅니다. 파일 시스템의 파일 이름과 임포트 구문의 대소문자가 정확히 일치하는지도 확인합니다. 일반적인 Windows 디렉터리는 대소문자를 구분하지 않지만 디렉터리별 설정으로 달라질 수 있으므로, Linux 배포 환경에서도 같은 임포트가 동작하도록 실제 파일 이름과 철자를 맞춥니다. 그다음 해당 모듈이 node_modules에 실제로 설치되었는지 package.json과 비교해 확인하고, 빠졌다면 npm install이나 yarn add로 설치합니다.
별칭 경로를 쓴다면 tsconfig.json의 paths 설정이 올바른지 확인합니다.
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}paths는 TypeScript가 임포트 대상을 찾는 설정입니다. Next.js의 번들러가 이 별칭을 지원하더라도, 같은 코드를 별도의 Node.js 스크립트로 실행할 때 그 별칭까지 자동으로 해석되지는 않습니다. TypeScript가 출력한 JavaScript의 임포트 문자열은 paths만으로 바뀌지 않으므로, 실행 도구의 별칭 지원도 확인합니다.
React Hooks 규칙 위반
ESLint의 eslint-plugin-react-hooks는 Hooks 규칙을 어긴 위치에 따라 다른 메시지로 알립니다. useState, useEffect, useMemo 같은 Hook은 React 함수 컴포넌트나 커스텀 Hook의 최상위에서 불러야 합니다. 조건문 안에서 부르면 React Hook "useState" is called conditionally., 반복문 안에서 부르면 React Hook "useState" may be executed more than once., 콜백 함수 안에서 부르면 React Hook "useState" cannot be called inside a callback.으로 시작하는 메시지가 나옵니다. 이름이 대문자나 use로 시작하지 않는 일반 함수 안에서 부르면 React Hook "useState" is called in function "..." that is neither a React function component nor a custom React Hook function.으로 시작하는 메시지가 나옵니다.
해당 Hook을 컴포넌트나 커스텀 Hook의 최상위에서 불렀는지 확인합니다. React의 use API는 조건문과 반복문 안에서도 부를 수 있는 별도 규칙을 따릅니다. 함수 컴포넌트의 이름은 대문자로, 커스텀 Hook의 이름은 use로 시작해야 규칙 검사가 이를 컴포넌트와 Hook으로 인식합니다. App Router에서 클라이언트 쪽 Hook을 쓰는 컴포넌트라면 파일 맨 위에 "use client"; 지시문을 두었는지도 확인합니다.
서버 사이드 렌더링(SSR) 및 데이터 페칭 오류
Next.js의 핵심 기능인 SSR과 데이터 페칭 과정에서 발생하는 오류들입니다. 이 유형은 같은 컴포넌트라도 서버에서 실행될 때와 브라우저에서 실행될 때 결과가 달라질 수 있다는 점이 앞의 오류들과 다릅니다. 그래서 오류 메시지가 터미널에 나왔는지 브라우저 콘솔에 나왔는지를 먼저 보고, 서버의 데이터 조회 문제와 서버와 브라우저의 렌더 결과가 어긋나는 하이드레이션 문제를 나누어 확인합니다.
fetch 또는 데이터베이스 연결 오류
서버 컴포넌트나 Route Handler에서 데이터를 가져올 때 Failed to connect to database, Network request failed, TypeError: Cannot read properties of undefined (reading 'map') 같은 오류를 만날 수 있습니다. 데이터베이스 연결 문자열이나 API 키 같은 환경 변수가 빠졌거나 잘못된 경우, 서버리스 함수가 방화벽이나 IP 허용 목록 때문에 외부 데이터베이스나 API에 연결하지 못하는 경우, 클라이언트 컴포넌트 Props에 일반 함수, 클래스 인스턴스, 데이터베이스 문서 객체처럼 React Server Components가 전달할 수 없는 값을 넣은 경우, 예상한 데이터 구조와 실제로 받은 구조가 다른 경우가 대표적인 원인입니다.
데이터를 받는 과정은 세 단계로 나누어 확인합니다. fetch가 응답을 받는 단계, response.json()이 본문을 해석하는 단계, 앱이 필요한 필드를 쓰는 단계입니다. HTTP 404나 500 응답을 받았다는 이유만으로 fetch의 Promise가 자동으로 거절되지는 않으므로, 상태와 response.ok를 먼저 확인합니다. 그 뒤 본문 형식을 확인하고, JSON을 해석한 뒤에도 필요한 필드의 타입을 검사합니다.
예를 들어 404 HTML 오류 페이지에 response.json()을 부르면 JSON 해석이 실패합니다. 반대로 HTTP 200과 JSON 객체를 받았어도 items 대신 records만 있다면 data.items.map(...)은 실패합니다. 이때는 네트워크 연결을 바꾸기보다 API가 약속한 응답 형식과 Array.isArray(data.items) 같은 조건을 먼저 확인해야 합니다. 오류 화면을 처리할 때도 HTTP 실패, 해석 실패, 업무 실패를 구분합니다.
해결은 원인에 따라 다릅니다. 먼저 .env.local 파일과 배포 환경(Vercel 대시보드 등)의 환경 변수가 올바르게 설정되었는지, process.env.MY_VAR 형태로 읽을 때 오타가 없는지 다시 확인합니다. 그다음 별도의 스크립트나 Postman 등으로 데이터베이스나 외부 API에 직접 연결해 정상 동작하는지 확인합니다. Vercel Functions의 기본 외부 송신 IP는 고정 범위가 아니므로 임의의 Vercel IP 목록을 허용하지 않습니다. 데이터베이스의 네트워크 정책에 맞춰 비공개 연결을 구성하거나, 고정 송신 IP가 꼭 필요하면 Vercel의 Static IPs나 Secure Compute 같은 별도 기능을 씁니다.
서버 컴포넌트에서 클라이언트 컴포넌트로 넘기는 Props는 React Server Components의 직렬화 규칙을 따라야 합니다. 문자열, 숫자, 배열, 일반 객체뿐 아니라 Date, Map, Set 등 React가 지원하는 값은 전달할 수 있습니다. 일반 함수는 넘기지 말고 이벤트 로직을 클라이언트 컴포넌트 안에 두며, 서버 호출이 필요하면 명시적인 서버 함수를 씁니다. 클래스 인스턴스나 Mongoose 문서는 그대로 넘기지 않고, lean()과 명시적인 DTO 변환으로 공개할 필드만 일반 데이터로 만듭니다. 예상하지 못한 페칭 오류는 try...catch로 기록하고 응답을 처리하며, 로그에는 토큰과 연결 문자열을 빼고 오류 종류와 발생 위치를 남깁니다.
Hydration 오류
하이드레이션 오류는 서버가 만든 HTML과 브라우저의 첫 렌더가 달라지는 문제이므로, 원인을 환경 차이와 HTML 구조 차이로 나누어 확인합니다. React 19는 이 경우 다음과 같은 메시지를 보여 줍니다.
Hydration failed because the server rendered HTML didn't match the client.원인은 서버에서 렌더링한 HTML과 브라우저에서 React가 렌더링하려는 HTML 구조가 일치하지 않는 데 있습니다. Date 객체나 Math.random()처럼 실행 환경과 시점에 따라 결과가 달라지는 로직, 잘못된 태그 중첩 때문에 브라우저가 DOM을 보정하는 경우, 브라우저 확장 프로그램이 DOM을 바꾸는 경우, 브라우저 전용 API나 환경에 따라 다른 값을 첫 렌더 중에 바로 읽어 서버 HTML과 다른 결과를 만드는 경우가 대표적입니다.
첫 번째 해결 방법은 같은 초기값을 쓰는 것입니다. 시간과 난수는 서버에서 만든 하나의 값을 넘겨 양쪽의 첫 렌더에 씁니다. 양쪽에서 각각 현재 시간을 문자열로 바꾸는 것만으로는 값이 같아지지 않습니다. 브라우저에서만 정할 값은 첫 렌더를 일치시킨 뒤 Effect에서 갱신합니다.
두 번째는 서버에서 실행할 수 없는 라이브러리를 따로 떼어 두는 방법입니다. 브라우저 전용 라이브러리처럼 서버 사전 렌더 자체가 불가능한 경우에만 클라이언트 컴포넌트 안에서 next/dynamic의 ssr: false를 씁니다. 일반 클라이언트 컴포넌트도 서버에서 정상적으로 사전 렌더되므로 이 옵션이 기본 해결책은 아닙니다.
"use client";
import dynamic from 'next/dynamic';
const NoSSRDynamicComponent = dynamic(() => import('./ClientOnlyComponent'), { ssr: false });
export default function ClientOnlySection() {
return <NoSSRDynamicComponent />;
}ssr: false로 불러온 ClientOnlyComponent는 서버 HTML에 들어가지 않고, 브라우저가 그 모듈을 받은 뒤에야 렌더링됩니다. 그래서 그 자리는 처음에 비어 있다가 나중에 채워지며, 이 예제에는 loading 옵션이 없으므로 기다리는 동안 보여 줄 대체 화면도 따로 정하지 않았습니다. 세 번째는 window처럼 브라우저에만 있는 전역 객체를 useEffect 안에서 읽어, 첫 렌더가 끝난 뒤 브라우저에서만 동작하게 하는 방법입니다.
"use client";
import { useState, useEffect } from 'react';
export default function ClientSideText() {
const [text, setText] = useState('');
useEffect(() => {
setText(window.innerWidth > 768 ? 'Large Screen' : 'Small Screen');
}, []);
return <div>{text}</div>;
}마지막으로 HTML 구조를 확인합니다. <p> 안에 <div>를 넣는 식의 잘못된 중첩은 브라우저가 DOM을 보정하면서 서버 HTML과 달라질 수 있으므로, 올바른 태그 구조로 고쳐 원인을 없앱니다. Suspense는 비동기 로딩 중에 보여 줄 대체 화면을 정하는 기능이지, 하이드레이션 불일치를 고치는 기능이 아닙니다.
"use client"는 이 파일부터 클라이언트 컴포넌트로 다룬다고 선언할 뿐, 그 컴포넌트의 초기 HTML을 서버에서 만들지 않는다는 뜻은 아닙니다. typeof window !== 'undefined'로 접근 오류를 막아도, 서버에서는 빈 결과를, 브라우저의 첫 렌더에서는 문단을 반환하면 초기 출력이 달라집니다. 위 ClientSideText는 양쪽의 첫 렌더를 빈 <div>로 맞춘 뒤 Effect에서 화면 너비를 읽으므로 그 차이를 첫 렌더 뒤로 옮깁니다. 처음부터 꼭 보여야 하는 정보라면 서버가 만든 같은 초기값을 Props로 넘기는 방법도 고려합니다.
같은 첫 렌더 뒤 브라우저 값 반영
서버와 브라우저 초기 렌더가 같은 빈 텍스트를 내고, effect가 window 폭을 읽은 뒤 지역 상태와 출력을 바꿉니다. 초기 일치와 effect 이후 변화가 같은 화면에서 명확히 구분됩니다.
Next · Initial output and effect
같은 첫 렌더 뒤 브라우저 값 반영
ClientSideText 예제는 서버와 브라우저의 첫 렌더를 모두 빈 text로 맞춥니다. 브라우저에서 Effect가 window.innerWidth를 읽고 상태를 바꾼 뒤 글자를 표시합니다.
이 예제는 첫 Effect에서 읽은 너비만 반영하며 resize 리스너는 없습니다. 유효한 HTML 구조와 같은 초기 출력이 전제이고, 브라우저 전용 라이브러리 격리는 별도 판단입니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 서버와 브라우저 첫 렌더의 빈 text는 일치한다. 이후 브라우저 Effect가 너비로 상태를 변경하고 다시 렌더한다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 초기 출력 서버가 빈 text로 초기 출력합니다. | 서버 text: 빈 값 · window 사용: 초기 출력 밖 | 브라우저와 맞출 출발 출력이 생깁니다. |
| 브라우저 첫 렌더 브라우저 첫 렌더도 같은 빈 text입니다. | 브라우저 text: 빈 값 · 초기 일치: 예 | 첫 출력이 서버와 일치합니다. |
| Effect 후 상태 변경 브라우저 Effect가 window 폭을 읽어 상태를 바꾸고 다시 렌더합니다. | 자료: 브라우저 width · 상태: Effect 이후 갱신 · 임의 폭 수치: 없음 | 초기 일치 이후에만 화면 text가 바뀝니다. |
같은 첫 렌더 뒤 브라우저 값 반영
서버와 브라우저 초기 렌더가 같은 빈 텍스트를 내고, effect가 window 폭을 읽은 뒤 지역 상태와 출력을 바꿉니다. 초기 일치와 effect 이후 변화가 같은 화면에서 명확히 구분됩니다.
Next · Initial output and effect
같은 첫 렌더 뒤 브라우저 값 반영
ClientSideText 예제는 서버와 브라우저의 첫 렌더를 모두 빈 text로 맞춥니다. 브라우저에서 Effect가 window.innerWidth를 읽고 상태를 바꾼 뒤 글자를 표시합니다.
이 예제는 첫 Effect에서 읽은 너비만 반영하며 resize 리스너는 없습니다. 유효한 HTML 구조와 같은 초기 출력이 전제이고, 브라우저 전용 라이브러리 격리는 별도 판단입니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 서버와 브라우저 첫 렌더의 빈 text는 일치한다. 이후 브라우저 Effect가 너비로 상태를 변경하고 다시 렌더한다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 서버 초기 출력 서버가 빈 text로 초기 출력합니다. | 서버 text: 빈 값 · window 사용: 초기 출력 밖 | 브라우저와 맞출 출발 출력이 생깁니다. |
| 브라우저 첫 렌더 브라우저 첫 렌더도 같은 빈 text입니다. | 브라우저 text: 빈 값 · 초기 일치: 예 | 첫 출력이 서버와 일치합니다. |
| Effect 후 상태 변경 브라우저 Effect가 window 폭을 읽어 상태를 바꾸고 다시 렌더합니다. | 자료: 브라우저 width · 상태: Effect 이후 갱신 · 임의 폭 수치: 없음 | 초기 일치 이후에만 화면 text가 바뀝니다. |
그림은 ClientSideText의 초기 출력과 너비 반영을 단계별로 나눕니다. 서버의 출력과 브라우저의 첫 렌더는 모두 빈 div로 같고, 서버에서는 Effect가 실행되지 않습니다. 브라우저에서 Effect가 실행된 뒤에야 window.innerWidth를 읽어 상태를 바꾸고 글자를 다시 렌더합니다. 이 예제는 첫 Effect에서 읽은 너비만 반영하며 resize 리스너는 없습니다. 초기 출력의 일치와 Effect의 실행 시점은 React의 Effect 설명에서 확인할 수 있습니다.
더 깊이: React가 불일치를 만났을 때 하는 일과 suppressHydrationWarning
하이드레이션 중에 클라이언트가 렌더링한 요소가 서버 HTML의 요소와 맞지 않으면, React는 클라이언트에서 다시 렌더링해 내용을 고칩니다. 앞에 보인 React 19의 오류 메시지는 실제로 더 길게 이어지며, 그 뒷부분에서 이 트리를 클라이언트에서 다시 만든다고 알립니다. React 문서의 hydrateRoot는 React가 일부 불일치에서는 회복하지만, 좋은 경우에도 느려지고 나쁜 경우에는 이벤트 처리 함수가 엉뚱한 요소에 연결될 수 있으므로 다른 버그처럼 고쳐야 한다고 말합니다. 속성 값의 차이는 고쳐진다는 보장도 없습니다.
React 19는 브라우저 확장 프로그램이나 외부 스크립트 때문에 생기는 불일치도 줄였습니다. 하이드레이션할 때 <head>와 <body>에 예상하지 못한 태그가 끼어 있으면 오류로 보지 않고 건너뛰며, 다른 원인으로 문서 전체를 다시 렌더링해야 할 때도 그런 스크립트나 확장 프로그램이 넣은 스타일시트는 그대로 둡니다. 또 예전처럼 경고를 여러 번 남기지 않고, 서버와 클라이언트의 차이를 diff로 보여 주는 오류 하나를 기록합니다.
시각 표시처럼 한 요소의 속성이나 텍스트가 서버와 클라이언트에서 어쩔 수 없이 다를 때는 그 요소에 suppressHydrationWarning={true}를 붙여 경고를 끌 수 있습니다. 이 속성은 그 요소 한 단계에만 적용되는 탈출구이며, React는 이렇게 표시한 요소의 어긋난 텍스트를 고치려 하지 않으므로 서버가 보낸 텍스트가 그대로 남을 수 있습니다. 원인을 고칠 수 있는 불일치를 덮는 데 쓰지 않고, 정말로 피할 수 없는 값에만 씁니다.
Vercel 배포 관련 오류
Vercel에 배포할 때 생기는 특유의 문제들입니다. 로컬에서는 잘 되던 코드가 배포에서만 실패한다면, 코드보다 두 환경의 차이를 먼저 의심합니다. 파일 시스템의 대소문자 구분, 환경 변수, Node.js 버전이 대표적인 차이이며, 아래에서는 빌드 단계의 실패와 배포한 뒤 요청을 처리하다 나는 실패를 나누어 봅니다.
빌드 실패
Module not found, TypeScript 오류, Command failed with exit code 1처럼 로컬에서는 빌드가 성공하지만 Vercel 환경에서는 실패하는 경우가 있습니다. 주로 대소문자 불일치(Linux 기반 서버), 환경 변수 누락, package.json의 scripts 오류, Node.js 버전 불일치가 원인입니다.
먼저 Vercel 대시보드에서 해당 배포의 빌드 로그를 열어 정확한 오류 메시지를 확인합니다. 그다음 Vercel 프로젝트 설정의 Environment Variables에 필요한 변수가 모두 올바르게 추가되었는지 확인합니다. MONGODB_URI는 DB에 접근하는 실행 단계에 필요하므로, 빌드 중 사전 렌더가 DB를 읽는다면 빌드 환경에도 설정하고, 요청 때만 조회한다면 런타임 환경을 확인합니다. 파일 이름과 임포트 경로의 대소문자가 로컬, Git 저장소, Linux 기반의 Vercel 서버에서 모두 일치하는지도 봅니다.
Node.js 버전도 맞춥니다. Next.js 16은 Node.js 20.9 이상이 필요하므로 로컬과 배포 환경을 맞추고, 필요하면 package.json에 "engines": { "node": ">=20.9.0" }을 적습니다. next.config.js나 next.config.ts에 오류가 없는지 검토하고, 빌드 명령이나 배포 설정을 바꿔야 한다면 vercel.json 파일로 조정할 수 있습니다.
배포 후 런타임 오류
웹 페이지에 500 오류가 나거나 Application error: a client-side exception has occurred가 보인다면, 배포된 함수나 페이지에서 런타임 오류가 발생한 경우입니다. 데이터베이스 연결 문제, 외부 API 호출 실패, 잘못된 로직 등이 원인일 수 있습니다.
Vercel 대시보드의 Logs 탭에서 서버리스 함수와 Edge Runtime의 오류를 확인하면, 스택 트레이스로 문제의 위치를 좁힐 수 있습니다. 로컬에서 npm run build && npm run start로 프로덕션 빌드를 실행하면 배포 환경과 비슷한 조건에서 오류를 재현하고 디버깅할 수 있습니다. 서버에서 클라이언트로 넘기는 데이터에 직렬화할 수 없는 객체가 들어 있는지도 다시 확인합니다.
일반적인 디버깅 방법
어떤 유형의 오류든 문제를 해결하는 데 도움이 되는 일반적인 디버깅 기술입니다.
콘솔 로깅(console.log)은 실행 위치에 따라 출력되는 곳이 다릅니다. 서버 컴포넌트와 Route Handler의 로그는 터미널에서 확인합니다. 클라이언트 컴포넌트도 초기 HTML을 만들 때 서버에서 사전 렌더될 수 있어 렌더 중의 로그는 터미널과 브라우저 양쪽에 나타날 수 있고, 이벤트 핸들러와 useEffect의 로그는 브라우저에서 확인합니다.
브라우저 개발자 도구의 각 탭은 다음과 같이 씁니다.
- Console 탭: 클라이언트 쪽 JavaScript 오류, 경고,
console.log출력을 확인합니다. - Elements 탭: 렌더링된 HTML 구조를 확인해 예상과 다른 DOM 구조를 찾습니다. 하이드레이션 오류를 볼 때 유용합니다.
- Network 탭: 기록 중인 페이지의 브라우저 요청(API 호출, 이미지 등)을 확인합니다. 서버 내부의 DB 쿼리나 외부 API 호출은 브라우저에 별도 요청 행으로 나타나지 않으므로 서버 로그와 함께 봅니다.
- Sources 탭: JavaScript 코드에 중단점(breakpoint)을 두고 실행을 멈춰 변수 값을 검사합니다. 배포된 JavaScript에도 중단점을 둘 수 있으며, Source Map이 있으면 원본 TypeScript의 위치와 연결해 읽기 쉽습니다.
VS Code의 내장 디버거로는 Node.js 환경(Route Handler, 서버 컴포넌트)과 브라우저 환경(클라이언트 컴포넌트)을 각각 디버깅할 수 있으며, launch.json 파일에 디버깅 구성을 저장해 둘 수 있습니다. 문제가 생긴 지점의 변경 이력을 찾을 때는 Git 기록을 확인합니다. git blame <file> 명령은 파일의 각 줄을 마지막으로 바꾼 커밋과 작성자를 보여 줍니다. 오류 메시지는 문제 해결의 첫 단서이므로, 메시지를 그대로 복사해 공식 문서, 이슈 트래커, 검색 엔진에서 찾아보면 비슷한 문제와 해결책을 찾을 가능성이 높습니다.
문제 해결은 개발 과정의 일부입니다. 오류 메시지를 주의 깊게 읽고, 발생 단계를 좁히는 순서를 지키며, 상황에 맞는 디버깅 도구를 고르면 대부분의 문제에 체계적으로 접근할 수 있습니다.
정리
- 오류를 고치기 전에 빌드, 서버, 브라우저 가운데 어디에서 처음 발생했는지 확인하고, 재현 URL과 최초 오류 위치를 기록합니다.
- 설치 오류는 락 파일, 권한, Node.js 버전, 네트워크로 나누어 확인하며, 캐시나 락 파일 삭제를 기본 해결책으로 삼지 않습니다.
- 실제 타입 오류와 모듈 경로 오류는 원본을 고쳐야 하며,
.next정리는 오래된 산출물이 어긋날 때만 의미가 있습니다. - 데이터 페칭은 응답 수신, 본문 해석, 필드 사용을 따로 확인하고, 클라이언트 컴포넌트로 넘기는 Props는 직렬화 규칙을 따릅니다.
- 하이드레이션 오류는 서버와 브라우저의 첫 렌더를 같게 맞추어 해결하며, 브라우저에서만 정할 값은 Effect에서 갱신합니다.
- 배포 오류는 빌드 로그와 런타임 로그를 먼저 확인하고, 대소문자, 환경 변수, Node.js 버전을 로컬과 맞춥니다.
연습 문제
1. HTML 오류 페이지와 JSON 해석
API가 HTTP 404, Content-Type: text/html, 본문 <p>Not Found</p>를 반환했습니다. fetch에서는 응답을 받았는데 이어서 response.json()이 실패합니다. 두 결과는 모순일까요?
해설 보기
모순이 아닙니다. HTTP 오류 상태도 응답 객체로 받을 수 있습니다. 응답 상태의 확인과 JSON 해석은 다른 단계이며, HTML은 JSON이 아니므로 해석이 실패합니다. response.ok와 상태, 본문 형식을 먼저 확인하고 HTTP 오류 처리를 고릅니다.
2. 성공 응답인데 map이 실패할 때
API가 HTTP 200과 올바른 JSON {"records": []}를 반환했습니다. 화면은 data.items.map(...)을 부릅니다. DB 연결을 다시 설정하기 전에 무엇을 확인해야 할까요?
해설 보기
API가 약속한 응답 형식을 확인합니다. JSON 해석은 성공했지만 items가 없으므로 그 값은 undefined입니다. API와 화면 가운데 어느 쪽의 필드 이름이 약속과 다른지 찾아 고치고, 배열인지 검사한 뒤 map을 부릅니다. 빈 배열 자체는 올바른 결과이므로 임의로 연결 오류로 바꾸지 않습니다.
3. typeof window 분기와 hydration
컴포넌트가 서버에서는 null을, 브라우저의 첫 렌더에서는 <p>Client</p>를 반환합니다. 분기 조건은 typeof window !== 'undefined'입니다. 이 조건과 "use client"만으로 하이드레이션 불일치가 해결될까요? 중요한 서버 초기 정보가 없는 예제로 한정해 수정 방향을 제시하세요.
해설 보기
해결되지 않습니다. 이 조건은 window 접근 오류를 막을 뿐, 양쪽의 첫 결과를 같게 만들지 않습니다. 서버와 브라우저의 첫 렌더가 모두 빈 결과를 반환하도록 상태를 시작하고, Effect가 실행된 뒤에 클라이언트용 문단을 표시하면 됩니다. 브라우저 전용 라이브러리를 떼어 둘 때는 별도의 클라이언트 컴포넌트에서 ssr: false를 검토하지만, 일반적인 불일치라면 원인부터 확인합니다.
4. 캐시를 지워도 남는 타입 오류
원본 코드에서 number 변수에 문자열을 대입해 빌드가 실패했습니다. .next를 정리했는데도 같은 줄에서 오류가 납니다. 다음 조치는 무엇일까요?
해설 보기
오류가 가리키는 대입과 타입 관계를 고칩니다. 숫자가 필요한 값이면 입력 변환과 유효성 검사를, 문자열이 맞는 값이면 쓰는 곳까지 포함해 타입 정의를 검토합니다. 캐시는 원본 코드의 타입 관계를 바꾸지 않습니다.
5. ClientSideText의 너비 판정
브라우저에서 첫 Effect가 실행될 때 너비가 각각 768px과 769px이라고 합시다. 어떤 글자가 보일까요? 769px에서 실행된 화면을 그 뒤 400px로 줄였지만 컴포넌트를 다시 마운트하지 않았다면, 이 코드가 글자를 바꿀까요?
해설 보기
조건은 768보다 큰지 검사하므로 768px은 Small Screen, 769px은 Large Screen입니다. 그 뒤 창만 줄여도 resize 리스너가 없고 Effect의 의존성 배열이 비어 있어, 이 코드는 너비 변화를 다시 읽지 않습니다. 같은 마운트에서 남는 글자는 Large Screen입니다.
화면 크기에 계속 반응해야 한다면 CSS로 표현할 수 있는지 먼저 보고, JavaScript가 필요할 때는 리스너의 등록과 정리까지 따로 설계합니다. 개발 모드 Strict Mode의 추가 Effect 점검이 resize 처리를 구현해 주지는 않습니다.
하이드레이션 오류의 원인과 해결 방법은 Next.js의 하이드레이션 오류 안내에서, 모듈을 찾지 못하는 오류는 Module Not Found 안내에서 확인할 수 있습니다.
다음 글 성능 병목 현상 식별 및 해결에서는 Next.js 애플리케이션의 느린 부분을 찾아 개선하는 방법을 알아봅니다.