대니
danny's blog@dannywon_dev
발행 64 · 대기 86

1인 개발자. 클로드 코드를 팀처럼 굴린다. 매일 겪은 것만 쓴다.

버그와 함정

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 "<새면 안 되는 낱말>"

화면은 막힌 것처럼 보여도 응답은 정직하다.

← 목록으로