Next.js layout.tsx 로 막은 비번 게이트가 안 막힌 이유
화면엔 비번 창만 떴는데 curl 응답에는 학생 이름이 다 들어 있었다. App Router 레이아웃 인증의 함정.
학교 네 곳이 쓰는 방과후 출결 앱에 비번 게이트를 붙였다. 로그인·회원가입은 안 만들기로 했다 — 쓰는 사람이 여덟 명뿐이라 계정을 만들 이유가 없었다. 대신 학교마다 공용 비번 하나.
Next.js App Router 라서 자연스럽게 레이아웃에서 막았다.
// app/s/[slug]/layout.tsx
export default async function SchoolLayout({ children, params }) {
const school = await getSchoolBySlug((await params).slug)
if (!canOpen(await readGate(), school.id)) {
return <GateForm school={school} /> // children 을 안 그린다
}
return <>{children}</>
}
브라우저로 열어 봤다. 비번 창만 떴다. 명단도 회차도 안 보였다. 됐다고 생각했다.
그런데 눈으로 본 것 말고 응답 자체를 확인해 봤다.
curl -s http://localhost:3100/s/garam | grep -oE "발로란트|김민준|오늘 수업"
# 발로란트
# 김민준
# 오늘 수업
쿠키가 하나도 없는데 반 이름과 학생 이름이 다 들어 있었다. 미성년자 명단이다.
레이아웃이 고를 수 있는 건 「그릴까 말까」뿐이다
처음엔 내 코드가 틀린 줄 알았다. canOpen 이 잘못 판정했나, 쿠키를 잘못 읽었나. 둘 다 아니었다 — 게이트는 정확히 동작했고 <GateForm/> 도 제대로 그려졌다.
문제는 다른 데 있었다. 레이아웃과 페이지는 나란히 실행된다. 레이아웃이 children 을 안 그려도, 그 아래 페이지 컴포넌트는 이미 돌아서 데이터베이스 조회까지 끝낸 뒤다. children 은 레이아웃이 렌더될 때 만들어지는 게 아니라 이미 만들어진 채로 건네진다.
그러니까 레이아웃이 고를 수 있는 건 「그릴까 말까」뿐이고, 「돌까 말까」는 애초에 레이아웃 손을 떠나 있다.
제일 무서웠던 건 화면으로는 구분이 안 된다는 점이었다. 비번 창만 보이니까 막힌 것처럼 보인다. 사람 눈으로 열 번을 봐도 못 잡는다.
검사를 화면에서 떼어 자료 쪽으로 내렸다
// lib/queries.ts
export async function requireSchool(slug: string) {
const school = await getSchoolBySlug(slug)
if (!school) notFound()
if (!canOpen(await readGate(), school.id)) redirect(`/s/${slug}/gate`)
return school
}
export async function getSchoolBoard(slug: string) {
const school = await requireSchool(slug) // 자료를 꺼내기 전에 문부터
…
}
바뀐 게 위치만은 아니다. 성질이 바뀌었다. 전에는 「화면이 검사를 하는」 구조라서 화면을 하나 새로 만들 때마다 검사를 붙이는 걸 기억해야 했다. 지금은 자료가 검사를 들고 있다. 화면이 깜빡해도 자료를 못 얻는다. 깜빡할 수 있는 자리를 없앤 것이다.
비번 화면은 /s/[slug]/gate 라는 별도 주소로 뺐다. 레이아웃은 통과했을 때만 머리글을 그리는 것까지만 한다 — 그건 모양 문제라 레이아웃이 맞다.
곁가지로 하나가 더 사라졌다. 원래 들어가려던 주소로 되돌려 보내려고 next 파라미터를 폼에 실었었고, 그게 오픈 리다이렉트가 될 수 있어서 검사 함수를 따로 붙여 뒀었다. 게이트를 별도 주소로 빼면서 되돌아갈 곳을 홈 하나로 고정하니 그 함수가 통째로 필요 없어졌다. 막을 것을 잘 막은 게 아니라, 막을 것이 애초에 안 생기게 됐다.
남긴 것
전역 규칙에 한 줄 올렸다. 이건 이 프로젝트만의 얘기가 아니라 App Router 를 쓰는 모든 앱에 참이라서.
그리고 확인하는 방법을 규칙에 같이 적었다. 눈으로 보고 판정하지 말 것.
curl -s <주소> | grep -oE "<새면 안 되는 낱말>"
화면은 막힌 것처럼 보여도 응답은 정직하다.