Koty's Blog
개발 경험과 지식을 공유하는 공간입니다.
개발 경험과 지식을 공유하는 공간입니다.

문제 상황 Next.js 개발 서버를 Docker로 띄우고, 소스 코드를 수정해도 HMR(Hot Module Replacement)가 동작하지 않았다. 이미지 출처: https://vercel.com/blog/turbopack 환경 OS: Windows 11 Next.js 16 (App Router) Webpack 방식으로 전환 관련 Github Discussion에서 참고한 결과, Turbopack을 포기하고 Webpack 방식으로 전환을 한 뒤, Polling 설정을 추가하는 방식이 있었다. next.config.ts에서 아래와 같은 옵션을 추가한다. const nextConfig = { webpack: (config) => { config.watchOptions = { poll: 1000, aggregateTimeout: 300, }; return config; }, }; 그리고 docker-compose.yaml 에 Polling 관련 옵션을 추가한다. environment: WATCHPACK_POLLING=true CHOKIDAR_USEPOLLING=true CHOKIDAR_INTERVAL=500 Webpack으로 전환 후 HMR이 동작은 했지만, 리빌딩하는 시간이 너무 길었고 간헐적으로 저장 이후에 수정 사항이 없는데도 반복적으로 리빌딩되는 현상이 생겨 메모리를 너무 많이 사용하는 문제가 발생되었다. 원인 분석 Turbopack은 파일 변경을 감지할 때 inotify 메커니즘을 사용한다. 파일이 바뀌면 OS가 이벤트를 날려주고, Turbopack이 그걸 받아서 HMR을 트리거한다. 문제는 Windows Docker가 WSL2 위에서 동작하는데, 프로젝트 파일이 C:\Users\... 같은 Windows 파일 시스템에 있으면 inotify 이벤트가 컨테이너까지 제대로 전달되지 않는다는 거다. Windows 파일시스템 (C:\) → WSL2 → Docker 컨테이너 ↑ inotify 이벤트가 여기서 소실됨 Docker 공식 문서에서도 이 문제를 언급하고 있다. "Linux containers only receive file change events, “inotify events”, if the original files are stored in the Linux filesystem. For example, some web development workflows rely on inotify events for automatic reloading when files have changed." Docker 공식 문서 - WSL "if you mount files that live in the Windows file system (such as with docker run -v /mnt/c/Users/Simon/windows-project:/sources ), you won’t get those performance benefits, as /mnt/c is actually a mountpoint exposing Windows files through a Plan9 file share. " Docker 공식 블로그 - WSL Best Practice 해결 방법 1. WSL2에 Ubuntu 설치 이미 설치된 Ubuntu가 있다면 이 단계는 건너뛰면 된다. WSL2에 Ubuntu가 없다면 먼저 설치해야 한다. PowerShell을 관리자 권한으로 실행한 뒤 아래 명령어를 입력한다. wsl --install -d Ubuntu-24.04 설치가 완료되면 자동으로 Ubuntu 터미널이 열리면서 계정 설정 화면이 나온다. username과 password를 설정하면 된다. 이후 접속은 윈도우 검색창에서 Ubuntu-24.04 를 검색하거나, PowerShell에서 아래 명령어를 입력하면 된다. wsl -d Ubuntu-24.04 2. 프로젝트 이동 및 설정 로컬 프로젝트를 복사를 통해 이동시키거나, git에 올린 경우 git clone을 통해 가져온다. 초기에 node.js의 경우 윈도우에 있는 경로로 설정되어 있을 가능성이 높다. which npm 명령어를 실행했을 때, /mnt/c/... 경로가 출려된다면 Windows npm을 바라보는 것이므로 별도로 nvm 등을 통해서 node.js를 설치해야 한다. 3. Docker Desktop WSL Integration 설정 Docker Desktop → Settings → Resources → WSL Integration → Ubuntu-24.04 토글 활성화 이렇게 하면 Ubuntu에서도 docker compose up 해도 Docker Desktop에서 컨테이너가 생성이 된다. 4. VSCode 연결 Ctrl + Shift + P → WSL: Connect to WSL using Distro → Ubuntu-24.04 선택 접속 이후 Open Folder를 통해 2단계에서 복사한 프로젝트 폴더로 이동한 뒤, 파일을 확인하고 수정할 수 있다. 요약 Windows + Docker 환경에서는 Turbopack의 HMR이 정상적으로 동작하지 않으므로, WSL2 파일시스템으로 옮겨서 실행해야 한다. 참고한 링크 Next.js 공식 문서 - Local Development Docker 공식 문서 - WSL Docker 공식 블로그 - WSL Best Practice GitHub Issue - Hot reload doesn't work inside Docker container GitHub Issue - Docker Compose Watch does not trigger hot reload with Turbopack GitHub Discussion - Hot Reload Not Working with WSL

