커스텀 훅 만들기
우리는 useReducer
훅을 통해 복잡한 상태 관리 로직을 효율적으로 다루는 방법을 배웠습니다. 이제 4장 "핵심 React 훅"의 마지막 장으로, 리액트 훅의 강력한 장점 중 하나인 커스텀 훅(Custom Hook) 에 대해 알아보겠습니다.
커스텀 훅은 컴포넌트 간에 상태 관련 로직(stateful logic)을 재사용할 수 있도록 해주는 메커니즘입니다. 즉, 여러 컴포넌트에서 동일하거나 유사한 로직을 반복해서 사용해야 할 때, 이 로직을 별도의 함수로 추출하여 use
로 시작하는 이름으로 만드는 것을 커스텀 훅이라고 합니다.
클래스형 컴포넌트에서는 render props
나 Higher-Order Components (HOCs)
와 같은 패턴을 사용하여 로직을 재사용했지만, 이들은 복잡성을 증가시키는 단점이 있었습니다. 훅은 이러한 문제점을 해결하고 훨씬 간결하고 직관적인 방법으로 로직 재사용을 가능하게 합니다.
왜 커스텀 훅이 필요한가?
리액트 애플리케이션을 개발하다 보면 다음과 같은 상황에 자주 직면합니다.
- 반복되는 로직: 여러 컴포넌트에서 동일한
useState
,useEffect
등의 훅 조합을 사용하여 비슷한 기능을 구현해야 할 때. (예: 특정 데이터를 불러오는 로직, 입력 폼의 값 관리, 마우스 위치 추적 등) - 복잡한 컴포넌트 분리: 하나의 컴포넌트가 너무 많은 상태 로직을 포함하여 가독성이 떨어지고 유지보수가 어려워질 때.
- 로직 재사용성 향상: 특정 기능을 독립적인 모듈로 만들어 다른 프로젝트에서도 쉽게 가져다 쓰고 싶을 때.
커스텀 훅은 이러한 문제들을 해결하여 코드의 재사용성, 가독성, 유지보수성을 크게 향상시킵니다.
커스텀 훅 만들기 규칙
커스텀 훅을 만들 때는 다음과 같은 두 가지 규칙만 지키면 됩니다.
- 이름 규칙: 커스텀 훅의 이름은 반드시
use
로 시작해야 합니다. (예:useToggle
,useFetch
,useLocalStorage
)- 이 규칙은 리액트가 훅의 규칙(Hooks Rule)을 강제하고, 개발자 도구에서 훅의 동작을 올바르게 인식하도록 돕습니다.
- 내부에서 다른 훅 호출: 커스텀 훅 내부에서는 다른 리액트 훅(
useState
,useEffect
,useContext
,useRef
등)을 호출할 수 있습니다.
간단한 커스텀 훅 예제: useToggle
가장 간단한 커스텀 훅 중 하나인 useToggle
을 만들어 봅시다. 이 훅은 불리언(boolean) 상태를 관리하며, 상태를 토글하는 함수를 반환합니다.
useToggle.js
파일 생성
src/hooks
폴더를 생성하고 그 안에 useToggle.js
파일을 만듭니다.
// src/hooks/useToggle.js
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) { // (1) 'use'로 시작하는 함수 이름
const [value, setValue] = useState(initialValue); // (2) 내부에서 useState 훅 사용
// (3) 상태를 토글하는 함수 정의 (useCallback으로 최적화)
const toggle = useCallback(() => {
setValue(prevValue => !prevValue);
}, []); // 의존성 배열이 비어있으므로, 컴포넌트가 마운트될 때 한 번만 생성
// (4) 외부에서 사용할 값과 함수를 배열 또는 객체로 반환
return [value, toggle]; // 배열로 반환하는 것이 useState와 유사하여 일반적
}
export default useToggle;
useToggle
함수는initialValue
를 인자로 받아useState
로value
상태를 초기화합니다.toggle
함수는value
상태를true
에서false
로,false
에서true
로 변경하는 역할을 합니다.useCallback
으로 감싸서 불필요한 함수 재생성을 막아 최적화했습니다.[value, toggle]
형태로 현재 상태 값과 상태 변경 함수를 배열로 반환합니다. (useState
와 동일한 패턴)
useToggle
훅 사용하기
이제 이 커스텀 훅을 여러 컴포넌트에서 사용해 봅시다.
// src/components/ToggleExample.js
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.js
에 ToggleExample
을 추가하여 실행해 보세요. 이제 isLightOn
과 isPanelOpen
이라는 두 개의 독립적인 불리언 상태를 useToggle
훅을 통해 간결하게 관리할 수 있습니다. 각 상태의 로직은 useToggle
내부에 캡슐화되어 있어 컴포넌트 코드가 훨씬 깔끔해집니다.
더 복잡한 커스텀 훅 예제
로컬 스토리지에 값을 저장하고 불러오는 로직은 많은 웹 애플리케이션에서 반복적으로 사용됩니다. 이 로직을 커스텀 훅으로 만들어 봅시다. 이 훅은 useState
와 useEffect
를 함께 사용합니다.
useLocalStorage.js
파일 생성
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// (1) 초기 상태를 계산하는 함수 (지연 초기화)
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key); // 로컬 스토리지에서 값 가져오기
// JSON 문자열이거나 일반 문자열일 수 있으므로 파싱 시도
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue; // 에러 발생 시 초기값 반환
}
});
// (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
의 초기값으로 함수를 전달하여 지연 초기화를 구현했습니다. 이 함수는 로컬 스토리지에서 값을 불러와 초기값으로 사용합니다.useEffect
훅을 사용하여storedValue
또는key
가 변경될 때마다 해당 값을 로컬 스토리지에 저장합니다.JSON.stringify
와JSON.parse
를 사용하여 객체나 배열도 저장할 수 있도록 했습니다.[storedValue, setStoredValue]
형태로 현재 값과 값을 업데이트하는 함수를 반환합니다.
useLocalStorage
훅 사용하기
// src/components/LocalStorageExample.js
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.js
에 LocalStorageExample
을 추가하고 실행해 보세요. 입력 필드에 값을 입력한 후 페이지를 새로고침하거나 브라우저를 닫았다가 다시 열어도 값이 유지되는 것을 확인할 수 있습니다. 이제 로컬 스토리지와 연동되는 상태 관리 로직을 단 한 줄의 코드로 여러 컴포넌트에서 재사용할 수 있게 되었습니다!
모든 컴포넌트 한 곳에서 테스트 (App.js)
// src/App.js (최종 수정)
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;
"커스텀 훅 만들기"는 여기까지입니다. 이 장에서는 커스텀 훅의 개념, 필요성, 생성 규칙을 설명하고, useToggle
과 useLocalStorage
라는 두 가지 실용적인 예제를 통해 상태 관련 로직을 어떻게 추출하고 재사용하는지 상세하게 다루었습니다.
이제 여러분은 리액트 컴포넌트의 복잡성을 줄이고, 코드의 재사용성과 유지보수성을 극대화할 수 있는 강력한 도구인 커스텀 훅을 자유롭게 만들고 활용할 수 있게 되었습니다. 이는 함수형 컴포넌트 개발의 진정한 힘을 경험하는 중요한 단계입니다.