홈은 조각 여덟 개로 쪼개져 있고, about 은 91줄을 한 파일에 다 쓴다.

app/page.js         31줄   Hero · ServiceIntro · WhyUs · Process · ContactCTA · Reviews · BottomCTA
app/about/page.js   91줄   전부 한 파일
app/care/page.js   109줄   전부 한 파일

여기서 page.js는 아무 이름이나 붙인 파일이 아니라, Next.js의 App Router가 폴더 주소마다 정해 둔 특수 파일 이름이다. app/ 폴더 밑에 page.js라는 이름으로 파일을 두면 그 폴더 경로가 곧 그 파일이 그리는 화면 주소가 된다 — app/about/page.js는 /about 페이지를 그린다는 뜻이다.

그 차이가 무엇인지 적힌 곳이 없다. 물어봤더니 홈을 여덟로 쪼갠 데 기준이 있었던 것이 아니라 그냥 그렇게 됐다.


먼저: 이미 정해진 것 둘

이 글은 앞선 표준 위에 선다.

둘 다 「이미 컴포넌트인 것」을 다룬다. 이 글은 그 앞의 질문에 답한다 — 애초에 무엇을 뺄 것인가.


먼저: 클라이언트 경계는 이미 잘 그어져 있다

이건 원칙이 아니라 실측이고, 이 표준이 새로 만들 것이 아니라 이미 하고 있는 것을 규칙으로 꺼낸 것이라는 근거다.

Next.js의 App Router에서 컴포넌트는 기본이 서버 컴포넌트다 — 브라우저가 아니라 서버에서만 실행된다. 그중 상태나 클릭처럼 브라우저에서 동작해야 하는 부분만 따로 떼어 클라이언트 컴포넌트로 만드는데, 그 나누는 금을 이 글에서는 클라이언트 경계라고 부른다.

page.js 가 서버 컴포넌트로 남고, 브라우저 코드가 필요한 부분만 조각으로 떨어져 있다.

// app/inquiries/page.js — 서버. metadata 만 들고 조립한다
export const metadata = { title: '1:1 문의', robots: { index: false } };
export default function InquiriesPage() {
  return <><InquiriesPageClient /><MobileCtaBar /></>;
}

한 줄씩 보면, export const metadata = {...}는 이 페이지의 제목과 검색 노출 여부를 metadata로 내보내는 줄이다. export default function InquiriesPage()는 이 페이지가 실제로 화면에 그릴 내용을 돌려주는 함수이고, return <>...</>;의 <>와 </>는 자바스크립트 코드 안에 HTML처럼 쓰는 JSX 문법으로, 이름이 없는 빈 껍데기(Fragment)로 <InquiriesPageClient />와 <MobileCtaBar /> 두 컴포넌트를 나란히 묶어서 돌려준다는 뜻이다.

그리고 떨어져 나간 쪽은 정말로 브라우저 코드가 필요하다.

InquiriesPageClient  219줄 · useState 12개   검색 · 목록 · 커서 페이징
Header                67줄 · 상태 2 · 효과 2  스크롤 · 메뉴 열림 · 현재 경로
Hero                  78줄 · 슬라이더 index   자동 넘김 · 점 클릭

여기서 useState는 컴포넌트가 값을 기억했다가 바뀌면 화면을 다시 그리게 하는 React 기능이고, 그 값 자체를 상태라고 부른다. 효과는 useEffect로 적는 코드로, 화면을 그린 뒤에 한 번 더 실행해 서버 요청이나 타이머처럼 화면 바깥과 맞춰야 하는 일을 한다.

셋 다 상태가 전체를 관통해서 더 좁힐 여지가 없다. 반대로 홈의 섹션 여덟 중 Hero 와 ContactCTA 둘만 'use client' 이고 나머지 여섯은 서버 컴포넌트로 남아 있다.


원칙 1. 빼는 이유는 둘뿐이다

무엇을 별도 파일로 떼어내는가.

안 A — 파일이 길어지면 뺀다. 읽기 편해진다는 것이 이유다. 버린 이유는 몇 줄이 기준인지를 정해야 한다는 것이다. 100줄인가 150줄인가. 그리고 화면별 대응 표준에서 이미 같은 함정을 피했다 — “얼마가 큰지를 매번 판단해야 하고, 오늘 작던 것이 데이터가 늘면 커진다.”

그리고 길이로 빼면 <div> 하나짜리 파일이 수십 개 생긴다. 파일을 열 때마다 다른 파일로 건너뛰게 되어 읽기가 오히려 어려워진다. 읽기 편하자고 뺐는데 반대가 된다.

골랐다 — 안 B, 이유는 둘뿐이다.

1. 브라우저 코드가 필요하다      →  그 부분만 떼어 'use client' 를 붙인다
2. 다른 갈래에서도 쓴다          →  컴포넌트 자리 표준이 정한 자리로 올린다

안 A 가 못 지켜주는 것은 뺄 때마다 얻는 것이 분명한 것이다. 1번은 브라우저로 내려가는 코드가 줄고, 2번은 같은 것을 두 번 만들지 않게 된다. 길이로 빼면 얻는 것이 「짧아졌다」뿐이고, 그건 파일이 늘어난 대가와 상쇄된다.


원칙 2. 길면 같은 파일 안에서 나눈다

파일이 길어 읽기 힘든 것은 진짜 문제다. 다만 별도 파일로 뺄 이유는 아니다.

// 한 파일 안에서 나눈다. import 가 늘지 않는다
function AboutHero() { ... }
function AboutStory() { ... }

export default function AboutPage() {
  return <main><AboutHero /><AboutStory /></main>;
}

얻는 것은 같고 잃는 것이 없다. 이름이 붙어 읽기 쉬워지는 것은 똑같은데, 파일을 오가지 않아도 되고 import 줄도 안 는다.

