이번 장에서는 배포를 하는 과정에서 마주한 문제들을 정리해 보려고 한다
첫 번째로 마주했던 문제는 CSS와 JavaScript가 404로 표시되는 것이었다
개발자 도구에는 다음과 같은 오류가 표시 됐다

원인
1. 서비스는 도메인 루트가 아니라 다음과 같은 하위 경로에 배포됐다
/junhyeok/pj
하지만 프론트엔드는 처음에 정적 파일을 도메인 루트에서 요청했다
https://daisy.wisoft.dev/_next/static/...
실제로 필요한 공개 주소는 다음과 같았다
https://daisy.wisoft.dev/junhyeok/pj/_next/static/...
따라서 빌드 설정에 외부 공개 경로를 반영해야 했다
APP_BASE_PATH=/junhyeok/pj
2. Nginx가 공개 경로를 제거함
공개 경로를 추가한 후에도 CSS와 JavaScript가 계속 404를 반환했다
Nginx 설정을 확인해 보니 다음과 같은 방식으로 요청을 전달하고 있었다
location ~ ^/([^/]+)/([^/]+)(/.*)?$ {
set $member $1;
set $app $2;
set $extra_path $3;
proxy_pass http://$service$extra_path$is_args$args;
}
예를 들어 브라우저가 다음 주소를 요청한다
/junhyeok/pj/_next/static/css/index.css
Nginx는 앞의 /junhyeok/pj 를 제거하고 컨테이너에는 다음 주소를 전달한다
/_next/static/css/index.css
즉, 외부 주소와 컨테이너 내부 주소가 서로 달랐다
브라우저: /junhyeok/pj/_next/static/...
컨테이너: /_next/static/...
해결 방법
프론트 설정을 다음 두 부분으로 분리했다
- 브라우저가 사용하는 정적 파일 주소에는 /junhyeok/pj 적용
- 컨테이너는 Nginx가 접두사를 제거한 루트 경로에서 정적 파일 제공
이후 Nginx 동작을 모의해 다음 파일들을 확인했다
CSS 200
JavaScript 200
사진 200
favicon 200
3. 슬래시 유무에 따른 404
정적 파일 문제를 해결한 후에도 다음 주소는 404를 반환했다
https://daisy.wisoft.dev/junhyeok/pj
하지만 끝에 / 를 붙이면 정상적으로 열렸다
https://daisy.wisoft.dev/junhyeok/pj/
확인 결과는 다음과 같았다
/junhyeok/pj → 404
/junhyeok/pj/ → 200
Nginx 정규식의 선택적인 추가 경로 때문에 두 요청이 컨테이너에 다르게 전달된 것이 원인이었다
공용 Nginx 설정은 다른 서비스도 사용하므로 수정하지 않고, 프론트 애플리케이션 /junhyeok/pj 경로의 홈 화면 별칭을 추가했다
4. favicon.ico 404 문제
브라우저는 문서에 favicon 주소가 지정되지 않으면 자동으로 다음 주소를 요청할 수 있다
https://daisy.wisoft.dev/favicon.ico
하지만 이 주소는 소제동 프로젝트의 Nginx 경로와 일치하지 않아 404가 발생했다
따라서 HTML 메타데이터에 favicon 주소를 명시했다
/junhyeok/pj/favicon.svg
Nginx는 이 요청에서 /junhyeok/pj를 제거하고 컨테이너의 다음 파일을 제공한다
/favicon.svg
배포 후 확인할 명령
Raspberry Pi의 컨테이너 상태를 확인한다
docker compose ps
문제가 있다면 로그를 확인한다
docker compose logs --tail=100 frontend
docker compose logs --tail=100 backend
공개 페이지 상태를 확인한다
curl -sS -o /dev/null -w 'PAGE: %{http_code}\n' \
https://daisy.wisoft.dev/junhyeok/pj
favicon 상태도 확인한다
curl -sS -o /dev/null -w 'FAVICON: %{http_code}\n' \
https://daisy.wisoft.dev/junhyeok/pj/favicon.svg
정상이라면 다음과 같이 표시되어야 한다
PAGE: 200
FAVICON: 200
브라우저에서는 이전 HTML이나 JavaScript가 캐시되어 있을 수 있으므로 강력 새로고침을 수행한다
처음 서버 배포를 하며 알게 된 점
로컬 실행과 실제 배포는 다르다
로컬에서는 / 에서 실행되던 애플리케이션이 실제 서버에서는 /junhyeok/pj 와 같은 하위 경로에서 실행될 수 있다
따라서 다음 항목을 함께 확인해야 한다
- HTML 문서 경로
- CSS와 JavaScript 경로
- 이미지 경로
- favicon 경로
- Nginx가 경로를 보존하는지 제거하는지
Docker 컨테이너 상태만으로는 충분하지 않다
컨테이너가 healthy여도 외부 Nginx 경로가 잘못되면 사용자는 페이지에 접근할 수 없다
최소한 다음 세 구간을 각각 확인해야 한다
1. 컨테이너 내부 주소
2. Raspberry Pi 공개 포트
3. 외부 도메인 주소
공용 Nginx는 함부로 수정하면 안 된다
이번 Nginx 설정은 여러 사용자의 서비스를 하나의 도메인에서 구분하는 공용 라우터였다
한 프로젝트를 고치기 위해 공용 규칙을 변경하면 다른 서비스에 영향을 줄 수 있다
따라서 가능한 경우 애플리케이션이 기존 프록시 규칙에 맞도록 구성하는 것이 안전하다
배포 서버에서는 직접 코드를 수정하지 않는다
개발을 로컬에서 하고, GitHub에 푸시한 뒤 Raspberry Pi에서는 다음 흐름만 사용하는 것이 좋다
로컬 수정
→ 테스트
→ Git commit
→ Git push
→ Raspberry Pi에서 git pull --ff-only
→ Docker 이미지 재빌드
→ 컨테이너 재실행
마무리
이번 프로젝트를 통해 React 화면을 만드는 것뿐만 아니라 다음 과정까지 처음 경험했다.
- 프론트엔드와 백엔드 모노레포 구성
- Docker 이미지 빌드
- Docker Compose 서비스 구성
- Raspberry Pi 온프레미스 배포
- Nginx 하위 경로 라우팅
- 정적 파일 404 분석
- GitHub를 이용한 배포 소스 관리
처음에는 CSS가 적용되지 않는 단순한 문제처럼 보였지만, 실제 원인은 브라우저가 사용하는 공개 경로와 Nginx가 컨테이너에 전달하는 내부 경로의 차이였다.
서버 배포에서는 “컨테이너가 실행됐는가”뿐만 아니라 “사용자의 요청이 어떤 경로로 컨테이너까지 도착하는가”를 함께 이해해야 한다는 점을 알게 됐다.
다음 단계에서는 매번 직접 git pull을 실행하고 Docker 이미지를 다시 빌드하던 과정을 CI/CD로 자동화해 보려고 한다. 수정사항을 GitHub에 푸시하면 더 간단하게 서버에 반영할 수 있도록 구성하는 것이 다음 목표다. ☺️
'프로젝트(스마트 휴먼 라이프)' 카테고리의 다른 글
| 처음 해보는 서버 배포: React·Spring Boot 프로젝트를 Raspberry Pi에 올리기 (0) | 2026.08.16 |
|---|---|
| AI를 ‘쓰는 것’에서 ‘잘 쓰는 것’으로: 소제동 인터랙티브 지도 개발기 (0) | 2026.08.16 |