대니
danny's blog@dannywon_dev
발행 126 · 대기 180

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

클로드 코드

Next.js route group 을 (admin) 으로 다시 묶었다

톤으로 폴더를 갈랐더니 /admin 이 두 그룹에 흩어졌다. 반대 근거가 틀린 걸 확인한 과정

route group 으로 app/ 을 나누고 나면, 한 경로 계열이 두 그룹에 걸쳐 흩어지는 일이 생긴다.

newbie_schoolapp/ 을 route group 으로 다시 세우는 작업이었다. 화면 갈래가 셋이다 — 서비스 고르는 화면, 어두운 콘솔(해커톤·관리자), 흰 바탕(방과후 수업 관리). 그래서 1차로 (chooser) · (hackathon) · (afterschool) 세 그룹을 만들었다.

결과가 이랬다.

  • /admin, /admin/data-room, /admin/hackathon, /admin/mapping(hackathon)
  • /admin/school(afterschool) 안 (이 화면만 흰 바탕이라)

트리를 보다가 걸렸다. admin 이 두 군데로 갈려 있는데 /admin 경로로 들어가면 누구의 admin 이 열리는 건가. 그래서 물었고, 이어서 (admin) 을 따로 만들어 admin 을 한 그룹으로 묶는 게 낫지 않냐고 제안했다.

클로드는 반대했다

돌아온 답은 반대였다. 근거는 이랬다.

그룹을 나누는 축이 「톤」이냐 「기능 영역」이냐가 문제다. 레이아웃이 실제로 하는 일이 껍데기·머리글·배경이니 톤으로 가르는 게 route group 의 목적에 맞다. 거기에 영역을 하나 섞으면 나중에 "이건 왜 톤으로 갈렸고 저건 왜 영역으로 갈렸지"를 매번 판단하게 된다.

보강 근거도 붙었다. (admin) 그룹을 만들어도 가장 큰 이득인 「공통 인증을 layout 하나로」는 이 레포에서 쓸 수 없다는 것이었다. 레이아웃에서 막으면 하위 페이지가 이미 실행돼 자료가 새어 나간다는 실측 기록이 남아 있어서, 문 검사는 각 페이지가 자료를 꺼내기 전에 걸게 돼 있다. 그러니 (admin)/layout.tsx 는 빈 껍데기가 되고, 남는 건 파일이 한 곳에 모인다는 것뿐인데 대가는 폴더가 한 겹 깊어지는 것이다.

말은 맞았다. 그런데 납득이 안 돼서 한 번 더 밀었다. (admin)/admin/page.tsxlayout.tsx 를 두면 프로젝트 전체를 관리하는 admin 페이지를 만들 수 있지 않냐고.

/admin 을 열어보니 이미 통합 콘솔이었다

그제서야 /admin 이 지금 뭘 하는 화면인지 파일을 열어봤다. 그전까지는 흩어져 있다는 사실만 보고 판단하고 있었다.

열어보니 이미 통합 콘솔이었다. 카드 4장이 전 영역 입구였다 — 자료실 관리, 해커톤 관리, 방과후 학교 관리, 매핑 조회. 문도 하나였다. isAdmin() 을 넷이 다 쓰고, 방과후 쪽이 쓰는 requireSchoolAdmin() 도 열어보니 이런 한 줄이었다.

export async function requireSchoolAdmin(): Promise<void> {
  if (!(await isAdmin())) redirect("/admin");
}

반대 근거의 전제가 틀렸다. "admin 을 영역으로 묶으면 축이 둘로 섞인다"고 했는데, /admin 은 이미 하나의 영역으로 설계돼 있었다. 대시보드도 하나, 문도 하나. 축이 섞이는 게 아니라 파일이 설계를 못 따라가고 있던 것이다.

톤은 중첩 그룹으로 갈랐다

추천을 뒤집고 (admin) 으로 묶었다. 남은 문제는 톤이다. /admin(어두운)과 /admin/school(흰 바탕)은 URL 이 부모-자식인데 껍데기는 남남이라, 중간에 layout 하나를 두면 둘 다에 걸린다. 그래서 중첩 그룹으로 풀었다.

app/(admin)/admin/
  (console)/layout.tsx    →  /admin · data-room · hackathon · mapping
  (school)/layout.tsx     →  /admin/school

(admin)/layout.tsx 는 일부러 안 만들었다. 거기 공통 문을 넣고 싶어지는데 그게 위의 자료 누출 함정이다. 빈 폴더로 두는 게 맞는 답이었다.

빌드하니 라우트 목록이 리팩터 전과 완전히 동일했다. URL 은 하나도 안 바뀌었다.

남는 것

반대 근거는 「구조에 대한 일반론」이었고, 뒤집은 근거는 「그 파일을 열어본 것」이었다. 일반론은 언제나 그럴듯하다. /admin/page.tsx 를 처음에 열었으면 반대할 이유가 없었다.

첫 질문("누구의 admin 이 열리지?")에 클로드는 실측으로 답했다 — 500 에러로 잡히니 안전하다고. 답 자체는 맞았다. 다만 그건 안전한지를 묻는 질문이 아니라 이 구조가 말이 되는지를 묻는 질문이었다. 한 번 더 밀지 않았으면 그대로 넘어갔을 자리다.

한 가지 더. 검증 스크립트에서 grep -c 'mt-13' 결과를 = "1" 로 비교했는데, 마크업에 2줄로 나와서 "어두운 껍데기가 안 붙고 있다"는 보고가 나왔다. 껍데기는 멀쩡했고 비교 조건이 틀렸다. -gt 0 으로 고치니 전부 정상이었다. 검사가 실패를 알릴 때, 검사 자체를 먼저 의심해야 하는 자리가 있다.

← 목록으로