[이제와서 시작하는 React 마스터하기 #8] Custom Hooks - 반복되는 로직 재사용하기
React Day 8 - 똑같은 코드를 여러 번 복사 붙여넣기 하고 계신가요? Custom Hook으로 반복되는 로직을 재사용해봅시다!
오늘 배울 내용
- ✅ Custom Hook이란 무엇인가?
- ✅ 왜 Custom Hook을 사용할까?
- ✅ Custom Hook 만드는 방법
- ✅ 실전 예제: useToggle, useLocalStorage, useWindowSize
시작하기 전에
이전에 배운 내용을 기억하시나요?
- Day 2: useState와 useEffect 기본 Hook을 배웠어요
- Day 5: useEffect 생명주기를 다뤘어요
- Day 7: Context API 전역 상태를 관리했어요
오늘은 반복되는 로직을 Custom Hook으로 재사용하는 방법을 배워봅시다!
Custom Hook이란?
Custom Hook은 여러분이 직접 만드는 Hook이에요. useState, useEffect 같은 기본 Hook들을 조합해서 반복되는 로직을 재사용 가능하게 만드는 거예요.
일상 비유 🍳
요리를 생각해보세요:
매번 재료부터 준비하기:
1
2
3
4
5
6
7
8
9
10
11
계란후라이 만들기:
1. 프라이팬 꺼내기
2. 기름 두르기
3. 계란 깨서 넣기
4. 익히기
베이컨 굽기:
1. 프라이팬 꺼내기
2. 기름 두르기
3. 베이컨 넣기
4. 익히기
요리 도구 세트 준비 (Custom Hook):
1
2
3
4
5
6
7
8
9
프라이팬 세트 = "프라이팬 꺼내기 + 기름 두르기"
계란후라이 만들기:
1. 프라이팬 세트 사용
2. 계란 넣고 익히기
베이컨 굽기:
1. 프라이팬 세트 사용
2. 베이컨 넣고 익히기
Custom Hook도 똑같아요! 반복되는 로직을 하나로 묶어서 여러 곳에서 재사용하는 거예요.
왜 Custom Hook을 사용할까?
Before: 중복 코드의 문제
같은 로직을 여러 컴포넌트에서 사용할 때, 복사 붙여넣기를 하면:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 컴포넌트 1
function ProfilePage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []);
if (loading) return <div>로딩중...</div>;
return <div>{user.name}</div>;
}
// 컴포넌트 2 - 똑같은 코드 반복!
function SettingsPage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []);
if (loading) return <div>로딩중...</div>;
return <div>{user.email}</div>;
}
문제점:
- 똑같은 코드를 여러 번 작성
- 수정할 때 모든 곳을 다 바꿔야 함
- 실수하기 쉬움
After: Custom Hook 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Custom Hook 만들기
function useUser() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []);
return { user, loading };
}
// 컴포넌트 1 - 간결!
function ProfilePage() {
const { user, loading } = useUser();
if (loading) return <div>로딩중...</div>;
return <div>{user.name}</div>;
}
// 컴포넌트 2 - 똑같이 간결!
function SettingsPage() {
const { user, loading } = useUser();
if (loading) return <div>로딩중...</div>;
return <div>{user.email}</div>;
}
장점:
- ✅ 코드 중복 제거
- ✅ 한 곳만 수정하면 모든 곳에 적용
- ✅ 테스트하기 쉬움
- ✅ 읽기 쉬운 코드
Custom Hook 만드는 규칙
규칙 1: 이름은 “use”로 시작
✅ 좋은 이름:
useUseruseToggleuseLocalStorage
❌ 나쁜 이름:
getUser(use로 시작하지 않음)Toggle(use로 시작하지 않음)
규칙 2: Hook 안에서 다른 Hook 호출 가능
1
2
3
4
5
6
7
8
9
10
function useCustomHook() {
const [state, setState] = useState(0); // ✅
const value = useContext(MyContext); // ✅
useEffect(() => { // ✅
// ...
}, []);
return { state, value };
}
규칙 3: 컴포넌트처럼 최상위에서만 호출
❌ 잘못된 사용:
1
2
3
4
5
function Component() {
if (condition) {
const data = useCustomHook(); // 조건문 안에서 사용 금지!
}
}
✅ 올바른 사용:
1
2
3
4
5
6
7
function Component() {
const data = useCustomHook(); // 최상위에서 호출
if (condition) {
// data 사용
}
}
실습 1: useToggle (토글 상태 관리)
모달, 메뉴, 체크박스 등 On/Off 상태를 관리하는 가장 간단한 Custom Hook이에요!
만들기
1
2
3
4
5
6
7
8
9
10
11
import { useState } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => {
setValue(!value);
};
return [value, toggle];
}
사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function App() {
const [isOpen, toggleOpen] = useToggle(false);
const [isChecked, toggleChecked] = useToggle(true);
return (
<div style=>
<h1>useToggle 데모</h1>
{/* 모달 예제 */}
<div style=>
<button
onClick={toggleOpen}
style=
>
{isOpen ? '모달 닫기' : '모달 열기'}
</button>
{isOpen && (
<div
style=
>
<h3>모달 내용</h3>
<p>useToggle로 간단하게 모달을 열고 닫을 수 있어요!</p>
<button onClick={toggleOpen}>닫기</button>
</div>
)}
</div>
{/* 체크박스 예제 */}
<div style=>
<label style=>
<input
type="checkbox"
checked={isChecked}
onChange={toggleChecked}
style=
/>
<span style=>
알림 받기 {isChecked ? '✅' : '❌'}
</span>
</label>
</div>
{/* 상태 표시 */}
<div
style=
>
<p><strong>모달:</strong> {isOpen ? '열림' : '닫힘'}</p>
<p><strong>알림:</strong> {isChecked ? '켜짐' : '꺼짐'}</p>
</div>
</div>
);
}
export default App;
해보기:
- 모달 열기/닫기 버튼을 클릭해보세요
- 체크박스를 클릭해보세요
- 상태가 실시간으로 업데이트되는지 확인하세요
useToggle의 장점:
- 매번
useState와 토글 함수를 만들 필요 없음 - 한 줄로 간단하게 토글 상태 생성
- 여러 곳에서 재사용 가능
실습 2: useLocalStorage (로컬 저장소 동기화)
브라우저의 localStorage에 데이터를 저장하고 불러오는 Hook이에요. 페이지를 새로고침해도 데이터가 유지돼요!
만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// localStorage에서 초기값 가져오기
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// value가 바뀔 때마다 localStorage에 저장
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
function App() {
const [name, setName] = useLocalStorage('user-name', '');
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [todos, setTodos] = useLocalStorage('todos', []);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, { id: Date.now(), text: input }]);
setInput('');
}
};
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div
style={{
padding: '40px',
minHeight: '100vh',
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
<h1>📦 useLocalStorage 데모</h1>
{/* 이름 입력 */}
<div style={{ marginBottom: '30px' }}>
<h2>사용자 이름</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="이름을 입력하세요"
style={{
padding: '10px',
fontSize: '16px',
width: '300px',
border: '2px solid #ddd',
borderRadius: '5px'
}}
/>
{name && (
<p style={{ marginTop: '10px' }}>
안녕하세요, <strong>{name}</strong>님! 👋
</p>
)}
</div>
{/* 테마 전환 */}
<div style={{ marginBottom: '30px' }}>
<h2>테마 설정</h2>
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
style={{
padding: '10px 20px',
fontSize: '16px',
backgroundColor: theme === 'light' ? '#333' : '#fff',
color: theme === 'light' ? '#fff' : '#333',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
{theme === 'light' ? '🌙 다크모드' : '☀️ 라이트모드'}
</button>
</div>
{/* 할 일 목록 */}
<div style={{ marginBottom: '30px' }}>
<h2>할 일 목록</h2>
<div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="할 일을 입력하세요"
style={{
flex: 1,
padding: '10px',
fontSize: '16px',
border: '2px solid #ddd',
borderRadius: '5px'
}}
/>
<button
onClick={addTodo}
style={{
padding: '10px 20px',
fontSize: '16px',
backgroundColor: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}
>
추가
</button>
</div>
<div>
{todos.length === 0 ? (
<p style={{ color: '#999' }}>할 일이 없습니다</p>
) : (
todos.map(todo => (
<div
key={todo.id}
style={{
padding: '12px',
marginBottom: '8px',
backgroundColor: theme === 'light' ? '#f5f5f5' : '#444',
borderRadius: '5px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<span>{todo.text}</span>
<button
onClick={() => removeTodo(todo.id)}
style={{
padding: '5px 12px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer'
}}
>
삭제
</button>
</div>
))
)}
</div>
</div>
{/* 안내 메시지 */}
<div
style={{
padding: '20px',
backgroundColor: theme === 'light' ? '#e3f2fd' : '#1a237e',
borderRadius: '8px',
border: `2px solid ${theme === 'light' ? '#2196f3' : '#3f51b5'}`
}}
>
<p><strong>💡 팁:</strong> 페이지를 새로고침(F5)해도 데이터가 유지됩니다!</p>
<p>브라우저의 localStorage에 자동으로 저장되기 때문이에요.</p>
</div>
</div>
);
}
export default App;
해보기:
- 이름을 입력하고 페이지를 새로고침(F5) 해보세요 - 이름이 그대로 유지돼요!
- 테마를 바꾸고 새로고침해보세요 - 테마가 유지돼요!
- 할 일을 추가하고 새로고침해보세요 - 할 일이 그대로 있어요!
useLocalStorage의 장점:
- 페이지 새로고침해도 데이터 유지
- localStorage 사용 로직을 직접 작성할 필요 없음
- useState와 똑같이 사용하기만 하면 자동으로 저장됨
실습 3: useWindowSize (창 크기 감지)
브라우저 창 크기가 바뀔 때를 감지하는 Hook이에요. 반응형 디자인에 유용해요!
만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// Cleanup: 컴포넌트가 사라질 때 이벤트 제거
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return size;
}
사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function App() {
const { width, height } = useWindowSize();
// 화면 크기에 따라 다른 메시지 표시
const getDeviceType = () => {
if (width < 768) return '📱 모바일';
if (width < 1024) return '📱 태블릿';
return '💻 데스크톱';
};
return (
<div
style=
>
<h1>📏 useWindowSize 데모</h1>
<div
style=
>
<h2>현재 창 크기</h2>
<p style=>
{width} × {height}
</p>
<h2 style=>디바이스 종류</h2>
<p style=>
{getDeviceType()}
</p>
<div
style=
>
<h3>반응형 가이드</h3>
<p>📱 모바일: 0 ~ 767px</p>
<p>📱 태블릿: 768 ~ 1023px</p>
<p>💻 데스크톱: 1024px ~</p>
</div>
<div
style=
>
<p><strong>💡 팁:</strong> 브라우저 창 크기를 조절해보세요!</p>
<p>실시간으로 크기가 업데이트됩니다.</p>
</div>
</div>
</div>
);
}
export default App;
해보기:
- 브라우저 창 크기를 조절해보세요 (창을 드래그해서 크기 변경)
- 크기가 실시간으로 업데이트되는지 확인하세요
- 디바이스 종류가 바뀌는 것을 확인하세요
useWindowSize의 장점:
- 반응형 UI를 쉽게 만들 수 있음
- 이벤트 리스너 등록/제거를 신경 쓸 필요 없음
- 여러 컴포넌트에서 재사용 가능
실습 4: useDebounce (입력 지연 처리)
사용자가 타이핑을 멈춘 후에 검색을 실행하는 등, 성능 최적화에 유용한 Hook이에요!
만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// delay 후에 값 업데이트
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 다음 effect 실행 전에 타이머 정리
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
function App() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const [results, setResults] = useState([]);
const [searchCount, setSearchCount] = useState(0);
// 모의 데이터
const allItems = [
'React', 'React Native', 'Redux', 'React Router',
'JavaScript', 'TypeScript', 'Next.js', 'Vue.js',
'Angular', 'Svelte', 'Node.js', 'Express',
'MongoDB', 'PostgreSQL', 'MySQL', 'GraphQL'
];
useEffect(() => {
if (debouncedSearchTerm) {
// 검색 실행 (타이핑 멈춘 후 500ms 뒤)
setSearchCount(prev => prev + 1);
const filtered = allItems.filter(item =>
item.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
);
setResults(filtered);
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
<div style=>
<h1>🔍 useDebounce 데모</h1>
<div style=>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="기술 검색... (예: React)"
style=
/>
</div>
<div
style=
>
<p><strong>입력값:</strong> {searchTerm || '(없음)'}</p>
<p><strong>Debounced 값:</strong> {debouncedSearchTerm || '(없음)'}</p>
<p><strong>검색 실행 횟수:</strong> {searchCount}번</p>
</div>
<div
style=
>
<p><strong>💡 설명:</strong></p>
<p>- 입력값은 타이핑할 때마다 즉시 바뀝니다</p>
<p>- Debounced 값은 타이핑을 멈춘 후 0.5초 뒤에 바뀝니다</p>
<p>- 검색은 Debounced 값이 바뀔 때만 실행됩니다</p>
</div>
{searchTerm && (
<div>
<h2>검색 결과 ({results.length}개)</h2>
{results.length > 0 ? (
<div style=>
{results.map((item, index) => (
<div
key={index}
style=
>
{item}
</div>
))}
</div>
) : (
<p style=>결과가 없습니다</p>
)}
</div>
)}
</div>
);
}
export default App;
해보기:
- 검색창에 “React”를 빠르게 타이핑해보세요
- “입력값”은 즉시 바뀌지만, “Debounced 값”은 0.5초 후에 바뀌는 것을 확인하세요
- 검색 실행 횟수를 보세요 - debounce 덕분에 횟수가 줄어들어요!
useDebounce의 장점:
- 불필요한 검색/API 호출 감소
- 성능 최적화
- 서버 부하 감소
useDebounce 없이 매번 검색하면?
- “React” 타이핑 시 → 검색 5번 실행 (R, Re, Rea, Reac, React)
- useDebounce 사용 시 → 검색 1번 실행 (타이핑 멈춘 후)
Custom Hook 모음 파일 만들기
여러 Custom Hook을 한 파일에 모아서 관리하면 편리해요!
src/hooks/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { useState, useEffect } from 'react';
// useToggle
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(!value);
return [value, toggle];
}
// useLocalStorage
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
// useWindowSize
export function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// useDebounce
export function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useToggle, useLocalStorage, useWindowSize, useDebounce } from './hooks';
function App() {
const [isOpen, toggle] = useToggle();
const [name, setName] = useLocalStorage('name', '');
const { width, height } = useWindowSize();
const debouncedName = useDebounce(name, 300);
return (
<div>
<p>모달: {isOpen ? '열림' : '닫힘'}</p>
<p>창 크기: {width} × {height}</p>
<p>Debounced 이름: {debouncedName}</p>
</div>
);
}
흔한 실수와 해결 방법
실수 1: “use”로 시작하지 않음
❌ 잘못된 코드:
1
2
3
4
function toggleHook() { // use가 없어요!
const [value, setValue] = useState(false);
return [value, () => setValue(!value)];
}
⚠️ 경고: React Hook 규칙 위반!
✅ 올바른 코드:
1
2
3
4
function useToggle() { // use로 시작!
const [value, setValue] = useState(false);
return [value, () => setValue(!value)];
}
실수 2: 조건부로 Hook 호출
❌ 잘못된 코드:
1
2
3
4
5
function useData(shouldFetch) {
if (shouldFetch) {
const data = useState(null); // 조건문 안에서 Hook 호출!
}
}
✅ 올바른 코드:
1
2
3
4
5
6
7
8
9
10
function useData(shouldFetch) {
const [data, setData] = useState(null); // 최상위에서 호출
useEffect(() => {
if (shouldFetch) {
// 조건은 useEffect 안에서
fetchData();
}
}, [shouldFetch]);
}
실수 3: Cleanup 함수 누락
❌ 메모리 누수 발생:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
// cleanup 함수가 없어요! 메모리 누수 발생!
}, []);
return size;
}
✅ 올바른 코드:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
// cleanup 함수 추가!
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return size;
}
Custom Hook 설계 팁
팁 1: 하나의 역할만 하기
✅ 좋은 예 (Single Responsibility):
1
2
function useToggle() { /* 토글만 */ }
function useLocalStorage() { /* 저장만 */ }
❌ 나쁜 예 (Too Many Responsibilities):
1
2
3
function useEverything() {
// 토글도 하고, 저장도 하고, 검색도 하고... 너무 많음!
}
팁 2: 명확한 이름 사용
✅ 좋은 이름:
useWindowSize- 창 크기를 가져온다useLocalStorage- localStorage를 사용한다useDebounce- 값을 debounce 한다
❌ 나쁜 이름:
useData- 무슨 데이터?useHelper- 무엇을 도와주는지?
팁 3: 값을 반환할 때 명확하게
객체로 반환 (값이 여러 개일 때):
1
2
3
4
5
function useWindowSize() {
return { width, height }; // 객체
}
const { width, height } = useWindowSize();
배열로 반환 (값과 함수 쌍일 때):
1
2
3
4
5
function useToggle() {
return [value, toggle]; // 배열
}
const [isOpen, toggleOpen] = useToggle();
정리
오늘은 Custom Hook으로 로직을 재사용하는 방법을 배웠어요!
핵심 포인트 ✅
Custom Hook이란?:
- 여러분이 직접 만드는 Hook
- 반복되는 로직을 재사용 가능하게 만듦
- 이름은 반드시 “use”로 시작
실전 Custom Hooks:
useToggle: On/Off 상태 관리useLocalStorage: 브라우저 저장소 동기화useWindowSize: 창 크기 감지useDebounce: 입력 지연 처리
설계 원칙:
- 하나의 역할만 하기
- 명확한 이름 사용하기
- Cleanup 함수 잊지 않기
오늘 배운 내용 체크 ✅
- Custom Hook이 무엇인지 이해했나요?
- “use”로 시작하는 이유를 알고 있나요?
- useToggle을 직접 만들어봤나요?
- useLocalStorage로 데이터를 저장해봤나요?
- useDebounce로 검색을 최적화해봤나요?
숙제 📚
- useCounter 만들기: 카운터 증가/감소/리셋 기능
- useFetch 만들기: API 데이터 가져오기
- useClickOutside 만들기: 외부 클릭 감지 (모달 닫기에 유용)
다음 단계
다음 포스트에서는 React Router를 배워봅니다! SPA(Single Page Application)에서 페이지 전환을 구현하는 방법을 알아봐요.
“늦었다고 생각할 때가 가장 빠를 때입니다. Custom Hook 마스터! 🎉”
React 완벽 가이드 시리즈
- React란 무엇인가?
- 첫 React 앱 만들기
- useState - 클릭하면 바뀌는 화면
- Props - 컴포넌트끼리 대화하기
- useEffect - 컴포넌트 생명주기
- 조건부/리스트 렌더링
- Context API - Props 지옥에서 탈출
- Custom Hooks - 반복되는 로직 재사용하기 ← 현재 글
- React Router 완벽 가이드
- 폼 처리와 유효성 검증
- React 성능 최적화
- 에러 바운더리와 Suspense
- 상태 관리 라이브러리
- 서버 상태 관리
- React와 TypeScript
- Next.js 입문
- React 테스팅 전략
- 고급 패턴과 베스트 프랙티스
- 실전 프로젝트
- React 배포와 DevOps