그러다 원칙 1의 이유가 생기면 그때 파일로 뺀다. 함수가 이미 나뉘어 있으므로 옮기는 일이 잘라내기 한 번이다.

이미 파일로 쪼갠 것을 되돌리지 않는다. 홈의 섹션 여덟이 그렇다 — 기준 없이 쪼갠 것이지만 되돌려서 얻는 것이 없고, 되돌리는 동안 깨질 위험만 있다.


원칙 3. 'use client' 는 가장 안쪽에 붙인다

원칙 1의 첫 번째 이유를 실제로 살리는 방법이다.

page.js 에 'use client'    →  그 아래 전부가 브라우저로 내려간다
조각에만 'use client'      →  그 조각만 내려간다

page.js 는 서버 컴포넌트로 남긴다. metadata 는 서버에서만 내보낼 수 있고, 서버 컴포넌트의 코드는 브라우저 번들에 안 실린다.

지금 코드가 이미 그렇게 되어 있다.

app/faq/page.js         16줄 · 서버 · metadata + 조립
app/faq/FaqPageClient   상태가 필요한 부분만

정적 export 라도 이득이 있다. HTML 은 어차피 빌드 때 만들어지지만, 클라이언트 컴포넌트는 브라우저에서 다시 살아나야(hydration) 해서 그만큼 JS 가 실려 온다. 서버 컴포넌트는 그 과정이 없다.

대가는 경계를 넘길 때 값을 넘겨야 하는 것이다. 서버 컴포넌트가 클라이언트 컴포넌트에 넘기는 값은 직렬화되는 것이어야 한다 — 함수는 못 넘긴다. 함수는 코드 자체와 그 코드가 가리키는 주변 변수까지 담고 있어서, 글자·숫자처럼 줄로 늘어선 순수한 데이터로 바꿀 수 없기 때문이다. 그래서 경계를 좁히려다 오히려 구조가 꼬이면, 그 조각은 통째로 클라이언트로 두는 것이 낫다. Header 와 Hero 가 그런 경우다.


판단 기준 정리

질문 답 결론
언제 별도 파일로 빼나 이유가 둘 중 하나일 때 브라우저 코드가 필요하거나, 다른 갈래에서도 쓰거나
파일이 길다 빼는 이유가 아니다 같은 파일 안에서 함수로 나눈다
몇 줄이면 빼나 그런 숫자를 두지 않는다 오늘 작던 것이 데이터가 늘면 커진다
이미 기준 없이 쪼개진 것 되돌리지 않는다 되돌려서 얻는 것이 없다
'use client' 는 어디에 가장 안쪽 page.js 는 서버로 남긴다
경계를 좁히려니 구조가 꼬인다 통째로 클라이언트로 둔다 함수는 경계를 못 넘는다

이 표준을 정하기까지

홈만 여덟 조각인 게 이상해서 시작했다. about 은 91줄을 한 파일에 다 쓰는데 홈은 조각이 여덟이다. 기준이 있을 거라 보고 물었더니 「그냥 그렇게 됐다」였다.

그래서 실측으로 기준을 찾았다. 'use client' 가 붙은 파일을 세어 보니 패턴이 있었다 — page.js 는 서버로 남고 상태가 필요한 부분만 조각으로 떨어져 있었다. app/faq/page.js 는 16줄에 metadata 와 조립뿐이고, FaqPageClient 가 검색과 탭을 맡는다. 기준이 없었던 게 아니라, 하나는 이미 있었고 말로 꺼내진 적이 없었다.

그런데 그 기준으로도 홈의 여덟은 설명이 안 됐다. Hero 와 ContactCTA 만 'use client' 이고 나머지 여섯은 서버 컴포넌트다. 그 기준대로면 둘만 빼면 됐다. 그래서 두 번째 이유가 있는지 물었고, 없었다.

길이를 이유로 넣을지가 마지막 갈림길이었다. 넣으면 홈의 여덟이 설명되고 InquiriesPageClient 219줄도 쪼갤 근거가 생긴다. 버린 이유는 오늘 이미 같은 함정을 한 번 피했기 때문이다 — 화면별 대응 표준에서 「크기로 가르면 얼마가 큰지를 매번 판단해야 한다」로 크기 기준을 버렸다.

그리고 길이로 빼면 읽기가 오히려 어려워진다. <div> 하나짜리 파일이 수십 개 생기고, 파일을 열 때마다 건너뛰게 된다. 읽기 편하자고 뺐는데 반대가 되는 것이다.

대신 「같은 파일 안에서 함수로 나눈다」를 원칙 2로 넣었다. 길어서 읽기 힘든 것은 진짜 문제이고, 그 문제는 파일을 안 늘리고도 풀린다. 그러다 원칙 1의 이유가 생기면 잘라내기 한 번으로 파일이 된다.


정리

  • 빼는 이유는 둘뿐이다. 브라우저 코드가 필요하거나, 다른 갈래에서도 쓰거나.
  • 길이는 이유가 아니다. 몇 줄이 기준인지를 정해야 하고, 오늘 작던 것이 데이터가 늘면 커진다.
  • 길면 같은 파일 안에서 함수로 나눈다. 얻는 것은 같고 파일이 안 는다.
  • 'use client' 는 가장 안쪽에 붙이고 page.js 는 서버로 남긴다. metadata 는 서버에서만 나가고, 서버 컴포넌트는 브라우저 번들에 안 실린다.
  • 경계를 좁히려니 구조가 꼬이면 통째로 클라이언트로 둔다. 함수는 경계를 못 넘는다.
  • 이미 기준 없이 쪼개진 것을 되돌리지 않는다. 되돌려서 얻는 것이 없다.

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기