빌드 성공, 배포 성공, 로그인한 사용자만 전부 500 — ignoreBuildErrors 가 숨긴 52건
빌드 성공, 배포 성공, 로그인한 사용자만 전부 500 — ignoreBuildErrors 가 숨긴 52건
결과부터 — 빌드도 배포도 초록불, 로그인한 사용자만 500
9월 2일 저녁, 로또 분석 서비스 pick6.kr 에 보안 수정을 배포했어요. 빌드 성공, 배포 성공, 배포 직후 점검도 통과였습니다. 로그인 없이 보호된 API 몇 개(프로필·관리자 알림·관리자 통계·광고 설정)를 찌르면 401(로그인이 필요하다는 응답)이 돌아오고, 공개 페이지는 200, 관리자 페이지는 로그인 화면으로 넘어갔어요. 점검한 범위 안에서는 전부 통과였습니다.
그런데 그 배포본에서는 로그인한 사용자가 쓰는 기능 상당수가 죽어 있었어요. 게시글 댓글 작성·수정·삭제, 댓글 좋아요, 공유 좋아요·저장, 리뷰와 꿈해몽 수정·삭제, 구매내역 조회·등록·당첨확인, 아바타 업로드, 관리자의 공지 지정 — 이것들이 전부 500(서버 내부 오류)이 나는 상태였습니다. 대부분은 로그인 확인(401) 뒤에서 터져 로그인한 사람에게만 나타났고, 댓글 작성과 관리자 공지 지정 두 경로는 로그인 확인보다 앞에서 session 을 읽어서 로그인 여부와 상관없이 터졌어요.
원인은 코드 어디에도 선언된 적 없는 변수 session(세션 — 로그인한 사람이 누구인지 서버가 기억하는 것) 이 14개 API 파일에 52군데 남아 있던 것이고, 빌드가 그걸 잡지 못한 이유는 설정 한 줄 — typescript: { ignoreBuildErrors: true }(타입 검사가 실패해도 빌드를 계속하라는 설정) 였습니다. 알아챈 건 다음 날 다른 정리 작업을 하다가였어요. 9월 2일 저녁 배포부터 9월 3일 재배포까지, 라이브는 그 배포본이었습니다.
원인 — 인증 라이브러리를 바꾸며 남긴 session 52곳, 그걸 덮은 설정 한 줄
배경은 한 문장이면 됩니다. 9월 2일에 실제로는 쓰이지 않던 로그인 라이브러리를 정리하면서, 로그인한 사람을 확인하는 함수를 옛 getServerSession() 에서 Supabase 의 getUser() 로 바꿨어요. 그 작업은 각 API 의 첫 부분 — "로그인했는지 확인하고 아니면 401" — 만 바꿨습니다. 그 아래에서 로그인한 사람의 ID 를 쓰는 session.user.id 같은 참조는 그대로 남았어요. session 이라는 변수는 이제 어디에도 없는데요.
이건 타입체크(코드를 실행하지 않고, 변수·함수가 서로 맞게 쓰였는지 미리 검사하는 것)가 가장 잘 잡는 종류의 오류입니다. 실제로 tsc(타입체크를 돌리는 명령) 한 줄을 돌리니 Cannot find name 'session' 이 52건, 14개 파일에서 바로 나왔어요. Next.js 는 빌드할 때 이 검사를 같이 돌리고, 오류가 있으면 빌드를 멈춥니다. 멈췄어야 했어요.
멈추지 않은 이유가 next.config.mjs 의 바로 그 설정입니다. 이 프로젝트에는 이 설정이 켜져 있었어요. AI 로 코딩하다 빌드가 빨간 글씨로 멈추면 가장 먼저 찾게 되는 설정이고, 켜면 정말로 빌드가 초록불이 되죠. 그 초록불이 이번 사고의 시작이었습니다.
왜 몰랐나 — "무인증 401 은 정상" 스모크는 로그인 경로를 안 지난다
배포 직후 스모크 테스트(대표 경로 몇 개만 찔러 보는 확인)를 분명히 했는데 왜 못 잡았을까요. 문제가 난 API 하나의 구조를 단순화하면 이렇습니다.
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); // 로그인 안 한 요청은 여기서 끝난다
}
// ... 로그인한 요청만 아래로 내려온다
if (existingShare.author_id !== session.user.id) { // session 은 어디에도 선언돼 있지 않다 → ReferenceError → 500
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
로그인 없이 이 API 를 부르면 두 번째 줄에서 401 을 받고 끝나요. 그건 정상 동작이고, 스모크 테스트는 "잠겼다, 통과"라고 기록합니다. 문제의 session 은 그 아래에 있어서 로그인한 요청만 거기까지 내려가고, 거기서 없는 변수를 읽다가 터져요. 즉 9월 2일의 검증이 확인한 것은 "문이 잠겼는가"였지 "문을 열면 안이 동작하는가"가 아니었습니다.
그러니 초록불이 두 개 겹쳤어요. 빌드는 검사를 끄고 통과했고, 스모크는 잠긴 문만 확인하고 통과했어요. 두 검사 모두 각자 맡은 일은 제대로 했는데, 어느 쪽도 "로그인한 사람의 요청"까지 가 보지 않았던 거예요.
고친 것과 닫은 방법 — tsc 한 줄, 다른 AI 가 더 찾은 3+1, 진짜 세션으로 라이브 E2E
고치는 것 자체는 단순했습니다. 14개 파일에서 session 을 user 로 바꾸고, 이름·프로필 이미지·권한 같은 속성은 Supabase 가 주는 자리(user_metadata·app_metadata)로 옮겼어요. 다시 tsc 를 돌려 Cannot find name 이 59건에서 0건이 된 걸 확인했어요. 59 는 다른 미선언 이름 7건까지 포함한 전체이고, session 만 센 값이 52 입니다.
여기서 끝내지 않고 다른 AI(Codex)에 반증을 시켰습니다. 10개 주장 중 PASS 4, PARTIAL 6. 요청 본문에 실린 사용자 ID 를 그대로 믿는 같은 패턴이 즐겨찾기·예측 아카이브 저장·프로필 생성 세 군데 더 있었고, 좋아요를 이메일 기준으로 저장하게 바꾼 건 제가 고치면서 새로 만든 오류(회귀)였어요. 전부 세션 기준으로 바꿨습니다. 이 교차검증 루프 자체는 따로 쓸 이야기라 여기서는 결과만 적습니다.
마지막이 이 사고의 진짜 마감이에요. 로그인한 상태로 라이브를 찔러 봐야 했습니다. 브라우저 대신 스크립트로 진짜 세션을 만들었어요. 서버 전용 관리자 키(서비스 롤)로 임시 계정을 만들고 비밀번호로 로그인한 뒤, 앱이 쓰는 것과 똑같은 형식의 세션 쿠키를 뽑아 그 쿠키로 라이브 API 를 호출하는 방식입니다. E2E(실제 사용자처럼 처음부터 끝까지 눌러 보는 검증)를 사람 손 없이 돌린 거죠.
재배포한 라이브에서 이번에 찌른 경로 — 댓글 작성·수정·삭제, 댓글 좋아요, 구매내역 조회, 공유 저장목록 조회, 관리자 공지 지정(관리자가 아닌 계정은 403), 세션 확인 — 는 여섯 경로(14개 파일 중 6개)였고, 전부 정상이었어요. 게시글 좋아요는 토글 2회로 원상 복귀까지 확인했습니다. 리뷰·꿈해몽 수정과 삭제, 공유 좋아요·저장, 아바타 업로드, 구매내역 등록·당첨확인은 이번엔 찌르지 않았고, tsc 0건이 근거예요. 임시 계정과 자동 생성된 프로필, 테스트 댓글은 지웠어요.
솔직하게 하나 남깁니다. ignoreBuildErrors 는 오늘도 켜져 있어요. 이번엔 tsc 를 손으로 돌려 잡았고, 설정을 끄는 것과 타입 검사를 배포 전 절차에 넣는 것은 아직 못 했습니다 — 다음 할 일이에요.
next.config 에 이 줄이 있다면, AI 에게 npx tsc --noEmit 을 한 번 돌려 달라고 해 보세요. 빨간 줄이 0 이 아니면 그 오류는 지금도 어딘가에서 터질 수 있어요.
검사를 끄면 오류는 없어지는 게 아니라 검사가 안 보는 곳으로 옮겨 갑니다. 이번엔 그곳이 "로그인한 사용자의 요청"이었고, 무인증 401 만 확인한 스모크 테스트는 거기까지 가 보지 않았어요.
댓글
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요.