홈은 조각 여덟 개로 쪼개져 있고, 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 가 서버 컴포넌트로 남고, 브라우저 코드가 필요한 부분만 조각으로 떨어져 있다.

// 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   자동 넘김 · 점 클릭

셋 다 상태가 전체를 관통해서 더 좁힐 여지가 없다. 반대로 홈의 섹션 여덟 중 HeroContactCTA 둘만 '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 가 실려 온다. 서버 컴포넌트는 그 과정이 없다.

대가는 경계를 넘길 때 값을 넘겨야 하는 것이다. 서버 컴포넌트가 클라이언트 컴포넌트에 넘기는 값은 직렬화되는 것이어야 한다 — 함수는 못 넘긴다. 그래서 경계를 좁히려다 오히려 구조가 꼬이면, 그 조각은 통째로 클라이언트로 두는 것이 낫다. HeaderHero 가 그런 경우다.


판단 기준 정리

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

이 표준을 정하기까지

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

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

그런데 그 기준으로도 홈의 여덟은 설명이 안 됐다. HeroContactCTA'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 가 실려 온다.
- **경계를 좁히려다 구조가 꼬이면 통째로 클라이언트로 둔다.** 서버에서 클라이언트로 넘기는 값은 직렬화되어야 해서 **함수는 못 넘긴다.** 상태가 전체를 관통하는 컴포넌트는 그냥 클라이언트다.
- **이미 기준 없이 쪼개진 것을 되돌리지 않는다.** 되돌려서 얻는 것이 없고 깨질 위험만 있다.

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

댓글남기기