다크 모드
Appendix C. 실습 환경·런타임 에러 트러블슈팅 FAQ 12종
"개발자가 가장 지치고 포기하고 싶을 때는 아키텍처가 어려울 때가 아니라, 환경 설정과 사소한 에러 때문에 화면이 안 뜰 때이다. 사내 실습 시 자주 마주치는 환경 이슈 5가지(FAQ 1~5)와, 챕터를 따라가다 만나는 React 19·Zustand·테스트 단골 런타임 에러 7가지(FAQ 6~12)의 원인과 해결 코드를 총정리한다."
파트 1: 개발 환경 및 서버 구동 트러블슈팅 (FAQ 1~5)
FAQ 1. npm run dev 실행 시 3000번 포트가 충돌하여 3001번으로 떠요
[증상]
터미널에 Port 3000 is in use, trying another one... 메시지가 뜨면서 주소가 http://localhost:3001로 열립니다.
[원인 및 해결 방법]
이전에 실행했던 Vite 프로세스가 백그라운드에 살아있거나 다른 개발 서버가 3000번 포트를 점유 중인 경우입니다.
- 점유 중인 3000번 포트 프로세스 강제 종료 (Windows PowerShell):
powershell
# 3000번 포트에서 대기(Listen) 중인 프로세스 PID만 추출하여 종료
Get-NetTCPConnection -LocalPort 3000 -State Listen |
Select-Object -ExpandProperty OwningProcess -Unique |
ForEach-Object { Stop-Process -Id $_ -Force }- 포트를 3000번으로 고정하려면 (
vite.config.ts): 기존/api프록시 설정은 그대로 두고strictPort: true한 줄만 추가합니다.
typescript
server: {
port: 3000,
strictPort: true, // 3000번이 사용 중이면 3001로 넘어가지 않고 즉시 에러를 띄워 알려줌
proxy: {
'/api': { target: 'http://localhost:8080', changeOrigin: true }
}
}FAQ 2. VS Code에서 @/ 경로 import에 빨간 밑줄(Cannot find module '@/...')이 떠요
[증상]
브라우저에서는 잘 돌아가는데, VS Code 에디터에서 import type { Post } from '@/types/board' 줄에 빨간 밑줄이 그어집니다.
[원인 및 해결 방법]
TypeScript 언어 서버가 tsconfig.json(또는 tsconfig.app.json)의 경로 별칭(paths)을 인식하지 못했거나 상위 폴더에서 VS Code를 열었을 때 발생합니다.
frontend/tsconfig.json(및tsconfig.app.json)의compilerOptions에 다음 설정이 있는지 확인합니다:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}Ctrl + Shift + P(macOS는Cmd + Shift + P) →TypeScript: Restart TS Server또는 **Developer: Reload Window**를 실행합니다.
FAQ 3. Spring Boot 실행 시 release version 21 not supported / class file version 61.0 에러가 나요
[증상]
backend/ 폴더에서 ./mvnw spring-boot:run 실행 시 빌드가 멈추며 자바 버전 에러가 발생합니다.
- 숫자 의미:
52.0= Java 8,55.0= Java 11,61.0= Java 17,65.0= Java 21.
[원인 및 해결 방법]
본 프로젝트 백엔드는 Java 21 LTS 기준인데 터미널의 JAVA_HOME이 구버전(Java 8/11/17)으로 잡혀 있는 경우입니다.
powershell
# [Windows PowerShell] JDK 21 경로 지정 후 실행
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.5.11-hotspot"
$env:Path = "$env:JAVA_HOME\bin;" + $env:Path
.\mvnw.cmd spring-boot:runbash
# [macOS / Linux]
export JAVA_HOME=$(/usr/libexec/java_home -v 21 2>/dev/null || echo "/usr/lib/jvm/temurin-21-jdk-amd64")
export PATH="$JAVA_HOME/bin:$PATH"
./mvnw spring-boot:runFAQ 4. http://localhost:8080/h2-console 접속 시 Database "mem:test" not found 에러가 나요
[증상]
H2 웹 콘솔에서 [Connect]를 눌렀는데 데이터베이스를 찾을 수 없다거나 테이블이 보이지 않습니다.
[원인 및 해결 방법]
H2 콘솔 로그인 창의 기본 JDBC URL이 jdbc:h2:~/test로 되어 있기 때문입니다. backend/src/main/resources/application.yml과 동일하게 수정해 접속하십시오:
- JDBC URL:
jdbc:h2:mem:boarddb - User Name:
sa - Password: (빈칸 유지)
FAQ 5. Windows PowerShell에서 npm.ps1 파일을 로드할 수 없습니다 (Execution_Policies) 에러가 나요
[증상]
PowerShell에서 npm이나 npx 명령을 치면 보안 정책 때문에 스크립트 실행이 차단됩니다.
[원인 및 해결 방법]
Windows PowerShell의 기본 실행 정책이 Restricted로 막혀 있기 때문입니다. 관리자 권한 없이 현재 사용자에 대해 아래 명령 한 줄을 실행하면 즉시 해결됩니다:
powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser파트 2: React 19.3 · Zustand · 테스트 단골 런타임 에러 (FAQ 6~12)
FAQ 6. Too many re-renders. React limits the number of renders to prevent an infinite loop. (1~3장)
[증상]
화면을 열자마자 하얀 화면과 함께 콘솔에 Too many re-renders 에러가 찍힙니다.
[원인 및 해결 방법]
렌더링 본문이나 JSX 이벤트 핸들러(onClick) 안에서 상태 변경 함수를 화살표 함수로 감싸지 않고 즉시 호출했기 때문입니다.
tsx
// ❌ 원인: 렌더링 도중 handleDelete(post.id)가 즉시 실행되고 상태 변경 → 무한 리렌더링!
<button onClick={handleDelete(post.id)}>삭제</button>
// ✅ 해결: 화살표 함수로 감싸 클릭 이벤트가 발생했을 때만 호출되도록 전달
<button onClick={() => handleDelete(post.id)}>삭제</button>FAQ 7. Invalid hook call. Hooks can only be called inside of the body of a function component. (3장, 5장)
[증상]
훅을 호출한 줄에서 Invalid hook call 에러가 발생하며 앱이 멈춥니다.
[원인 및 해결 방법]
- 조건문(
if), 반복문(for), 또는onClick같은 이벤트 핸들러 함수 내부에서useState,useNavigate,usePostStore같은 훅을 호출했습니다. - 컴포넌트 함수 이름이 소문자(
function boardApp())로 시작합니다.
tsx
// ❌ 원인: 이벤트 핸들러 안에서 훅을 호출함
function handleClick() {
const navigate = useNavigate(); // 💥 Invalid hook call!
navigate('/write');
}
// ✅ 해결: 훅은 반드시 컴포넌트 최상단(Top-level)에서 선언하고, 핸들러 안에서는 반환된 함수만 호출
export function BoardHeader() {
const navigate = useNavigate();
function handleClick() {
navigate('/write');
}
return <button onClick={handleClick}>글쓰기</button>;
}FAQ 8. Zustand 구독 후 Maximum update depth exceeded 무한 루프 에러가 나요 (9장)
[증상]
9장에서 usePostStore를 컴포넌트에 연결하자마자 Maximum update depth exceeded 에러가 쏟아집니다.
[원인 및 해결 방법]
Zustand v5에서 선택자 함수가 ({ posts: state.posts, isLoading: state.isLoading })처럼 매번 새로운 객체 리터럴 {}을 반환하면, Object.is 참조 비교가 매번 다르다고 판단해 무한 리렌더링에 빠집니다.
tsx
import { useShallow } from 'zustand/react/shallow';
// ❌ 원인: 렌더링마다 새 객체 참조가 생성되어 무한 리렌더링 발생
const { posts, isLoading } = usePostStore((s) => ({ posts: s.posts, isLoading: s.isLoading }));
// ✅ 해결 1: 단일 값 선택자로 각각 구독 (가장 깔끔함)
const posts = usePostStore((s) => s.posts);
const isLoading = usePostStore((s) => s.isLoading);
// ✅ 해결 2: 객체로 묶을 때는 반드시 useShallow로 감싸기
const { posts, isLoading } = usePostStore(
useShallow((s) => ({ posts: s.posts, isLoading: s.isLoading }))
);FAQ 9. An optimistic state update occurred outside a transition or action. (10장)
[증상]
10장에서 좋아요 버튼을 눌렀더니 콘솔에 React 19의 useOptimistic 관련 경고가 찍히고 낙관적 상태가 즉시 원래 값으로 돌아가 버립니다.
[원인 및 해결 방법]
React 19.3의 useOptimistic 세터 함수(addOptimisticLike)를 startTransition 바깥의 일반 함수에서 호출했기 때문입니다.
tsx
const [isPending, startTransition] = useTransition();
const [optimisticLikes, addOptimisticLike] = useOptimistic(likeCount, (c, d: number) => c + d);
function handleLikeClick() {
// ✅ 반드시 startTransition(async () => { ... }) 블록 내부에서 호출해야 함!
startTransition(async () => {
addOptimisticLike(1);
await toggleLikeOptimistic(postId);
});
}FAQ 10. 로그인 페이지로 무한 리다이렉트되거나 주소창에 returnUrl이 끝없이 중첩돼요 (11장)
[증상]
주소창이 /login?returnUrl=/login?returnUrl=...처럼 길어지거나 브라우저가 리다이렉트 횟수 초과 에러를 띄웁니다.
[원인 및 해결 방법]
- 실수로
/login라우트까지<ProtectedRoute>로 감쌌습니다. - 로그인 성공 후
getSafeReturnUrl에서/login으로 시작하는 경로를 걸러내지 않았습니다.
/login은 반드시<ProtectedRoute>바깥의 공개 라우트로 두고, 11장 [코드 11-4]의getSafeReturnUrl에!rawUrl.startsWith('/login')조건이 포함되어 있는지 확인하십시오.
FAQ 11. 테스트 실행 중 Error: Not implemented: navigation (except hash changes)가 찍혀요 (12장, 14장)
[증상]
npx vitest run httpClient 실행 시 콘솔에 빨간 글씨로 Not implemented: navigation이 출력되지만 마지막 요약은 2 passed입니다.
[원인 및 해결 방법]
12장 httpClient.ts에서 토큰 갱신 실패 시 실행되는 window.location.href = '/login?expired=true' 코드를 가상 브라우저인 jsdom이 만나서 출력하는 안내 메시지입니다. jsdom은 실제 페이지 전체 이동을 수행하지 않으므로 정상 동작이며, 테스트 통과(passed)에는 아무런 지장이 없습니다.
FAQ 12. Vitest 통합 테스트에서 useNavigate() may be used only in the context of a <Router> component. 에러가 나요 (11장, 14장)
[증상]
render(<BoardApp />)를 실행했더니 라우터 컨텍스트가 없다는 에러와 함께 테스트가 즉시 실패합니다.
[원인 및 해결 방법]
BoardApp이나 하위 훅(usePostFilter의 useSearchParams 등)이 React Router 훅을 사용하는데, 테스트에서 <BoardApp />만 단독으로 렌더링했기 때문입니다. 테스트 환경에서는 항상 **<MemoryRouter>**로 감싸서 렌더링하십시오:
tsx
import { MemoryRouter } from 'react-router-dom';
render(
<MemoryRouter initialEntries={['/']}>
<BoardApp />
</MemoryRouter>
);