Next.js Server Action ID가 배포마다 달라진 원인
핫픽스 배포 전에 열어둔 탭에서 임시 저장과 수업 생성 요청이 모두 실패했다. Server Action ID가 달라지는 원인을 CodeBuild 작업 경로에서 찾았다.
핫픽스 배포가 나가고 20분쯤 뒤 CS 문의가 들어왔다. 선생님 한 분이 수업을 만들 수 없다는 내용이었고 CS 담당자가 나에게 확인을 요청했다.
Datadog Session Replay를 열어봤다. 선생님은 수업 제목과 소개를 입력한 뒤 임시 저장을 눌렀지만 요청이 실패했다. 마지막 수업 생성 요청도 마찬가지였다. 화면에는 An unexpected response was received from the server.라는 토스트가 떴다.
Next.js를 쓰면서 익숙하게 봤던 에러 문구였다. 같은 시간대의 ECS 로그에는 Failed to find Server Action이 남아 있었다. 수업 API까지 가기 전에 Next.js 서버에서 요청을 처리하지 못하고 있었다.
마침 20분 전에 핫픽스 배포도 나갔다. 배포 전부터 열려 있던 페이지에서 이전 빌드의 Server Action을 호출한 건 아닐까 싶었다.
새로고침하면 다시 저장할 수 있었다. 다만 지금까지 입력한 수업 정보가 사라졌다. 새로고침을 안내하면 선생님은 긴 수업 폼을 처음부터 다시 작성해야 했다.
오래 열어둔 탭에서만 재현됐다
dev에서 수업 생성 페이지를 열어둔 채 새 버전을 배포해봤다. 기존 탭에서 임시 저장을 누르니 같은 에러가 났다. 그 탭을 새로고침하거나 페이지를 다시 열면 정상적으로 저장됐다.
Next.js는 Server Action을 URL이 아니라 빌드할 때 만든 ID로 찾는다. 브라우저 탭에는 이전 빌드의 ID가 남아 있고 배포 뒤 서버는 새 manifest만 가지고 있었다.
배포 전부터 열려 있던 탭
→ 이전 Action ID로 요청
새로 배포된 서버
→ 새 manifest에서 이전 ID를 찾지 못함
→ Failed to find Server Action
배포 뒤에 페이지를 연 사용자에게는 문제가 없었다. 배포 전에 수업 생성 페이지를 열고 내용을 작성하던 사용자에게만 생기는 문제였다. 수업 생성처럼 오래 머무는 화면이라 더 쉽게 드러났다.
env를 고정해도 그대로였다
에러 문구를 그대로 검색하니 Next.js GitHub Discussion #58431에 배포 전부터 열어둔 탭에서 같은 Failed to find Server Action 에러가 난다는 글이 있었다. 댓글에서 가장 먼저 본 해결 방법은 NEXT_SERVER_ACTIONS_ENCRYPTION_KEY를 고정하는 것이었다.
Next.js self-hosting 가이드에도 빌드마다 새 암호화 키가 만들어지기 때문에 self-hosting 환경에서는 고정 키를 환경 변수로 넣으라고 되어 있었다. 고정 키를 만들어 빌드 환경 변수에 추가하고 다시 배포했다.
탭을 열어둔 채 확인하니 같은 에러가 났다. env만으로는 해결되지 않았다.
같은 Discussion에는 키를 설정한 뒤에도 에러가 계속 난다는 댓글이 있었다. 더 내려가 보니 CI의 절대 경로가 빌드마다 달라 Server Action ID가 바뀌었다는 사례가 나왔다. 그 사례도 AWS CodeBuild에서 고정 경로를 bind mount해 해결하고 있었다.
우리 빌드 환경도 CodeBuild였다. 소스를 받을 때마다 작업 경로의 숫자가 달라지고 있었다.
/codebuild/output/src111111111/src
/codebuild/output/src222222222/src
같은 소스와 같은 키를 서로 다른 절대 경로에서 빌드해봤다. Server Action ID는 달라졌다. 경로를 고정하고 두 번 빌드했을 때는 같은 ID가 나왔다. CodeBuild의 작업 경로가 배포마다 Action ID를 바꾸고 있었다.
buildspec에서 경로를 고정했다
수정한 건 buildspec 7줄이었다. CodeBuild가 내려받은 소스를 항상 같은 경로에서 보도록 bind mount했다. pre_build에서 경로를 만든 뒤 build와 post_build도 그 위치에서 실행했다.
pre_build:
commands:
- export FIXED_BUILD_DIR=/tmp/fixed-build
- mkdir -p "$FIXED_BUILD_DIR"
- mount --bind "$CODEBUILD_SRC_DIR" "$FIXED_BUILD_DIR"
- cd "$FIXED_BUILD_DIR"
build:
commands:
- cd "$FIXED_BUILD_DIR"
- yarn build
소스를 복사하는 대신 bind mount를 쓴 건 기존 node_modules와 .next/cache를 그대로 보기 위해서였다. Docker build와 정적 파일 업로드도 같은 데이터를 사용한다.
대신 CodeBuild에 bind mount 권한이 필요했다. 권한이 없으면 mount 단계에서 빌드가 바로 실패한다. 이미지가 push되기 전이라 실패한 빌드가 운영에 배포되지는 않는다.
우리 팀은 작업 브랜치를 AWS Pipeline의 Target Deploy로 dev에 먼저 올려본다. PR을 Draft로 열어두는 경우도 있고 테스트를 끝낸 뒤 리뷰를 요청하기도 한다. 이번에도 리뷰 전에 작업 브랜치를 dev에 배포했다. 수업 생성 페이지를 열어둔 채 한 번 더 배포하고 임시 저장과 수업 생성 요청을 다시 눌러봤다.
새로고침하지 않은 탭에서도 임시 저장과 수업 생성 요청이 정상 동작했다.
페이지를 자동으로 새로고침하는 방법은 넣지 않았다. 사용자 입력이 또 사라질 수 있고 생성 요청을 무작정 재시도하면 중복 처리될 가능성도 있었다.
다른 프로젝트에도 같은 에러가 있었다
PR을 올려 리뷰를 요청했는데 다른 프론트 개발자분이 댓글을 남겼다. 본인이 개발하는 프로젝트에서도 같은 에러가 나고 있는데 이 수정도 적용할 수 있는지 물었다.
코드를 바로 옮기기 전에 그 프로젝트의 dev 환경에서도 같은 상황을 만들어봤다. 수업 상세와 내 강의실 페이지를 열어둔 채 재배포했다. 새로고침하지 않고 Server Action을 쓰는 찜하기와 스케줄 요청을 누르니 아무 반응이 없었다. 캘린더에서 날짜를 옮길 때는 오류 토스트가 몇 초 간격으로 반복됐다. CloudWatch에는 앞에서 본 것과 같은 Failed to find Server Action 로그가 연달아 남았다.
처음 문제가 생긴 프로젝트에서 했던 것처럼 환경별 NEXT_SERVER_ACTIONS_ENCRYPTION_KEY를 추가하고 CodeBuild 경로를 고정했다. dev에서 다시 배포한 뒤 같은 기능을 눌렀을 때는 에러가 나지 않았다.
이 프로젝트는 내가 담당하던 서비스보다 사용자가 많았다. dev에서 재현하고 수정 뒤 동작까지 확인했지만 바로 운영에 배포하지는 않았다. 먼저 내가 담당한 서비스에 배포한 뒤 일주일 동안 같은 문제가 다시 생기지 않는지 확인했다. 그다음 같은 변경을 다른 프로젝트에도 배포했다.
고정 경로를 처음 적용하는 배포에서는 Action ID가 마지막으로 한 번 바뀐다. 실제로 이전 탭과 다음 배포의 ID가 같아지는 건 그다음 배포부터였다.
Datadog에서 처음 확인한 운영 이슈는 1건이었다. 수정 뒤 같은 이슈가 다시 잡히지는 않았다. 그렇다고 오류율이 개선됐다고 쓰기에는 표본이 너무 작다. 랜덤 빌드 경로를 없앤 뒤 같은 조건으로 재현했을 때 실패하지 않았다는 정도만 확인했다.