홈은 조각 여덟 개로 쪼개져 있고, about 은 91줄을 한 파일에 다 쓴다.
app/page.js 31줄 Hero · ServiceIntro · WhyUs · Process · ContactCTA · Reviews · BottomCTA
app/about/page.js 91줄 전부 한 파일
app/care/page.js 109줄 전부 한 파일
그 차이가 무엇인지 적힌 곳이 없다. 물어봤더니 홈을 여덟로 쪼갠 데 기준이 있었던 것이 아니라 그냥 그렇게 됐다.
먼저: 이미 정해진 것 둘
이 글은 앞선 표준 위에 선다.
- 컴포넌트 자리 표준이 뺀 것을 어디 두는지를 정했다 —
components/layout/·app/<갈래>/_components/·components/. - 태그 선택 표준이 무슨 태그로 그리는지를 정했다.
둘 다 「이미 컴포넌트인 것」을 다룬다. 이 글은 그 앞의 질문에 답한다 — 애초에 무엇을 뺄 것인가.
먼저: 클라이언트 경계는 이미 잘 그어져 있다
이건 원칙이 아니라 실측이고, 이 표준이 새로 만들 것이 아니라 이미 하고 있는 것을 규칙으로 꺼낸 것이라는 근거다.
page.js 가 서버 컴포넌트로 남고, 브라우저 코드가 필요한 부분만 조각으로 떨어져 있다.
// app/inquiries/page.js — 서버. metadata 만 들고 조립한다
export const metadata = { title: '1:1 문의', robots: { index: false } };
export default function InquiriesPage() {
return <><InquiriesPageClient /><MobileCtaBar /></>;
}
그리고 떨어져 나간 쪽은 정말로 브라우저 코드가 필요하다.
InquiriesPageClient 219줄 · useState 12개 검색 · 목록 · 커서 페이징
Header 67줄 · 상태 2 · 효과 2 스크롤 · 메뉴 열림 · 현재 경로
Hero 78줄 · 슬라이더 index 자동 넘김 · 점 클릭
셋 다 상태가 전체를 관통해서 더 좁힐 여지가 없다. 반대로 홈의 섹션 여덟 중 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는 서버에서만 나가고, 서버 컴포넌트는 브라우저 번들에 안 실린다.- 경계를 좁히려니 구조가 꼬이면 통째로 클라이언트로 둔다. 함수는 경계를 못 넘는다.
- 이미 기준 없이 쪼개진 것을 되돌리지 않는다. 되돌려서 얻는 것이 없다.
AI 코드 어시스턴트에 바로 적용하기
Claude Code · Codex — .claude/skills/component-extraction-standard/SKILL.md · .agents/skills/component-extraction-standard/SKILL.md
두 파일은 바이트까지 같아야 한다. .agents 쪽은 tools/agents-skills.rb 가 뽑는 생성물이다.
---
name: component-extraction-standard
description: 화면 코드를 언제 별도 컴포넌트 파일로 빼는지 정한다. 빼는 이유 둘, 길이를 이유로 삼지 않는 근거, 한 파일 안에서 나누는 방법, 'use client' 경계를 어디에 두는지를 담는다. 페이지가 길어졌을 때, 브라우저 코드가 필요해졌을 때, 컴포넌트를 새로 만들지 정할 때 적용한다. Next.js App Router 기준이다.
---
# 컴포넌트 추출 표준
**빼는 이유는 둘뿐이다.**
```
1. 브라우저 코드가 필요하다 → 그 부분만 떼어 'use client' 를 붙인다
2. 다른 갈래에서도 쓴다 → 컴포넌트 자리 표준이 정한 자리로 올린다
```
## 길이는 이유가 아니다
- **「몇 줄 넘으면 뺀다」를 두지 않는다.** 그 숫자를 정해야 하고, 오늘 작던 것이 데이터가 늘면 커진다.
- **길이로 빼면 `<div>` 하나짜리 파일이 수십 개 생긴다.** 파일을 열 때마다 건너뛰게 되어 읽기가 오히려 어려워진다.
**길어서 읽기 힘든 것은 같은 파일 안에서 함수로 나눈다.**
```jsx
function AboutHero() { ... }
function AboutStory() { ... }
export default function AboutPage() {
return <main><AboutHero /><AboutStory /></main>;
}
```
- 이름이 붙어 읽기 쉬워지는 것은 똑같은데 **파일을 오가지 않아도 되고 `import` 도 안 는다.**
- 그러다 위의 이유가 생기면 **잘라내기 한 번으로 파일이 된다.**
- **이미 기준 없이 쪼개진 것을 되돌리지 않는다.** 되돌려서 얻는 것이 없다.
## `'use client'` 는 가장 안쪽에
```
page.js 에 붙이면 → 그 아래 전부가 브라우저로 내려간다
조각에만 붙이면 → 그 조각만 내려간다
```
```jsx
// app/faq/page.js — 서버로 남긴다
export const metadata = { title: '자주 묻는 질문' };
export default function FaqPage() {
return <><FaqPageClient /><MobileCtaBar /></>;
}
```
- **`page.js` 는 서버 컴포넌트로 남긴다.** `metadata` 는 서버에서만 내보낼 수 있고, 서버 컴포넌트 코드는 브라우저 번들에 안 실린다.
- **정적 export 라도 이득이 있다.** 클라이언트 컴포넌트는 브라우저에서 다시 살아나야(hydration) 해서 그만큼 JS 가 실려 온다.
- **경계를 좁히려다 구조가 꼬이면 통째로 클라이언트로 둔다.** 서버에서 클라이언트로 넘기는 값은 직렬화되어야 해서 **함수는 못 넘긴다.** 상태가 전체를 관통하는 컴포넌트는 그냥 클라이언트다.
## 하지 않는 것
- **파일이 길다는 이유로 빼지 않는다.**
- **「몇 줄 이상」 같은 숫자 기준을 만들지 않는다.**
- **`page.js` 에 `'use client'` 를 붙이지 않는다.** 상태가 필요하면 그 부분을 조각으로 뺀다.
- **이미 쪼개진 것을 기준에 안 맞는다고 되돌리지 않는다.**
GitHub Copilot — .github/instructions/component-extraction-standard.instructions.md
---
description: 화면 코드를 언제 별도 컴포넌트 파일로 빼는가
applyTo: "**"
---
# 컴포넌트 추출
- **빼는 이유는 둘뿐이다** — **브라우저 코드가 필요하거나**(그 부분만 떼어 `'use client'`), **다른 갈래에서도 쓰거나**(컴포넌트 자리 표준이 정한 자리로 올린다).
- **길이는 빼는 이유가 아니다.** 「몇 줄 넘으면 뺀다」를 두면 그 숫자를 정해야 하고, 오늘 작던 것이 데이터가 늘면 커진다. 그리고 **`<div>` 하나짜리 파일이 수십 개 생겨 읽기가 오히려 어려워진다.**
- **길어서 읽기 힘든 것은 같은 파일 안에서 함수로 나눈다.** 이름이 붙어 읽기 쉬워지는 것은 똑같은데 파일을 오가지 않아도 되고 `import` 도 안 는다. 그러다 위의 이유가 생기면 **잘라내기 한 번으로 파일이 된다.**
- **`'use client'` 는 가장 안쪽에 붙이고 `page.js` 는 서버 컴포넌트로 남긴다.** `page.js` 에 붙이면 그 아래 전부가 브라우저로 내려간다. `metadata` 는 서버에서만 내보낼 수 있고, 서버 컴포넌트 코드는 브라우저 번들에 안 실린다.
- **정적 export 라도 이득이 있다.** 클라이언트 컴포넌트는 브라우저에서 다시 살아나야(hydration) 해서 그만큼 JS 가 실려 온다.
- **경계를 좁히려다 구조가 꼬이면 통째로 클라이언트로 둔다.** 서버에서 클라이언트로 넘기는 값은 직렬화되어야 해서 **함수는 못 넘긴다.** 상태가 전체를 관통하는 컴포넌트는 그냥 클라이언트다.
- **이미 기준 없이 쪼개진 것을 되돌리지 않는다.** 되돌려서 얻는 것이 없고 깨질 위험만 있다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기