문제 상황 Object Storage인 Cloudflare R2에 이미지를 올리고, Docker(Nginx + Next.js)로 배포한 앱에서 next/image로 이미지를 불러오려 하니 아래와 같은 에러가 발생했다. 환경 Next.js (App Router) Docker: Nginx + Next.js 멀티스테이지 빌드 Cloudflare R2 + 커스텀 도메인 삽질한 과정 1. remotePatterns 확인 // next.config.ts const nextConfig: NextConfig = { images: { remotePatterns: { protocol: "https", hostname: "images.kotys.dev" }, ], }, ..., }; next.config.ts 설정 자체는 문제없었다. 2. Docker 재빌드 next.config.ts 변경사항은 빌드 타임에 번들링되기 때문에 컨테이너 재시작만으로는 반영이 안 된다. docker compose build --no-cache docker compose up -d --no-cache 옵션으로 재빌드했지만 여전히 같은 에러가 발생했다. 3. 빌드 결과물에 config 반영 여부 확인 컨테이너 안에서 실제 빌드된 config를 확인해봤다. docker exec -it sh cat .next/required-server-files.json | grep -A 20 "remotePatterns" "remotePatterns": [ { "protocol": "https", "hostname": "images.kotys.dev" }, ..., ] 빌드에는 제대로 반영돼 있었다. 4. 네트워크 문제 확인 Next.js는 이미지 최적화 시 서버 사이드에서 직접 이미지를 fetch한다. 컨테이너 안에서 네트워크 접근이 되는지 확인했다. wget -O- https://images.kotys.dev/test.png 네트워크에도 문제가 없었다. 5. Cloudflare Cache Purge 혹시 Cloudflare가 이전의 잘못된 응답을 캐싱하고 있을 수도 있어서 Cache Purge를 수행했지만, 동일한 에러가 계속 발생했다. 원인 및 해결 Dockerfile의 멀티스테이지 빌드에서 runner 스테이지에 next.config.ts를 복사하지 않은 것이 원인이었다. Runner stage ..., COPY --from=builder /usr/src/app/.next ./.next COPY --from=builder /usr/src/app/public ./public COPY --from=builder /usr/src/app/next.config.ts ./next.config.ts # 추가 ..., next.config.ts가 없으면 Next.js 서버가 기본값으로 동작해서 remotePatterns가 빈 상태로 실행된다. [Next.js 공식문서에 아래와 같은 설명이 있다. next.config.js is a regular Node.js module, not a JSON file. It gets used by the Next.js server and build phases, and it's not included in the browser build. .next/required-server-files.json에는 config가 반영되어 있었다 하더라도, 서버 환경에서 next.config.ts가 없으면 소용이 없다. 수정 후 재빌드하니 문제가 해결되었다. 마치며 Docker의 멀티스테이지 빌드를 쓸 때 runner 스테이지에 필요한 파일을 빠뜨리기 쉽다. 공식 문서를 잘 참고하여 런타임에 필요한 설정 파일들을 누락하지 않도록 조심해야겠다. 비슷한 증상으로 헤매는 사람에게 도움이 되었으면 좋겠다. 참고한 링크 Next.js 공식 문서 - next/image remotePatterns Next.js 공식 문서 - Un-configured Host 에러 Next.js 공식 문서 - next.config.js GyanBlog - Docker 환경에서 next/image url parameter not allowed 해결

