diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 074b9ce..46d3883 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -46,6 +46,9 @@ export default defineConfig({
text: '훅 설계',
items: [
{ text: '개요', link: '/custom-hooks/' },
+ { text: '커스텀 훅 추출 기준', link: '/custom-hooks/extraction-criteria' },
+ { text: '훅 합성 패턴', link: '/custom-hooks/composition' },
+ { text: '훅의 책임 범위', link: '/custom-hooks/responsibility-scope' },
],
},
],
diff --git a/docs/custom-hooks/composition.md b/docs/custom-hooks/composition.md
new file mode 100644
index 0000000..e60c35f
--- /dev/null
+++ b/docs/custom-hooks/composition.md
@@ -0,0 +1,90 @@
+# 작은 훅을 조합해 유스케이스 구성하기
+
+React와 Toss의 공개 라이브러리를 보면, 훅 합성의 목적은 "더 잘게 쪼개기"보다 "호출부를 더 단순하게 만드는 것"에 가깝습니다. `overlay-kit`은 반복적인 상태 관리와 이벤트 처리를 감추고, `use-funnel`은 복잡한 단계 흐름을 하나의 분명한 인터페이스로 다룹니다.
+
+## 규칙: 작은 훅은 관심사별로 나누고, 조합 훅은 한 가지 사용자 흐름을 설명해야 합니다
+
+작은 훅은 폼 상태, 서버 요청, 온라인 상태, 스크롤 잠금처럼 한 관심사만 다룹니다. 조합 훅은 이런 훅을 묶어 `게시글 작성`, `결제 진행`, `회원가입 흐름`처럼 한 가지 사용자 흐름을 설명할 때만 의미가 있습니다.
+
+- `useState` 하나마다 훅을 만드는 방식은 합성이라기보다 잘게 쪼개는 데 가깝습니다
+- 단계 전환, 뒤로 가기 흐름, 라우터 연동이 함께 움직인다면 `use-funnel`처럼 흐름 자체를 다루는 훅이 더 잘 맞을 수 있습니다
+- 같은 훅 안에 도메인 상태와 화면 전용 상태를 별 고민 없이 섞지 마세요
+
+## 규칙: 조합 훅은 읽기 쉬운 계약을 만들고, 화면 부수 효과는 대체로 호출부에 남기세요
+
+페이지 단위 훅 자체가 문제는 아닙니다. 다만 조합 훅이 라우팅, 토스트, 모달 닫기까지 모두 숨기기 시작하면 호출부는 무엇이 일어나는지 알기 어려워집니다. 반대로 `form`과 `mutation`을 한 객체에 담아 되돌려주는 정도라면 합성의 이점도 크지 않습니다. 조합 훅은 내부 도구를 그대로 노출하기보다, 호출부가 바로 쓸 수 있는 목적 단위의 동작을 만드는 편이 낫습니다.
+
+:::tabs
+== Bad
+```tsx
+function useCreatePostPage() {
+ const form = usePostForm()
+ const mutation = useCreatePostMutation()
+
+ return { form, mutation }
+}
+```
+
+== Good
+```tsx
+function useCreatePostForm() {
+ const form = usePostForm()
+ const mutation = useCreatePostMutation()
+
+ const submit = form.handleSubmit(async (values) => {
+ await mutation.mutateAsync(values)
+ })
+
+ return {
+ form,
+ submit,
+ isSubmitting: mutation.isPending,
+ }
+}
+
+function CreatePostPage() {
+ const router = useRouter()
+ const { form, submit, isSubmitting } = useCreatePostForm()
+
+ const handleSubmit = async () => {
+ await submit()
+ toast.success('게시글이 등록되었어요.')
+ router.push('/posts')
+ }
+
+ return (
+
+ )
+}
+```
+:::
+
+## 규칙: 조합 훅이 세터와 플래그만 늘린다면 경계를 다시 보세요
+
+좋은 조합 훅은 호출부에서 "무엇을 하는지"를 바로 읽히게 만듭니다. `overlay-kit`이 반복적인 이벤트 처리와 상태 전이를 목적에 맞는 API로 바꾸는 것처럼, 조합 훅도 내부 훅을 한곳에 모아 다시 내보내는 데서 끝나면 안 됩니다.
+
+- `usePageState()`처럼 지나치게 넓은 이름은 책임을 흐립니다
+- 호출부가 세터와 내부 플래그를 전부 알아야 한다면 훅 경계가 너무 얕습니다
+- `form`, `query`, `mutation`을 그대로 되돌려주기만 한다면 합성의 이점이 약합니다
+- 반환값이 지나치게 많다면 훅을 다시 나누거나, 반대로 더 높은 수준의 인터페이스가 맞는지 검토하세요
+
+## 빠른 참조
+
+| 코드 냄새 | 개선 방법 |
+|----------|----------|
+| `useState` 하나마다 훅을 만드는 식의 과도한 분해 | 관심사 단위로 다시 묶기 |
+| 페이지 이름만 달고 모든 로직을 담은 거대한 훅 | 작은 훅으로 나누고 유스케이스 단위로 조합 |
+| 내부 훅을 그대로 모아 되돌려주기만 하는 조합 훅 | 목적 단위의 동작과 파생 상태로 계약 재설계 |
+| 조합 훅이 라우팅/토스트까지 수행 | 화면 부수 효과는 호출부로 이동 |
+| 단계 전환과 뒤로 가기 흐름을 직접 조립하는 복잡한 화면 | 흐름 전용 조합 훅 또는 전용 라이브러리 검토 |
+| 반환값이 너무 많아 다시 분해해서 사용 | 조합 경계 축소 또는 더 높은 수준의 인터페이스 검토 |
+
+## 참고 자료
+
+- [React 공식 문서 - Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks)
+- [toss/overlay-kit](https://github.com/toss/overlay-kit)
+- [toss/use-funnel](https://github.com/toss/use-funnel)
diff --git a/docs/custom-hooks/extraction-criteria.md b/docs/custom-hooks/extraction-criteria.md
new file mode 100644
index 0000000..dc54915
--- /dev/null
+++ b/docs/custom-hooks/extraction-criteria.md
@@ -0,0 +1,97 @@
+# 커스텀 훅 추출 기준 세우기
+
+React 공식 문서는 커스텀 훅을 상태를 다루는 로직을 나누어 쓰는 방법으로 설명합니다. 그래서 훅을 뽑을지 말지는 "중복이 있느냐"보다 "역할이 더 또렷해지느냐"로 판단하는 편이 낫습니다.
+
+## 규칙: Hook과 Effect가 한 가지 목적을 이룰 때만 추출하세요
+
+다음 조건이 함께 맞을 때 커스텀 훅의 가치가 커집니다.
+
+- 실제로 Hook을 호출합니다
+- 상태, 파생값, 이벤트, Effect가 한 가지 맥락으로 묶입니다
+- 이름을 붙였을 때 호출부가 더 또렷해집니다
+
+반대로 `useState` 하나를 감싸거나, 한 컴포넌트 안에서만 잠깐 쓰는 로컬 로직을 무리하게 빼면 추상화만 남습니다.
+
+## 규칙: Hook을 쓰지 않는 함수는 일반 함수로 두세요
+
+커스텀 훅은 Hook을 호출하는 함수입니다. `sort`, `map`, `filter` 같은 순수 계산만 하는 함수에 `use` 접두사를 붙이면 React 로직이 숨어 있는 것처럼 보여 호출부를 오히려 헷갈리게 만듭니다.
+
+:::tabs
+== Bad
+```tsx
+function useSortedProducts(products: Product[]) {
+ return [...products].sort((a, b) => a.price - b.price)
+}
+```
+
+== Good
+```tsx
+function getSortedProducts(products: Product[]) {
+ return [...products].sort((a, b) => a.price - b.price)
+}
+```
+:::
+
+## 규칙: Effect 기반 훅은 생명주기보다 동기화 대상을 이름에 드러내세요
+
+Effect는 DOM, 브라우저 API, 구독, 네트워크처럼 React 바깥과 동기화할 때만 씁니다. 이런 로직이 여러 곳에서 반복된다면 `useMount`처럼 시점만 말하는 이름보다, 무엇과 동기화하는지 드러나는 훅이 더 낫습니다.
+
+:::tabs
+== Bad
+```tsx
+function useMount(callback: () => void) {
+ useEffect(() => {
+ callback()
+ }, [])
+}
+```
+
+== Good
+```tsx
+function useBodyScrollLock(locked: boolean) {
+ useEffect(() => {
+ if (!locked) {
+ return
+ }
+
+ const original = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
+
+ return () => {
+ document.body.style.overflow = original
+ }
+ }, [locked])
+}
+```
+:::
+
+## 규칙: Effect가 필요 없다면 훅도 다시 생각하세요
+
+렌더링용 계산이나 클릭 이후의 처리라면 Effect보다 계산식과 이벤트 핸들러를 우선합니다. 한 컴포넌트 안에서만 쓰이고 주변 상태와 강하게 결합된 Effect라면, 굳이 커스텀 훅으로 감추지 않고 컴포넌트 안의 `useEffect`로 두는 편이 더 읽기 쉽습니다.
+
+- 계산 로직은 렌더링 중 계산합니다
+- 사용자 동작 뒤에 일어나는 처리는 이벤트 핸들러에서 다룹니다
+- 훅 추출은 Effect를 숨기기보다 동기화 목적을 분명하게 만드는 선택이어야 합니다
+
+## 빠른 참조
+
+| 코드 냄새 | 개선 방법 |
+|----------|----------|
+| `useState` 하나만 감싼 훅 | 컴포넌트 안에 두거나 목적이 드러나는 단위로 다시 묶기 |
+| Hook을 호출하지 않는 `useX` 함수 | 일반 함수로 변경 |
+| `useMount`, `useEffectOnce` 같은 lifecycle 훅 | 동기화 대상이 드러나는 훅으로 변경 |
+| 계산용 로직을 Effect로 동기화 | 렌더링 중 계산 또는 이벤트 핸들러로 이동 |
+| 한 컴포넌트에만 묶인 Effect를 무리하게 훅으로 추출 | 컴포넌트 안의 `useEffect`로 유지 |
+
+## 체크리스트
+
+- 이름을 붙였을 때 호출부가 더 읽기 쉬워지나요?
+- 상태, 이벤트, 파생값, Effect가 한 가지 목적 아래 묶이나요?
+- Hook을 호출하지 않는 일반 함수를 억지로 훅으로 만들고 있지 않나요?
+- Effect가 정말 외부 시스템 동기화인가요?
+
+## 참고 자료
+
+- [React 공식 문서 - Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks)
+- [React 공식 문서 - You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
+- [React 공식 문서 - Lifecycle of Reactive Effects](https://react.dev/learn/lifecycle-of-reactive-effects)
diff --git a/docs/custom-hooks/index.md b/docs/custom-hooks/index.md
index 7dbb0e8..e8bbea4 100644
--- a/docs/custom-hooks/index.md
+++ b/docs/custom-hooks/index.md
@@ -1,9 +1,37 @@
# 훅 설계 (Custom Hooks)
-커스텀 훅은 컴포넌트의 로직을 재사용 가능한 단위로 분리하는 핵심 도구입니다.
+이 문서는 React 공식 문서와 Toss의 공개 라이브러리에서 공통으로 드러나는 기준을 바탕으로, 커스텀 훅을 언제 만들고 어떤 경계로 설계할지 정리합니다.
+
+커스텀 훅은 중복을 감추는 편의 함수가 아닙니다. 상태와 Effect가 얽힌 로직을, 목적이 보이는 인터페이스로 정리하는 설계 도구입니다.
+
+## 이 문서가 답하는 질문
+
+- 언제 훅으로 추출하고, 언제 일반 함수나 컴포넌트 내부 로직으로 두어야 하나요?
+- 언제 작은 훅을 조합하고, 언제 큰 훅이 필요한가요?
+- 한 훅은 어디까지 책임지고, 어디서 멈춰야 하나요?
## 다루는 내용
-- **커스텀 훅 추출 기준**: 언제 훅으로 분리해야 하는지 판단하는 기준을 세웁니다
-- **훅 합성(Composition) 패턴**: 작은 훅을 조합하여 복잡한 로직을 구성합니다
-- **훅의 책임 범위**: 하나의 훅이 담당해야 할 적절한 범위를 정합니다
+훅 설계 문서는 커스텀 훅을 어떤 기준으로 추출하고, 작은 훅을 어떻게 조합하며, 한 훅의 책임 범위를 어디까지 둘지에 집중합니다. 특히 아래 판단을 빠르게 내리기 위한 기준을 다룹니다.
+
+- **커스텀 훅 추출 기준**: 일반 함수와 커스텀 훅을 구분하고, 언제 훅으로 분리할지 판단합니다
+- **훅 합성(Composition) 패턴**: 여러 훅을 묶어 더 읽기 쉬운 인터페이스를 만드는 기준을 다룹니다
+- **훅의 책임 범위**: 한 훅이 몇 가지 역할까지 맡아도 되는지 경계를 정합니다
+
+다음 내용은 다른 토픽에서 다룹니다.
+
+- **상태 관리**: 상태를 어디에 둘지, 파생 상태를 어떻게 다룰지, 서버 상태와 클라이언트 상태를 어떻게 나눌지
+- **컴포넌트 설계**: 컴포넌트 책임 경계, Compound Components, Headless UI 같은 UI 구조 설계
+- **Props/인터페이스**: Props 네이밍, `children`/render props/slot 선택 기준, 외부 API를 컴포넌트 인터페이스에 맞추는 방법
+
+## 지침 목록
+
+- [커스텀 훅 추출 기준 세우기](./extraction-criteria) - 훅으로 추출할지, 일반 함수나 컴포넌트 내부 로직으로 둘지를 판단합니다
+- [작은 훅을 조합해 유스케이스 구성하기](./composition) - 여러 훅을 조합해 언제 더 읽기 쉬운 인터페이스를 만들 수 있는지 정합니다
+- [한 훅의 책임 범위 정하기](./responsibility-scope) - 한 훅이 담당해야 할 변화 이유와 반환 계약의 경계를 정합니다
+
+## 참고 기준
+
+- [React 공식 문서 - Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks)
+- [toss/overlay-kit](https://github.com/toss/overlay-kit)
+- [toss/use-funnel](https://github.com/toss/use-funnel)
diff --git a/docs/custom-hooks/responsibility-scope.md b/docs/custom-hooks/responsibility-scope.md
new file mode 100644
index 0000000..bf31bf8
--- /dev/null
+++ b/docs/custom-hooks/responsibility-scope.md
@@ -0,0 +1,125 @@
+# 한 훅의 책임 범위 정하기
+
+좋은 훅은 이름과 반환값만 보고도 무엇을 하고 무엇을 하지 않는지 대략 감이 와야 합니다. React 공식 문서도 커스텀 훅은 상태를 같이 쓰는 수단이 아니라, 상태를 다루는 로직을 나누어 쓰는 방법이라고 설명합니다.
+
+## 규칙: 훅은 상태를 공유하는 도구가 아니라 로직을 나누는 방법입니다
+
+- 같은 훅을 두 번 호출해도 두 호출은 서로 독립적으로 동작합니다
+- 여러 컴포넌트가 같은 값을 공유해야 한다면 기준이 되는 상태를 어디에 둘지 먼저 정해야 합니다
+- 상태 공유 문제를 훅 추출로 해결하려 하면 동기화 버그가 생기기 쉽습니다
+
+여러 컴포넌트가 같은 상태를 봐야 한다면 훅 추출보다 상태 끌어올리기, Context, 외부 스토어 중 무엇이 맞는지 먼저 판단하세요.
+
+## 규칙: 훅 이름과 반환 계약이 예측 가능해야 합니다
+
+훅 이름만 보고도 어떤 값을 돌려주고 어떤 부수 효과를 감출지 대략 예상할 수 있어야 합니다. 데이터를 가져오는 훅이 화면 이동까지 해버리거나, 반환값이 지나치게 많아 호출부가 무엇을 믿어야 할지 모르게 만들면 설계가 흔들립니다. 다만 `useUserQuery`처럼 라이브러리에서 익숙한 계약을 그대로 따르는 경우에는, 이름이 충분히 구체적이라면 전체 결과 객체를 반환해도 괜찮습니다.
+
+:::tabs
+== Bad
+```tsx
+function useUser() {
+ const router = useRouter()
+ const query = useQuery({
+ queryKey: ['user'],
+ queryFn: fetchUser,
+ })
+
+ useEffect(() => {
+ if (query.isError) {
+ router.replace('/login')
+ }
+ }, [query.isError, router])
+
+ return query.data
+}
+```
+
+== Good
+```tsx
+function useUserQuery() {
+ return useQuery({
+ queryKey: ['user'],
+ queryFn: fetchUser,
+ })
+}
+
+function ProfilePage() {
+ const router = useRouter()
+ const userQuery = useUserQuery()
+
+ useEffect(() => {
+ if (userQuery.isError) {
+ router.replace('/login')
+ }
+ }, [userQuery.isError, router])
+}
+```
+:::
+
+## 규칙: 한 훅에는 한 가지 주된 변경 이유만 남기세요
+
+폼 상태, 서버 요청, 라우팅, 토스트는 자주 함께 등장하지만 항상 같은 이유로 바뀌는 것은 아닙니다. 이런 로직이 한 훅에 섞이면 수정 범위가 넓어지고, 호출부는 숨겨진 부수 효과를 따라가야 합니다. 페이지 단위 훅을 만드는 것 자체가 문제는 아니지만, 그 안에서 숨기는 책임의 수는 신중하게 제한해야 합니다.
+
+:::tabs
+== Bad
+```tsx
+function useCheckoutPage() {
+ const form = useForm()
+ const router = useRouter()
+
+ const submit = async (values: OrderFormValues) => {
+ await submitOrder(values)
+ toast.success('주문이 완료되었어요.')
+ router.push('/complete')
+ }
+
+ return { form, submit }
+}
+```
+
+== Good
+```tsx
+function useCheckoutForm() {
+ return useForm()
+}
+
+function useSubmitOrder() {
+ return useMutation({
+ mutationFn: submitOrder,
+ })
+}
+
+function CheckoutPage() {
+ const router = useRouter()
+ const form = useCheckoutForm()
+ const submitOrderMutation = useSubmitOrder()
+
+ const handleSubmit = form.handleSubmit(async (values) => {
+ await submitOrderMutation.mutateAsync(values)
+ toast.success('주문이 완료되었어요.')
+ router.push('/complete')
+ })
+}
+```
+:::
+
+## 빠른 참조
+
+| 코드 냄새 | 개선 방법 |
+|----------|----------|
+| 상태 공유 문제를 훅 추출로 해결하려 함 | 상태 끌어올리기, Context, 외부 스토어 검토 |
+| 훅 이름과 실제 동작이 다름 | 이름과 책임을 다시 맞추기 |
+| 훅 하나가 `fetch`, `form`, `navigation`, `toast`를 모두 담당 | 변화 이유별로 분리 |
+| 반환값이 너무 많아 호출부에서 다시 분해 | 계약 축소 또는 작은 훅으로 분리 |
+
+## 체크리스트
+
+- 이 훅은 상태를 같이 쓰기 위한 것인가, 로직을 나누기 위한 것인가?
+- 이 훅을 한 문장으로 설명할 수 있나요?
+- 반환값을 처음 보는 사람도 역할을 추측할 수 있나요?
+- 다른 이유로 바뀌는 로직이 한 훅에 섞여 있지 않나요?
+
+## 참고 자료
+
+- [React 공식 문서 - Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks)
+- [React 공식 문서 - Sharing State Between Components](https://react.dev/learn/sharing-state-between-components)