커스텀 훅 만들기
반복되는 상태와 effect 로직을 useToggle·useLocalStorage 훅으로 추출해 컴포넌트 사이에서 재사용합니다.
앞 글에서는 useReducer로 복잡한 상태 관리 로직을 다루는 방법을 배웠습니다. 이제 4장에서 다루는 훅의 마지막 주제인 커스텀 훅(custom hook)을 알아봅니다. 커스텀 훅은 컴포넌트 사이에서 상태와 관련된 로직(stateful logic)을 재사용하게 해 주는 방법입니다. 여러 컴포넌트에서 같거나 비슷한 로직을 되풀이해야 할 때, 그 로직을 별도의 함수로 뽑아 use로 시작하는 이름을 붙인 것이 커스텀 훅입니다.
클래스형 컴포넌트에서는 render props나 고차 컴포넌트(Higher-Order Components, HOCs) 같은 패턴으로 로직을 재사용했지만, 이런 방식은 컴포넌트 구조를 복잡하게 만드는 단점이 있었습니다. 훅은 이 문제를 줄이고 더 간결하고 직관적으로 로직을 재사용하게 해 줍니다.
왜 커스텀 훅이 필요한가?
React 애플리케이션을 만들다 보면 여러 컴포넌트에서 같은 useState와 useEffect의 조합으로 비슷한 기능을 구현해야 할 때가 자주 있습니다. 특정 데이터를 불러오는 로직, 입력 폼의 값 관리, 마우스 위치 추적 같은 것이 그 예입니다. 하나의 컴포넌트가 너무 많은 상태 로직을 품어 읽기 어렵고 고치기 힘들어질 때도 있고, 어떤 기능을 독립된 모듈로 만들어 다른 프로젝트에서 쉽게 가져다 쓰고 싶을 때도 있습니다. 커스텀 훅은 이런 문제를 해결해 코드의 재사용성과 가독성, 유지보수성을 높여 줍니다. 커스텀 훅을 설계할 때는 UI의 모양보다, 되풀이되는 상태 로직과 Effect가 어디서 어디까지인지를 먼저 찾으면 쉬워집니다.
커스텀 훅 추출과 공개 계약 판단표 — 상태 추적
useToggle(true)와 useToggle(false) 중 전등만 toggle하면 false/false가 되고 패널은 그대로다. 같은 로직과 호출별 독립 state를 실제 입력으로 보여 주고 설계 표 전체는 보존한다.
CUSTOM HOOK: LOGIC REUSE
커스텀 Hook은 반복되는 로직을 공유하고 state는 호출마다 가집니다
UI 모양보다 반복되는 state·Effect·이벤트 규칙을 찾습니다. 입력과 반환값, 외부 시스템 경계를 계약으로 정하고 추출 전후의 동작과 독립적인 상태를 확인합니다.
같은 useToggle 구현, 서로 다른 state
전등만 toggle합니다. 두 값이 false로 같아져도 공유 state가 아닙니다. 두 호출 사이에 state 전달 간선이 없습니다.
원래 설명과 상세 기준
ToggleExample · 같은 구현, 두 state
useToggle(true) → 전등: true
useToggle(false) → 패널: false
toggleLight() → 전등: false
패널: false 유지useToggle의 내부 구현을 재사용해도 전등과 패널은 서로 다른 Hook 호출입니다.
각 행에서 추출 신호 → 공개 계약 → 검증·경계를 확인합니다.
| 질문 · 추출 신호 | 공개 계약 | 검증·경계 |
|---|---|---|
| 반복? 여러 컴포넌트에 같은 state 전환, Effect, 이벤트 처리 규칙이 반복된다. | JSX와 스타일은 남기고 상태 관련 로직만 함수로 옮긴다. | 추출 전후 동작이 같고 각 Hook 호출의 state가 독립적인지 확인한다. |
Hook? use 이름을 붙일 함수는 일반적으로 하나 이상의 React Hook을 호출한다. |
use 다음 대문자로 이름 짓는다. 커스텀 훅과 useState·useEffect 같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출한다. |
일반 Hook의 조건문·반복문·중첩 함수 호출은 린터로 막는다. React 19의 use(resource)는 조건문·반복문에서 호출할 수 있지만 try/catch 안에서는 호출할 수 없다. |
| 입력? 초기값, key, id, URL, 옵션처럼 사용 지점마다 달라지는 값이 있다. | 차이를 명시적 인자로 받고 Effect가 읽는 reactive 값은 의존성에 포함한다. | 입력이 바뀔 때 의도한 재동기화만 일어나며 숨은 전역 의존성이 없는지 본다. |
| 반환? 컴포넌트가 상태값, 파생값, setter나 명령 함수 중 일부를 사용한다. | 필요한 값만 반환한다. 배열·객체·단일 값은 사용 의미에 맞춰 선택한다. | 호출부가 내부 state나 Effect 구현을 몰라도 의도를 읽을 수 있어야 한다. |
| 연결? 구독, 타이머, 네트워크 연결, 브라우저 API와 동기화한다. | Effect setup이 지속 자원을 만들 때만 그 자원을 되돌리는 cleanup을 반환한다. | 개발 Strict Mode의 추가 setup·cleanup에서도 동일하게 동작하는지 확인한다. 일회성 setItem 쓰기에는 해제할 자원이 없다. |
| 가치? 중복을 줄이는 동시에 컴포넌트가 구현보다 의도를 말하게 된다. | 커스텀 훅은 상태 자체가 아니라 상태 관련 로직을 공유한다. | 호출별 상태 변화, 오류 경로, 비동기 결과와 실제로 존재하는 cleanup을 테스트한다. |
- 반복 항목?
- 신호 여러 컴포넌트에 같은 state 전환, Effect, 이벤트 규칙이 반복됩니다.
- 선택 JSX는 남기고 상태 관련 로직만 추출합니다.
- 확인 추출 전후 동작과 호출별 독립 state를 검증합니다.
- 정말 Hook인가?
- 계약 다른 React Hook을 호출한다면
use다음 대문자로 이름 짓습니다. - 위치 커스텀 훅과
useState·useEffect같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출하고 린터로 확인합니다. - 예외 React 19의
use(resource)는 조건문·반복문에서도 호출할 수 있지만try/catch안에서는 호출할 수 없습니다. - 입력?
- 계약 초기값, key, id, URL, 옵션처럼 달라지는 값을 명시적 인자로 받습니다.
- 확인 Effect가 읽는 reactive 값은 의존성에 포함하고, 의도한 재동기화만 일어나며 숨은 전역 의존성이 없는지 확인합니다.
- 반환?
- 계약 필요한 상태값, 파생값, setter나 명령 함수만 노출합니다.
- 형태 배열, 객체, 단일 값 중 사용 의미가 가장 잘 드러나는 형태를 고릅니다.
- 확인 호출부가 내부 state나 Effect 구현을 몰라도 이름과 반환 계약으로 의도를 읽을 수 있어야 합니다.
- 외부 연결?
- Effect 구독, 타이머, 연결처럼 지속 자원을 만들 때 그 자원을 되돌리는 cleanup을 반환합니다.
- 예외 일회성
localStorage.setItem쓰기는 해제할 자원을 만들지 않습니다. - 확인 개발 Strict Mode의 추가 setup·cleanup에서도 같은 의미를 유지합니다.
- 추출 가치가 있는가?
- 목표 상태 자체가 아니라 상태 관련 로직을 공유해 컴포넌트가 구현보다 의도를 말하게 합니다.
- 테스트 호출별 상태 변화, 오류·비동기 경로와 실제 cleanup을 검증합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 표 행 | 질문 · 추출 신호 | 공개 계약 | 검증·경계 |
| 원도표 표 행 | 반복? 여러 컴포넌트에 같은 state 전환, Effect, 이벤트 처리 규칙이 반복된다. | JSX와 스타일은 남기고 상태 관련 로직만 함수로 옮긴다. | 추출 전후 동작이 같고 각 Hook 호출의 state가 독립적인지 확인한다. |
| 원도표 표 행 | Hook? use 이름을 붙일 함수는 일반적으로 하나 이상의 React Hook을 호출한다. | use 다음에 대문자를 써서 이름 짓는다. 커스텀 훅과 useState·useEffect 같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출한다. | 일반 Hook의 조건문·반복문·중첩 함수 호출은 린터로 막는다. React 19의 use(resource)는 조건문·반복문에서 호출할 수 있지만 try/catch 안에서는 호출할 수 없다. |
| 원도표 표 행 | 입력? 초기값, key, id, URL, 옵션처럼 사용 지점마다 달라지는 값이 있다. | 차이를 명시적 인자로 받고 Effect가 읽는 reactive 값은 의존성에 포함한다. | 입력이 바뀔 때 의도한 재동기화만 일어나며 숨은 전역 의존성이 없는지 본다. |
| 원도표 표 행 | 반환? 컴포넌트가 상태값, 파생값, setter나 명령 함수 중 일부를 사용한다. | 필요한 값만 반환한다. 배열·객체·단일 값은 사용 의미에 맞춰 선택한다. | 호출부가 내부 state나 Effect 구현을 몰라도 의도를 읽을 수 있어야 한다. |
| 원도표 표 행 | 연결? 구독, 타이머, 네트워크 연결, 브라우저 API와 동기화한다. | Effect setup이 지속 자원을 만들 때만 그 자원을 되돌리는 cleanup을 반환한다. | 개발 Strict Mode의 추가 setup·cleanup에서도 동일하게 동작하는지 확인한다. 일회성 setItem 쓰기에는 해제할 자원이 없다. |
| 원도표 표 행 | 가치? 중복을 줄이는 동시에 컴포넌트가 구현보다 의도를 말하게 된다. | 커스텀 훅은 상태 자체가 아니라 상태 관련 로직을 공유한다. | 호출별 상태 변화, 오류 경로, 비동기 결과와 실제로 존재하는 cleanup을 테스트한다. |
| 원도표 정의 내용 | 계약 다른 React Hook을 호출한다면 use 다음에 대문자를 써서 이름 짓습니다. |
| 원도표 정의 내용 | 위치 커스텀 훅과 useState·useEffect 같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출하고 린터로 확인합니다. |
| 원도표 정의 내용 | 예외 React 19의 use(resource)는 조건문·반복문에서도 호출할 수 있지만 try/catch 안에서는 호출할 수 없습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 독립 두 호출 | 전등: true / 패널: false | 전등은 true, 패널은 false로 시작한다 | useToggle 로직은 같지만 호출별 state다 |
| 전등 toggle | 전등 요청: false / 패널: false 그대로 | toggleLight가 전등의 updater만 보낸다 | 한 호출의 setter는 다른 호출 state를 수정하지 않는다 |
| 다음 표시 | 전등: false / 패널: false | 전등 false와 패널 false를 읽는다 | 같은 값이 된 결과가 공유 상태를 뜻하지 않는다 |
커스텀 훅 추출과 공개 계약 판단표 — 상태 추적
useToggle(true)와 useToggle(false) 중 전등만 toggle하면 false/false가 되고 패널은 그대로다. 같은 로직과 호출별 독립 state를 실제 입력으로 보여 주고 설계 표 전체는 보존한다.
CUSTOM HOOK: LOGIC REUSE
커스텀 Hook은 반복되는 로직을 공유하고 state는 호출마다 가집니다
UI 모양보다 반복되는 state·Effect·이벤트 규칙을 찾습니다. 입력과 반환값, 외부 시스템 경계를 계약으로 정하고 추출 전후의 동작과 독립적인 상태를 확인합니다.
같은 useToggle 구현, 서로 다른 state
전등만 toggle합니다. 두 값이 false로 같아져도 공유 state가 아닙니다. 두 호출 사이에 state 전달 간선이 없습니다.
원래 설명과 상세 기준
ToggleExample · 같은 구현, 두 state
useToggle(true) → 전등: true
useToggle(false) → 패널: false
toggleLight() → 전등: false
패널: false 유지useToggle의 내부 구현을 재사용해도 전등과 패널은 서로 다른 Hook 호출입니다.
각 행에서 추출 신호 → 공개 계약 → 검증·경계를 확인합니다.
| 질문 · 추출 신호 | 공개 계약 | 검증·경계 |
|---|---|---|
| 반복? 여러 컴포넌트에 같은 state 전환, Effect, 이벤트 처리 규칙이 반복된다. | JSX와 스타일은 남기고 상태 관련 로직만 함수로 옮긴다. | 추출 전후 동작이 같고 각 Hook 호출의 state가 독립적인지 확인한다. |
Hook? use 이름을 붙일 함수는 일반적으로 하나 이상의 React Hook을 호출한다. |
use 다음 대문자로 이름 짓는다. 커스텀 훅과 useState·useEffect 같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출한다. |
일반 Hook의 조건문·반복문·중첩 함수 호출은 린터로 막는다. React 19의 use(resource)는 조건문·반복문에서 호출할 수 있지만 try/catch 안에서는 호출할 수 없다. |
| 입력? 초기값, key, id, URL, 옵션처럼 사용 지점마다 달라지는 값이 있다. | 차이를 명시적 인자로 받고 Effect가 읽는 reactive 값은 의존성에 포함한다. | 입력이 바뀔 때 의도한 재동기화만 일어나며 숨은 전역 의존성이 없는지 본다. |
| 반환? 컴포넌트가 상태값, 파생값, setter나 명령 함수 중 일부를 사용한다. | 필요한 값만 반환한다. 배열·객체·단일 값은 사용 의미에 맞춰 선택한다. | 호출부가 내부 state나 Effect 구현을 몰라도 의도를 읽을 수 있어야 한다. |
| 연결? 구독, 타이머, 네트워크 연결, 브라우저 API와 동기화한다. | Effect setup이 지속 자원을 만들 때만 그 자원을 되돌리는 cleanup을 반환한다. | 개발 Strict Mode의 추가 setup·cleanup에서도 동일하게 동작하는지 확인한다. 일회성 setItem 쓰기에는 해제할 자원이 없다. |
| 가치? 중복을 줄이는 동시에 컴포넌트가 구현보다 의도를 말하게 된다. | 커스텀 훅은 상태 자체가 아니라 상태 관련 로직을 공유한다. | 호출별 상태 변화, 오류 경로, 비동기 결과와 실제로 존재하는 cleanup을 테스트한다. |
- 반복 항목?
- 신호 여러 컴포넌트에 같은 state 전환, Effect, 이벤트 규칙이 반복됩니다.
- 선택 JSX는 남기고 상태 관련 로직만 추출합니다.
- 확인 추출 전후 동작과 호출별 독립 state를 검증합니다.
- 정말 Hook인가?
- 계약 다른 React Hook을 호출한다면
use다음 대문자로 이름 짓습니다. - 위치 커스텀 훅과
useState·useEffect같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출하고 린터로 확인합니다. - 예외 React 19의
use(resource)는 조건문·반복문에서도 호출할 수 있지만try/catch안에서는 호출할 수 없습니다. - 입력?
- 계약 초기값, key, id, URL, 옵션처럼 달라지는 값을 명시적 인자로 받습니다.
- 확인 Effect가 읽는 reactive 값은 의존성에 포함하고, 의도한 재동기화만 일어나며 숨은 전역 의존성이 없는지 확인합니다.
- 반환?
- 계약 필요한 상태값, 파생값, setter나 명령 함수만 노출합니다.
- 형태 배열, 객체, 단일 값 중 사용 의미가 가장 잘 드러나는 형태를 고릅니다.
- 확인 호출부가 내부 state나 Effect 구현을 몰라도 이름과 반환 계약으로 의도를 읽을 수 있어야 합니다.
- 외부 연결?
- Effect 구독, 타이머, 연결처럼 지속 자원을 만들 때 그 자원을 되돌리는 cleanup을 반환합니다.
- 예외 일회성
localStorage.setItem쓰기는 해제할 자원을 만들지 않습니다. - 확인 개발 Strict Mode의 추가 setup·cleanup에서도 같은 의미를 유지합니다.
- 추출 가치가 있는가?
- 목표 상태 자체가 아니라 상태 관련 로직을 공유해 컴포넌트가 구현보다 의도를 말하게 합니다.
- 테스트 호출별 상태 변화, 오류·비동기 경로와 실제 cleanup을 검증합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 표 행 | 질문 · 추출 신호 | 공개 계약 | 검증·경계 |
| 원도표 표 행 | 반복? 여러 컴포넌트에 같은 state 전환, Effect, 이벤트 처리 규칙이 반복된다. | JSX와 스타일은 남기고 상태 관련 로직만 함수로 옮긴다. | 추출 전후 동작이 같고 각 Hook 호출의 state가 독립적인지 확인한다. |
| 원도표 표 행 | Hook? use 이름을 붙일 함수는 일반적으로 하나 이상의 React Hook을 호출한다. | use 다음에 대문자를 써서 이름 짓는다. 커스텀 훅과 useState·useEffect 같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출한다. | 일반 Hook의 조건문·반복문·중첩 함수 호출은 린터로 막는다. React 19의 use(resource)는 조건문·반복문에서 호출할 수 있지만 try/catch 안에서는 호출할 수 없다. |
| 원도표 표 행 | 입력? 초기값, key, id, URL, 옵션처럼 사용 지점마다 달라지는 값이 있다. | 차이를 명시적 인자로 받고 Effect가 읽는 reactive 값은 의존성에 포함한다. | 입력이 바뀔 때 의도한 재동기화만 일어나며 숨은 전역 의존성이 없는지 본다. |
| 원도표 표 행 | 반환? 컴포넌트가 상태값, 파생값, setter나 명령 함수 중 일부를 사용한다. | 필요한 값만 반환한다. 배열·객체·단일 값은 사용 의미에 맞춰 선택한다. | 호출부가 내부 state나 Effect 구현을 몰라도 의도를 읽을 수 있어야 한다. |
| 원도표 표 행 | 연결? 구독, 타이머, 네트워크 연결, 브라우저 API와 동기화한다. | Effect setup이 지속 자원을 만들 때만 그 자원을 되돌리는 cleanup을 반환한다. | 개발 Strict Mode의 추가 setup·cleanup에서도 동일하게 동작하는지 확인한다. 일회성 setItem 쓰기에는 해제할 자원이 없다. |
| 원도표 표 행 | 가치? 중복을 줄이는 동시에 컴포넌트가 구현보다 의도를 말하게 된다. | 커스텀 훅은 상태 자체가 아니라 상태 관련 로직을 공유한다. | 호출별 상태 변화, 오류 경로, 비동기 결과와 실제로 존재하는 cleanup을 테스트한다. |
| 원도표 정의 내용 | 계약 다른 React Hook을 호출한다면 use 다음에 대문자를 써서 이름 짓습니다. |
| 원도표 정의 내용 | 위치 커스텀 훅과 useState·useEffect 같은 일반 Hook은 컴포넌트나 Hook의 최상위에서 호출하고 린터로 확인합니다. |
| 원도표 정의 내용 | 예외 React 19의 use(resource)는 조건문·반복문에서도 호출할 수 있지만 try/catch 안에서는 호출할 수 없습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 독립 두 호출 | 전등: true / 패널: false | 전등은 true, 패널은 false로 시작한다 | useToggle 로직은 같지만 호출별 state다 |
| 전등 toggle | 전등 요청: false / 패널: false 그대로 | toggleLight가 전등의 updater만 보낸다 | 한 호출의 setter는 다른 호출 state를 수정하지 않는다 |
| 다음 표시 | 전등: false / 패널: false | 전등 false와 패널 false를 읽는다 | 같은 값이 된 결과가 공유 상태를 뜻하지 않는다 |
위 그림의 위쪽은 이 글의 ToggleExample이 같은 useToggle 구현을 두 번 호출하는 모습입니다. useToggle(true)는 전등의 state를 true로, useToggle(false)는 패널의 state를 false로 만들고, toggleLight()로 전등만 바꾸면 전등은 false가 되고 패널은 false를 그대로 유지합니다. 그림의 설명처럼 두 값이 모두 false로 같아져도 state를 공유하는 것은 아니며, 두 호출 사이에는 state를 전달하는 선이 없습니다. 커스텀 훅은 되풀이되는 구현을 공유할 뿐 상태 자체를 공유하지 않으며, 같은 훅을 두 번 호출하면 각 호출이 자기만의 state와 Effect를 가집니다.
그림의 아래쪽 표는 로직을 커스텀 훅으로 뽑을지 판단하는 여섯 가지 질문을, 추출의 신호와 밖으로 드러낼 입출력, 확인할 점으로 나눠 정리합니다. 여러 컴포넌트에 같은 state 전환이나 Effect, 이벤트 규칙이 되풀이된다면 JSX와 스타일은 남기고 상태 관련 로직만 함수로 옮긴 뒤, 추출 전후의 동작이 같고 호출마다 state가 독립적인지 확인합니다. 그 함수가 다른 React 훅을 호출한다면 use 다음에 대문자가 오는 이름을 붙이고, 사용하는 곳마다 달라지는 초기값이나 key, id, URL은 명시적인 인자로 받으며, Effect가 읽는 반응형 값은 의존성에 넣습니다.
반환값으로는 상태 값, 파생 값, setter나 명령 함수 가운데 호출하는 쪽이 쓰는 것만 드러내고, 배열과 객체, 단일 값 가운데 쓰임이 가장 잘 드러나는 형태를 고릅니다. 구독이나 타이머, 네트워크 연결처럼 지속되는 자원을 만드는 Effect라면 그 자원을 되돌리는 정리 함수를 함께 돌려주지만, 한 번의 localStorage.setItem처럼 해제할 자원이 없는 쓰기에는 정리 함수가 필요 없습니다. 마지막 질문처럼, 추출의 목표는 상태가 아니라 상태 관련 로직을 공유해 컴포넌트가 구현보다 의도를 말하게 하는 것이며, 호출마다의 상태 변화와 오류·비동기 경로, 실제로 있는 정리 함수를 테스트합니다.
커스텀 훅 만들기 규칙
커스텀 훅을 만들 때는 이름뿐 아니라 훅을 호출하는 위치도 함께 지켜야 합니다. 먼저 이름은 useToggle, useFetch, useLocalStorage처럼 use 다음에 대문자가 오도록 짓습니다. 이 이름 덕분에 React와 린터가 그 함수 안에 훅 호출이 있을 수 있다는 것을 알아봅니다. 반대로 다른 훅을 호출하지 않는 일반 함수라면, 헷갈리지 않도록 use를 붙이지 않는 편이 좋습니다.
다음으로 호출 위치입니다. 커스텀 훅과 useState, useEffect, useContext, useRef 같은 일반 훅은 컴포넌트나 다른 커스텀 훅의 최상위에서 호출하며, 조건문이나 반복문, 중첩된 함수 안에서 호출해서는 안 됩니다. 예외로 React 19의 use(resource)는 컴포넌트나 커스텀 훅 안이라면 조건문과 반복문 안에서도 호출할 수 있지만, try/catch 안에서는 호출할 수 없습니다(React 문서의 use).
간단한 커스텀 훅 예제: useToggle
가장 간단한 커스텀 훅 가운데 하나인 useToggle을 만들어 봅니다. 이 훅은 불리언(boolean) 상태를 관리하고, 그 상태를 뒤집는 함수를 돌려줍니다.
useToggle.js 파일 생성
src/hooks 폴더를 만들고 그 안에 useToggle.js 파일을 만듭니다. 이 파일에는 JSX가 없으므로 .js 확장자를 씁니다.
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) { // (1) 'use'로 시작하는 함수 이름
const [value, setValue] = useState(initialValue); // (2) 내부에서 useState 훅 사용
// (3) 의존성이 바뀌지 않는 동안 같은 함수 참조를 유지
const toggle = useCallback(() => {
setValue(prevValue => !prevValue);
}, []);
// (4) 외부에서 사용할 값과 함수를 배열 또는 객체로 반환
return [value, toggle]; // 배열로 반환하는 것이 useState와 유사하여 일반적
}
export default useToggle;useToggle 함수는 initialValue를 인자로 받아 useState로 value 상태를 초기화합니다. toggle 함수는 업데이터로 이전 불리언 값을 반대로 바꾸며, 여기서는 useCallback이 의존성이 바뀌지 않는 동안 같은 함수 참조를 유지하게 합니다. 다만 useCallback으로 감쌌다는 이유만으로 성능이 좋아진다고 단정할 수는 없습니다. 마지막으로 [value, toggle]처럼 현재 상태 값과 상태를 바꾸는 함수를 배열로 돌려줍니다. 이 예제는 useState와 비슷한 배열 형태를 골랐지만, 커스텀 훅은 쓰는 곳에 맞춰 값이나 배열, 객체 등 필요한 형태를 돌려줄 수 있습니다.
useToggle 훅 사용하기
이제 이 커스텀 훅을 컴포넌트에서 써 봅니다. 커스텀 훅은 일반 함수처럼 호출하고, 돌려받은 배열을 useState처럼 구조 분해해 원하는 이름을 붙입니다. 아래 컴포넌트는 같은 훅을 두 번 호출해 전등과 패널의 상태를 따로 만들며, 각 버튼은 자기 쪽의 토글 함수만 부릅니다.
import React from 'react';
import useToggle from '../hooks/useToggle'; // 커스텀 훅 불러오기
function ToggleExample() {
const [isLightOn, toggleLight] = useToggle(true); // (1) useToggle 훅 사용
const [isPanelOpen, togglePanel] = useToggle(false); // (2) 다른 상태에도 재사용
return (
<div style={{ border: '1px solid #FF8F00', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>useToggle 커스텀 훅 예제</h2>
{/* 첫 번째 토글 */}
<div style={{ marginBottom: '15px' }}>
<p>전등 상태: {isLightOn ? '켜짐 💡' : '꺼짐 🌑'}</p>
<button onClick={toggleLight} style={{ padding: '8px 15px', backgroundColor: '#ff8f00', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>
전등 {isLightOn ? '끄기' : '켜기'}
</button>
</div>
{/* 두 번째 토글 (재사용) */}
<div>
<p>패널 {isPanelOpen ? '열림 ▼' : '닫힘 ▶'}</p>
<button onClick={togglePanel} style={{ padding: '8px 15px', backgroundColor: '#ff8f00', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>
패널 {isPanelOpen ? '닫기' : '열기'}
</button>
{isPanelOpen && ( // isPanelOpen 상태에 따라 조건부 렌더링
<div style={{ border: '1px dashed #ccc', padding: '10px', marginTop: '10px', backgroundColor: '#fffbe6' }}>
<p>이것은 토글된 패널 내용입니다.</p>
</div>
)}
</div>
</div>
);
}
export default ToggleExample;App.jsx에 ToggleExample을 넣어 실행해 보세요. 처음에는 “전등 상태: 켜짐 💡”과 “패널 닫힘 ▶”이 보입니다. isLightOn과 isPanelOpen이라는 두 개의 독립된 불리언 상태를 useToggle로 간결하게 관리할 수 있고, 각 상태의 로직이 useToggle 안에 감춰져 있어 컴포넌트의 코드가 훨씬 깔끔해집니다.
더 복잡한 커스텀 훅 예제
로컬 스토리지에 값을 저장하고 불러오는 로직은 많은 웹 애플리케이션에서 되풀이됩니다. 이 로직을 useState와 useEffect를 함께 쓰는 커스텀 훅으로 만들어 봅니다.
useLocalStorage의 초기 읽기와 Effect 쓰기 경계 — 상태 추적
lazy 초기 읽기와 독립 Hook state, setter 후 commit/현재 key에 Effect 쓰기가 시점별로 달라진다. key 변경의 동작과 오류 fallback 범위를 실제 코드에 맞춰 표현한다.
CUSTOM HOOK: READ & WRITE
초기 저장값은 initializer가 읽고 변경값은 Effect가 씁니다
useLocalStorage는 한 호출의 React state를 브라우저 저장소와 연결합니다. 읽기·JSON 파싱 실패는 initialValue로, 쓰기·직렬화 실패는 catch에서 로그로 처리합니다. state 갱신과 저장 성공은 별개입니다.
한 Hook 호출 · 초기화와 업데이트
초기: getItem(key) → JSON.parse
없음/읽기실패 → initialValue
setter → 다음 storedValue
commit → [key, storedValue] 쓰기storedValue는 호출마다 독립적입니다. 쓰기 실패가 state를 이전 값으로 되돌리거나 성공적인 영속 저장을 보장하지는 않습니다.
LocalStorageExample · 두 호출
userName: "게스트" / key:userName
userAge: 25 / key:userAge이름 setter와 나이 setter는 각각의 state를 바꿉니다. 같은 저장 key를 사용해도 이 구현이 Hook state를 자동으로 공유하지는 않습니다.
key 변경 / 다른 탭 / hydration
현재 구현은 key가 바뀌면 현재 state를 새 key에 쓰고 기존 저장값을 다시 읽지 않습니다. storage 이벤트를 구독하지 않으며 서버와 첫 클라이언트 출력 일치는 별도 전략이 필요합니다.
Effect 쓰기와 cleanup
한 번의 setItem은 해제할 지속 연결을 만들지 않아 cleanup이 없습니다. Strict Mode의 추가 initializer/setup을 허용해야 하며, origin·접근 정책·용량·JSON 직렬화가 저장에 영향을 줍니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| initializer 읽기 | storage: 해당 key raw 값 / state: 호출별 초기 값 | useLocalStorage 호출별로 key 저장값을 읽어 초기 state를 만든다 | 같은 Hook을 호출해도 state 하나를 공유하지 않는다 |
| setter 변경 | state: 새 값 / storage: 쓰기 전 | 한 호출의 setter가 그 state를 바꾼다 | 렌더 중 일회성 읽기와 후속 쓰기가 다르다 |
| Effect 쓰기 | key: 현재 key / storage: 쓰기 결과 | commit 뒤 현재 key에 value JSON을 기록한다 | key가 바뀌면 현재 state를 새 key에 쓰며 기존 값을 다시 읽지 않는다. 쓰기 실패는 state를 되돌리지 않는다 |
useLocalStorage의 초기 읽기와 Effect 쓰기 경계 — 상태 추적
lazy 초기 읽기와 독립 Hook state, setter 후 commit/현재 key에 Effect 쓰기가 시점별로 달라진다. key 변경의 동작과 오류 fallback 범위를 실제 코드에 맞춰 표현한다.
CUSTOM HOOK: READ & WRITE
초기 저장값은 initializer가 읽고 변경값은 Effect가 씁니다
useLocalStorage는 한 호출의 React state를 브라우저 저장소와 연결합니다. 읽기·JSON 파싱 실패는 initialValue로, 쓰기·직렬화 실패는 catch에서 로그로 처리합니다. state 갱신과 저장 성공은 별개입니다.
한 Hook 호출 · 초기화와 업데이트
초기: getItem(key) → JSON.parse
없음/읽기실패 → initialValue
setter → 다음 storedValue
commit → [key, storedValue] 쓰기storedValue는 호출마다 독립적입니다. 쓰기 실패가 state를 이전 값으로 되돌리거나 성공적인 영속 저장을 보장하지는 않습니다.
LocalStorageExample · 두 호출
userName: "게스트" / key:userName
userAge: 25 / key:userAge이름 setter와 나이 setter는 각각의 state를 바꿉니다. 같은 저장 key를 사용해도 이 구현이 Hook state를 자동으로 공유하지는 않습니다.
key 변경 / 다른 탭 / hydration
현재 구현은 key가 바뀌면 현재 state를 새 key에 쓰고 기존 저장값을 다시 읽지 않습니다. storage 이벤트를 구독하지 않으며 서버와 첫 클라이언트 출력 일치는 별도 전략이 필요합니다.
Effect 쓰기와 cleanup
한 번의 setItem은 해제할 지속 연결을 만들지 않아 cleanup이 없습니다. Strict Mode의 추가 initializer/setup을 허용해야 하며, origin·접근 정책·용량·JSON 직렬화가 저장에 영향을 줍니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| initializer 읽기 | storage: 해당 key raw 값 / state: 호출별 초기 값 | useLocalStorage 호출별로 key 저장값을 읽어 초기 state를 만든다 | 같은 Hook을 호출해도 state 하나를 공유하지 않는다 |
| setter 변경 | state: 새 값 / storage: 쓰기 전 | 한 호출의 setter가 그 state를 바꾼다 | 렌더 중 일회성 읽기와 후속 쓰기가 다르다 |
| Effect 쓰기 | key: 현재 key / storage: 쓰기 결과 | commit 뒤 현재 key에 value JSON을 기록한다 | key가 바뀌면 현재 state를 새 key에 쓰며 기존 값을 다시 읽지 않는다. 쓰기 실패는 state를 되돌리지 않는다 |
위 그림은 useLocalStorage의 한 호출이 React의 state와 브라우저 저장소를 잇는 흐름을 보여 줍니다. 처음에는 지연 초기화 함수가 getItem(key)로 저장값을 읽어 JSON.parse로 바꾸고, 값이 없거나 읽기에 실패하면 initialValue를 써서 그 호출만의 state를 만듭니다. 그 뒤 setter로 다음 storedValue가 정해지면, 커밋한 뒤에 Effect가 [key, storedValue]가 바뀐 것을 보고 현재 key에 JSON으로 씁니다. 그림의 설명처럼 state의 갱신과 저장의 성공은 별개이며, 쓰기에 실패해도 state가 이전 값으로 되돌아가지는 않습니다.
그림의 아래 칸은 이 글의 LocalStorageExample처럼 userName(초기값 “게스트”)과 userAge(초기값 25) 두 호출이 각자의 state를 바꾼다는 점과, 같은 저장 키를 써도 이 구현이 state를 저절로 공유하지는 않는다는 점을 보여 줍니다. 또 지금의 구현은 key가 바뀌면 기존 저장값을 다시 읽지 않고 현재 state를 새 key에 쓰며, 다른 탭의 변경을 알리는 storage 이벤트도 구독하지 않습니다. 한 번의 setItem은 해제할 연결을 만들지 않으므로 정리 함수가 없으며, Strict Mode의 추가 호출에도 같은 결과가 나와야 합니다.
useLocalStorage.js 파일 생성
src/hooks 폴더에 다음 파일을 만듭니다. 이 훅은 useState와 같은 모양으로 쓰도록 저장 키와 초기값을 받아 [값, setter]를 돌려줍니다. 아래 코드의 (1)은 처음 한 번 저장소에서 값을 읽는 부분이고, (2)는 값이 바뀔 때마다 저장소에 쓰는 부분입니다. typeof window 검사는 서버 렌더링처럼 window가 없는 환경에서 호출되어도 오류 없이 초기값을 쓰게 합니다.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// (1) 초기 상태를 계산하는 함수 (지연 초기화)
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key); // 로컬 스토리지에서 값 가져오기
return item === null ? initialValue : JSON.parse(item);
} catch {
return initialValue; // 저장소 접근 또는 JSON 파싱 실패 시 초기값 반환
}
});
// (2) storedValue 또는 key가 변경될 때마다 로컬 스토리지 업데이트
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue)); // 값을 JSON 문자열로 저장
} catch (error) {
console.error(error);
}
}, [key, storedValue]); // key나 storedValue가 변경될 때마다 실행
// (3) useState와 유사하게 현재 값과 setter 함수 반환
return [storedValue, setStoredValue];
}
export default useLocalStorage;useState의 초기값으로 함수를 넘겨 지연 초기화를 구현했습니다. 이 함수는 마운트할 때 로컬 스토리지를 읽고, 항목이 없거나 접근이나 파싱에 실패하면 initialValue를 씁니다. useEffect는 커밋한 뒤 storedValue나 key가 바뀔 때마다 그 값을 로컬 스토리지에 저장하며, JSON.stringify와 JSON.parse를 써서 JSON으로 나타낼 수 있는 객체나 배열도 저장할 수 있게 했습니다. 마지막으로 [storedValue, setStoredValue]처럼 현재 값과 그 값을 바꾸는 함수를 돌려줍니다.
이 예제가 어디까지 다루는지도 분명히 알아 둡니다. React는 지연 초기화 함수가 순수하기를 기대합니다. 이 예제는 초기화하는 동안 저장소의 값이 바뀌지 않는다고 보고 읽기만 하며, 쓰기는 Effect에 둡니다. 개발 모드의 Strict Mode에서는 초기화 함수가 두 번 호출될 수 있으므로 그 안에서 한 번만 일어나야 하는 부수 효과를 실행해서는 안 됩니다. 또 이 Effect는 구독이나 타이머, 연결처럼 해제할 지속 자원을 만들지 않고 setItem을 한 번 호출할 뿐이므로 정리 함수가 필요 없습니다. Strict Mode에서는 Effect의 setup도 한 번 더 실행될 수 있으므로, 같은 값을 다시 써도 안전해야 합니다.
localStorage는 브라우저와 출처(origin)에 묶인 외부 시스템이라서 접근 정책이나 저장 용량 때문에 실패할 수 있습니다. 서버 렌더링과 하이드레이션(hydration)을 쓰는 앱이라면 서버와 클라이언트의 첫 출력이 같도록 클라이언트에서 따로 초기화하는 전략을 세워야 합니다.
useLocalStorage 훅 사용하기
import React from 'react';
import useLocalStorage from '../hooks/useLocalStorage'; // 커스텀 훅 불러오기
function LocalStorageExample() {
// useLocalStorage 훅을 사용하여 'userName'과 'userAge' 상태를 로컬 스토리지와 동기화
const [userName, setUserName] = useLocalStorage('userName', '게스트');
const [userAge, setUserAge] = useLocalStorage('userAge', 25);
return (
<div style={{ border: '1px solid #1abc9c', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>useLocalStorage 커스텀 훅 예제</h2>
<div style={{ marginBottom: '15px' }}>
<label>
이름:
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
style={{ marginLeft: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
</label>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
나이:
<input
type="number"
value={userAge}
onChange={(e) => setUserAge(Number(e.target.value))}
style={{ marginLeft: '10px', padding: '8px', fontSize: '16px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
</label>
</div>
<p>
저장된 이름: <span style={{ fontWeight: 'bold', color: '#16a085' }}>{userName}</span>
</p>
<p>
저장된 나이: <span style={{ fontWeight: 'bold', color: '#16a085' }}>{userAge}</span>
</p>
<p style={{ fontSize: '14px', color: '#666' }}>
브라우저를 닫았다가 다시 열어도 입력값이 유지됩니다. (로컬 스토리지 확인)
</p>
</div>
);
}
export default LocalStorageExample;App.jsx에 LocalStorageExample을 넣어 실행해 보세요. 처음에는 “저장된 이름: 게스트”와 “저장된 나이: 25”가 보이고, 입력 필드에 값을 입력한 뒤 페이지를 새로고침하거나 브라우저를 닫았다가 다시 열어도 값이 유지됩니다. 이제 로컬 스토리지와 이어지는 상태 관리 로직을 한 줄의 호출로 여러 컴포넌트에서 재사용할 수 있습니다.
이 실습은 userName 키에 JSON 문자열이, userAge 키에 JSON 숫자가 들어 있거나 두 키가 없는 상태를 전제로 합니다. JSON 파싱이 성공했다고 원하는 값의 타입까지 확인되지는 않으므로, 예전 실험의 값 때문에 화면이 다르다면 그 키만 지운 뒤 초기값을 다시 확인하세요. 로직의 재사용과 상태의 공유를 이렇게 구분하는 방법은 React 문서의 커스텀 훅으로 로직 재사용하기에서 자세히 설명합니다.
더 깊이: 같은 저장 항목을 여러 컴포넌트와 탭이 함께 보려면
이 글의 useLocalStorage는 호출마다 따로 state를 두므로, 같은 키를 쓰는 다른 컴포넌트나 다른 탭의 변경을 알지 못합니다. 브라우저 저장소처럼 React 바깥에 있는 값을 여러 컴포넌트가 같은 값으로 읽어야 한다면, React가 바깥 저장소를 구독하도록 제공하는 useSyncExternalStore를 쓸 수 있습니다. 이 훅은 저장소가 바뀔 때 부를 콜백을 등록하는 subscribe와 현재 값을 돌려주는 getSnapshot을 받고, 콜백이 불리면 getSnapshot을 다시 호출해 값이 Object.is로 달라졌을 때만 다시 렌더링합니다(React 문서의 useSyncExternalStore).
localStorage에 이 훅을 쓸 때는 브라우저의 storage 이벤트가 어디로 전달되는지 알아야 합니다. HTML 표준에 따르면 setItem으로 값을 바꿀 때 같은 출처의 다른 문서, 예를 들어 다른 탭이나 창에만 storage 이벤트가 전달되고 값을 바꾼 문서 자신은 받지 않습니다. 그래서 같은 탭의 다른 컴포넌트에도 알리려면 값을 쓰는 쪽에서 등록된 콜백들을 직접 불러 줘야 합니다. 또 getSnapshot은 저장소가 그대로인 동안 같은 값을 돌려줘야 하므로, 호출할 때마다 JSON.parse로 새 객체를 만들지 말고 getItem이 돌려준 문자열을 그대로 돌려준 뒤 렌더에서 해석하는 편이 안전합니다.
커스텀 훅 전체 컴포넌트 테스트 (App.jsx)
두 예제를 함께 확인하려면 App.jsx를 다음과 같이 고칩니다. 두 컴포넌트는 서로 다른 커스텀 훅을 쓰고 상태도 공유하지 않으므로, 한 화면에 함께 놓아도 서로의 동작에 영향을 주지 않습니다. 아래 App은 둘 사이에 <hr />를 넣어 영역만 나눕니다.
import React from 'react';
import './App.css';
import ToggleExample from './components/ToggleExample';
import LocalStorageExample from './components/LocalStorageExample';
function App() {
return (
<div className="App">
<h1>커스텀 훅 만들기</h1>
<ToggleExample />
<hr />
<LocalStorageExample />
</div>
);
}
export default App;커스텀 훅은 코드를 함수로 빼는 일에 그치지 않고, 상태 로직의 입력과 출력, 안에서 일어나는 효과, 재사용할 범위를 분명히 하는 설계 작업입니다. 앞의 판단표처럼 되풀이되는 신호와 밖으로 드러낼 입출력을 먼저 정하고, Effect가 외부 자원을 만들 때만 그 자원을 되돌리는 정리 함수를 함께 설계하세요.
정리
- 커스텀 훅은 컴포넌트 사이에서 상태 관련 로직을 재사용하는 함수이며,
use다음에 대문자가 오는 이름을 붙입니다. - 같은 커스텀 훅을 여러 번 호출해도 구현만 공유할 뿐, 각 호출은 자기만의 state와 Effect를 가집니다.
- 커스텀 훅과 일반 훅은 컴포넌트나 다른 훅의 최상위에서만 호출하며, React 19의
use(resource)만 조건문과 반복문 안에서 호출할 수 있습니다. - 사용하는 곳마다 달라지는 값은 인자로 받고, 호출하는 쪽에 필요한 값만 알맞은 형태로 돌려줍니다.
- 지연 초기화 함수와 Effect는 Strict Mode의 추가 호출에도 같은 결과를 내야 하며, 정리 함수는 해제할 지속 자원이 있을 때만 돌려줍니다.
- 같은 저장 키를 쓰는 두
useLocalStorage호출은 state를 공유하지 않고 같은 저장 항목을 서로 덮어쓸 수 있습니다.
연습 문제
1. 두 번 쓴 useToggle
ToggleExample은 처음에 전등이 true, 패널이 false입니다. 전등 버튼을 두 번, 패널 버튼을 한 번 누른 뒤 두 값은 각각 무엇일까요?
해설 보기
전등은 true에서 false, 다시 true가 되고, 패널은 false에서 true가 되어 둘 다 true입니다. 전등을 누른다고 패널의 상태가 바뀌지는 않습니다.
훅의 함수 정의는 같지만 각 호출의 state는 따로 관리됩니다. 또 이미 마운트된 호출에 다른 initialValue를 넘기는 것만으로 useState가 다시 초기화되지도 않습니다.
2. 같은 저장 키를 쓰는 두 컴포넌트
서로 다른 두 컴포넌트가 모두 useLocalStorage('userName', '게스트')를 호출합니다. 첫 번째 컴포넌트의 입력칸에 “지수”를 입력하면 두 번째 컴포넌트의 화면도 바뀔까요? 새로고침하면 두 컴포넌트는 무엇을 보여 줄까요?
해설 보기
두 번째 컴포넌트의 화면은 바뀌지 않습니다. 두 호출의 React state는 서로 다르고, 이 구현은 다른 호출의 저장 변경을 구독하지 않으므로 두 번째 컴포넌트는 여전히 자기 state를 보여 줍니다.
다만 두 호출은 같은 저장 항목에 쓰므로, 마지막으로 저장한 값이 앞의 값을 덮어씁니다. 첫 번째 컴포넌트가 마지막으로 “지수”를 썼다면 새로고침한 뒤 두 컴포넌트 모두 “지수”로 시작합니다. 같은 현재 값을 함께 보여 줘야 한다면 공통 부모나 Context에서 하나의 state를 내려 주고, 서로 독립된 값이라면 다른 키를 씁니다.
3. 나이 입력칸을 비우면
LocalStorageExample의 나이 입력칸에서 숫자를 모두 지우면 화면과 저장값은 어떻게 될까요?
해설 보기
입력칸을 비우면 e.target.value가 빈 문자열이 되고, Number('')는 0이므로 userAge가 0이 됩니다. 그래서 입력칸과 “저장된 나이:”에는 0이 보이고, 로컬 스토리지에도 0이 저장됩니다.
빈 칸을 “아직 입력하지 않음”으로 다루고 싶다면 빈 문자열을 그대로 state에 두거나, 숫자로 바꾸기 전에 빈 값인지 먼저 검사해야 합니다. 이처럼 입력을 어떤 타입으로 바꿔 저장할지도 훅을 쓰는 쪽이 정해야 합니다.
다음 글 실습에서는 4장에서 배운 훅들을 모아 하나의 작은 애플리케이션을 직접 만들어 봅니다.