들어가며 TanStack Query의 Devtools에는 Trigger Error 버튼으로 쿼리의 에러 상태를 테스트할 수 있다. 그런데 이미 데이터가 캐시에 존재하는 상태에서 Trigger Error를 눌러도 에러가 발생하지 않아, Error Boundary가 작동되지 않는 현상을 확인했다. 이 글에서는 왜 이런 현상이 발생하는지 원인을 분석하고, 현재는 어떻게 해결됐는지에 대해서 정리한다. 문제 상황 useSuspenseQuery는 useQuery와는 다르게 데이터가 보장된 상태에서 컴포넌트를 렌더링한다. 그리고 로딩 상태와 에러 상태를 각각 `와 `가 처리하는 구조이다. }> }> function MyComponent() { const { data } = useSuspenseQuery({ queryKey: "category"], queryFn: fetchCategory, }); return {data.name}; } 이 구조에서 에러 처리를 테스트하려고 Devtools의 Trigger Error 버튼을 클릭하였지만, ``의 fallback UI가 전혀 나타나지 않았다. 원인 분석 useSuspenseQuery의 에러 처리 방식 useSuspenseQuery는 에러가 발생했을 때 throw를 통해 가장 가까운 Error Boundary로 에러를 전파한다. 그런데 여기에 핵심적인 조건이 있다. [공식 문서에 따르면, useSuspenseQuery의 throwOnError는 기본값이 (error, query) => typeof query.state.data === 'undefined'로 고정되어 있다. throwOnError: (error, query) => typeof query.state.data === 'undefined' 캐시에 데이터가 있으면 에러를 throw하지 않고, 컴포넌트를 그대로 렌더링하는 것이 의도된 동작이다. Devtools Trigger Error가 하는 일 (수정 전) 기존 Devtools Trigger Error 버튼의 동작 코드는 다음과 같았다. activeQueryVal.setState({ status: 'error', error, fetchMeta: { ...activeQueryVal.state.fetchMeta, __previousQueryOptions, } as any, }) 쿼리가 한 번이라도 성공적으로 데이터를 받아온 상태라면, data에는 이전에 fetch한 값이 남아 있다. 버튼을 눌러도 data는 지워지지 않았기 때문에, useSuspenseQuery는 에러를 throw하지 않고, Error Boundary가 동작하지 않는 것이다. 공식 PR을 통한 해결 이 문제는 Discussion #10044에서 제기되었고, PR #10072를 통해 수정되어 @tanstack/[email protected]에 반영되었다. 수정된 코드는 간단하다: activeQueryVal.setState({ data: undefined, // 추가된 부분 status: 'error', error, fetchMeta: { ...activeQueryVal.state.fetchMeta, __previousQueryOptions, } as any, }) 이제 "Trigger Error"를 누르면 data가 undefined로 설정되기 때문에, useSuspenseQuery가 에러를 정상적으로 throw하고 Error Boundary가 동작한다. 핵심 정리 | 구분 | 수정 전 | 수정 후 | | -------------------------- | ---------------- | -------------------- | | Trigger Error 시 data 상태 | 기존 데이터 유지 | undefined로 초기화 | | Error Boundary 동작 | ❌ 동작 안 함 | ✅ 정상 동작 | | 적용 버전 | 5.92.x | 5.93.0 | 마치며 라이브러리 내부 동작을 이해하지 못하면 이런 함정에 빠지기 쉽다. useSuspenseQuery가 에러를 throw하려면 단순히 status: 'error'가 아니라 data: undefined도 함께 충족되어야 한다는 점을 기억하자. 현재는 @tanstack/[email protected] 이상으로 업그레이드하면 Trigger Error 버튼 하나로 Error Boundary 테스트가 가능하다. 참고한 링크 Discussion #5956 - Comment Discussion #10044 PR #10072 @tanstack/query-devtools 5.93.0 ChangeLog
2,588
3
0