๐ช 22. ์ปค์คํ ํ ์ค๊ณ ์ฒ ํ: ๋ก์ง์ ์บก์ํ์ ํฉ์ฑ
๐ ๊ฐ์
๋ณต๋ถ ์ง์ฅ์์ ํ์ถํ๋ ์ปค์คํ ํ ์ถ์ถ ๊ธฐ์ค๋ถํฐ, useAsyncยทuseLocalStorageยทuseDebounce ๋ฑ ์ค๋ฌด ํ ์ค๊ณ ํจํด๊น์ง. 'ํ ์ด์ด์ผ ํ๋๊ฐ, ์ปดํฌ๋ํธ์ฌ์ผ ํ๋๊ฐ'์ ํ๋จ ๊ธฐ์ค์ ๋ช ํํ ๋ค์ง๋๋ค.
๐ ๋ชฉ์ฐจ
- ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ์ ์์์ผ ํ๋๊ฐ: ๋ณต๋ถ ์ง์ฅ์ ๊ณตํฌ
- ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ์ปค์คํ ํ ์ถ์ถ ํ๋จ ๊ธฐ์ค
- ์ค๋ฌด ํ ํจํด 1: useAsync
- ์ค๋ฌด ํ ํจํด 2: useLocalStorage
- ์ค๋ฌด ํ ํจํด 3: useDebounce
- ์ค๋ฌด ํ ํจํด 4: useIntersectionObserver
- ํ ๋ฐํ๊ฐ API ์ค๊ณ
- ํ ํฉ์ฑ ํจํด
- ํ vs ์ปดํฌ๋ํธ ๊ฒฝ๊ณ์
- ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[๋ณต๋ถ ๋ฌธ์ ์ธ์] โ [์ถ์ถ ๊ธฐ์ค ์ฒด๋] โ [์ค๋ฌด ํ
4์ข
์ง์ ๋ง๋ค๊ธฐ] โ [ํ
ํฉ์ฑ ํจํด] โ [ํ
vs ์ปดํฌ๋ํธ ๊ฒฝ๊ณ]
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- "์ด ๋ก์ง์ด ํ ์ผ๋ก ์ถ์ถ๋์ด์ผ ํ๋๊ฐ?" ๋ฅผ 5์ด ์์ ํ๋จํ ์ ์๋ค
-
useAsync,useLocalStorage,useDebounce๋ฅผ ์ง์ ์ค๊ณํ ์ ์๋ค - ํ ๋ฐํ๊ฐ์ ๋ฐฐ์ด vs ๊ฐ์ฒด๋ก ์ธ์ ํด์ผ ํ๋์ง ์ค๋ช ํ ์ ์๋ค
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์ ์
): "๊ฒ์๊ธ ๋ชฉ๋ก, ์ ์ ๋ชฉ๋ก, ๋๊ธ ๋ชฉ๋ก... ์ธ ์ปดํฌ๋ํธ์
useState + useEffect + fetchํจํด์ ์์ ํ ๋๊ฐ์ด ๋ณต๋ถํด๋จ๋๋ฐ, ๋ก๋ฉ ๋ก์ง์ ๋ฒ๊ทธ๊ฐ ๋์ ์ธ ๊ตฐ๋ฐ๋ฅผ ๋ค ๊ณ ์ณ์ผ ํด์ ใ ใ " - ์ํธ(๋ฆฌ๋): "์์ฒ ๋, ๋ณต๋ถํ ์๊ฐ ๊ทธ ์ฝ๋๋ ์ธ ๊ฐ์ ๋ ๋ฆฝ๋ ๋ฒ๊ทธ ํ๋ณด๊ฐ ๋ฉ๋๋ค. ๊ทธ ํจํด, ์ง๊ธ ๋น์ฅ ์ปค์คํ ํ ํ๋๋ก ๋นจ์๋ค์ด๊ฒ ์ต๋๋ค."
๐ค ์ ์์์ผ ํ๋๊ฐ: ๋ณต๋ถ ์ง์ฅ์ ๊ณตํฌ
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ณต๋ถ์ด ์ ๊ธฐ์ ๋ถ์ฑ์ ์จ์์ธ์ง ์ค๋ช ํ ์ ์๋ค
- ์ปค์คํ ํ ์ด "์ปดํฌ๋ํธ ๋ถ๋ฆฌ"์ ๋ฌด์์ด ๋ค๋ฅธ์ง ๊ตฌ๋ถํ ์ ์๋ค
'์์๋ค ์ปค๋ฎค๋ํฐ' ์ฑ์ด ์ปค์ง๋ฉด์ ์์ฒ ์ด๋ ๋ฐ์ดํฐ ํจ์นญ ์ฝ๋๋ฅผ ์ธ ๊ตฐ๋ฐ์ ๋ณต๋ถํ์ด์.
// โ PostList.tsx โ ์์ฒ ์ด์ ๋ณต๋ถ 1๋ฒ
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchPosts()
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <Spinner />;
if (error) return <ErrorMessage />;
return <ul>{posts.map(p => <PostItem key={p.id} post={p} />)}</ul>;
}
// โ UserList.tsx โ ๋ณต๋ถ 2๋ฒ (loading ๋ฆฌ์
์ finally๋ก ์ ์ด ๊ฒ ์ฃผ๋ชฉ)
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true); // ๐ ๋ฒ๊ทธ ์จ์: loading ์ด๊ธฐํ ํ์ด๋ฐ ๋ค๋ฆ
const [error, setError] = useState(null);
useEffect(() => {
fetchUsers()
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err);
// ๐ฃ ์ฌ๊ธฐ์ setLoading(false) ๋น ๋จ๋ฆผ! โ ์๋ฌ๋๋ฉด ์์ํ ๋ก๋ฉ ์ค ํ์
});
}, []);
// ...
}์ด ์ฝ๋์ ๋ฌธ์ ์ ์ ๋ถ๋ช ํด์:
- ๋ฒ๊ทธ ํ๋, ์์ ์ธ ๊ณณ:
loading๋ฆฌ์ ๋ฒ๊ทธ๊ฐUserList์๋ง ์์ด๋, ๋์ค์CommentList์๋ ๊ฐ์ ๋ฒ๊ทธ๊ฐ ์๊ธธ ๊ฐ๋ฅ์ฑ์ด ๋์์ - ํ
์คํธ ๋ถ๊ฐ๋ฅ:
useEffect๊ฐ ์ปดํฌ๋ํธ ์์ ๋ฐํ์์ผ๋ฉด ๋จ๋ ์ผ๋ก ํ ์คํธํ ์ ์์ด์ - ์งํ ๋น์ฉ ์ฆ๊ฐ: ๋์ค์ "์์ฒญ ์ทจ์(cancel)" ๊ธฐ๋ฅ์ ์ถ๊ฐํ ๋, ์ธ ๊ตฐ๋ฐ ๋ชจ๋ ์์ ํด์ผ ํด์
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"๋ณต๋ถ์ ๋ฒ๊ทธ์ ์จ์์ ์ธ ๊ตฐ๋ฐ์ ์ฌ๋ ๊ฒ์ด๋ค. ๊ฐ์ ๋ก์ง์ ๋ฐ๋์ ํ๋์ ํ ์ผ๋ก ์ด์์ผ ํ๋ค."
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
์์๋ค ์ปค๋ฎค๋ํฐ ํ์ด ์ ๊ธฐ๋ฅ์ ๋ง๋ค ๋๋ง๋ค ๋งค๋ฒ ์ ์๋ฒ๋ฅผ ์ฒ์๋ถํฐ ์กฐ๋ฆฝํ๋ค๊ณ ์์ํด๋ด.
CPU ๋ผ์ฐ๊ณ , ๋ฉ๋ชจ๋ฆฌ ๊ฝ๊ณ , ์ ์ ์ฐ๊ฒฐํ๊ณ ... ์ธ ๋ฒ ๋ฐ๋ณตํ๋ฉด ์ธ ๊ฐ์ ์๋ฒ๊ฐ ์๊ธฐ๋๋ฐ, ๋์ค์ ์ ์ ์ผ์ด๋ธ ๊ท๊ฒฉ์ด ๋ฐ๋๋ฉด ์ธ ๊ฐ๋ฅผ ๋ค ๋ถํดํด์ผ ํด.์ปค์คํ ํ ์ "์กฐ๋ฆฝ ๋งค๋ด์ผ ํ์ผ ํ ์ฅ" ์ด์ผ.
๋งค๋ด์ผ์ ์ ๋ฐ์ดํธํ๋ฉด, ๊ทธ ๋งค๋ด์ผ๋๋ก ๋ง๋ค์ด์ง ์๋ฒ๋ค์ ์๋์ผ๋ก ์ต์ ๋ฐฉ๋ฒ์ผ๋ก ๋์ํด.
๋ณต๋ถ์ "๋งค๋ด์ผ ๋ณต์ฌ๋ณธ ์ธ ์ฅ"์ด๊ณ , ํ ์ "์๋ณธ ๋งค๋ด์ผ ํ๋๋ฅผ ์ธ ๊ณณ์์ ์ฐธ์กฐํ๋ ๊ฒ"์ด์ผ.
๐ ์ปค์คํ ํ ์ถ์ถ ํ๋จ ๊ธฐ์ค ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- 5์ด ์์ "ํ ์ผ๋ก ๋บ๊น, ์ปดํฌ๋ํธ๋ก ๋บ๊น, ๊ทธ๋ฅ ์ ํธ ํจ์๋ก ๋บ๊น"๋ฅผ ํ๋จํ ์ ์๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
์๋ ์ธ ๊ฐ์ง ์ค ์ด๋ค ๊ฒ ์ปค์คํ ํ ์ผ๋ก ์ถ์ถ๋์ด์ผ ํ ๊น?
useState+useEffect๋ก API๋ฅผ ํธ์ถํ๋ 20์ค ๋ก์ง<ul>ํ๊ทธ๋ก ์์ดํ ์ ๋ ๋๋งํ๋ JSX- ๋ฐฐ์ด์ ์ ๋ ฌํ๋
sortByDate(items)ํจ์
์ปค์คํ ํ ์ถ์ถ ๊ฒฐ์ ํธ๋ฆฌ:
์ด ์ฝ๋๋ฅผ ์ฌ์ฌ์ฉํ๊ณ ์ถ์๊ฐ?
โ
โโ JSX(UI)๋ฅผ ๋ฐํํ๋ค
โ โโ โ ์ปดํฌ๋ํธ๋ก ์ถ์ถ
โ
โโ React Hook(useState, useEffect ๋ฑ)์ ์ฌ์ฉํ๋ค
โ โโ โ ์ปค์คํ
ํ
์ผ๋ก ์ถ์ถ
โ
โโ Hook์ ์ ํ ์ฌ์ฉํ์ง ์๋ ์์ ๋ก์ง์ด๋ค
โโ โ ์ ํธ ํจ์๋ก ์ถ์ถ (hooks/ ํด๋ ์๋, utils/ ํด๋)
ํ๋จ ๊ธฐ์ค ์์ฝํ:
| ์ํฉ | ์ ํ | ์ด์ |
|---|---|---|
useState + useEffect ์กฐํฉ์ด 2๊ณณ ์ด์ | ์ปค์คํ ํ | ๋ก์ง ์ฌ์ฌ์ฉ |
| ๊ฐ์ UI(JSX)๊ฐ 2๊ณณ ์ด์ | ์ปดํฌ๋ํธ | UI ์ฌ์ฌ์ฉ |
| ํ ์๋ ์์ ๊ณ์ฐ ํจ์ | ์ ํธ ํจ์ | ํ ๋ถํ์ |
| ์ปดํฌ๋ํธ ์์ ์กฐ๊ฑด๋ฌธ์ด ๋๋ฌด ๋ณต์ก | ์ปค์คํ ํ | ๋ก์ง ๋ถ๋ฆฌ |
โ ๏ธ ์ฃผ์: ํ ์ด JSX๋ฅผ ๋ฐํํ๋ฉด ์ ๋ผ
// โ ํ ์ฒ๋ผ ์๊ฒผ์ง๋ง ํ ์ด ์๋ โ ์ปดํฌ๋ํธ์ function useUserProfile(userId: string) { const user = useUser(userId); return <div>{user.name}</div>; // JSX ๋ฐํ โ ์ด๊ฑด ํ ์ด ์๋๋ผ ์ปดํฌ๋ํธ! } // โ ํ ์ ๋ฐ์ดํฐ/๋ก์ง๋ง ๋ฐํ function useUserProfile(userId: string) { const [user, setUser] = useState(null); // ... ๋ก์ง return { user, loading, error }; // ๋ฐ์ดํฐ๋ง ๋ฐํ }
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"ํ ์ ๋ก์ง์ ์บก์ํํ๊ณ , ์ปดํฌ๋ํธ๋ UI๋ฅผ ์บก์ํํ๋ค. JSX๊ฐ ๋์ค๋ฉด ํ ์ด ์๋๋ค."
โก ์ค๋ฌด ํ ํจํด 1: useAsync ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋น๋๊ธฐ ๋ฐ์ดํฐ ํจ์นญ ๋ก์ง ์ ์ฒด๋ฅผ ํ ํ๋๋ก ์ถ์ํํ ์ ์๋ค
- ๊ฒฝ์ ์กฐ๊ฑด(Race Condition) ๋ฒ๊ทธ๋ฅผ ํด๋ฆฐ์ ํจ์๋ก ๋ฐฉ์งํ ์ ์๋ค
useAsync ๋ ๋ชจ๋ ๋น๋๊ธฐ ํธ์ถ์ ๊ณตํต์ผ๋ก ํ์ํ loading / data / error ์ํ๋ฅผ ํตํฉ ๊ด๋ฆฌํด์.
// hooks/useAsync.ts
import { useState, useEffect } from 'react';
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
// T: ๋น๋๊ธฐ ํจ์๊ฐ ๋ฐํํ๋ ๋ฐ์ดํฐ์ ํ์
function useAsync<T>(
asyncFn: () => Promise<T>, // ์คํํ ๋น๋๊ธฐ ํจ์
deps: React.DependencyList = [] // useEffect ์์กด์ฑ ๋ฐฐ์ด
): AsyncState<T> {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let cancelled = false; // ์ธ๋ง์ดํธ ํ setState ํธ์ถ ๋ฐฉ์ง์ฉ ํ๋๊ทธ
// ๋งค ์คํ๋ง๋ค ์ด๊ธฐ ์ํ๋ก ๋ฆฌ์
setState({ data: null, loading: true, error: null });
asyncFn()
.then(data => {
if (!cancelled) { // ์ธ๋ง์ดํธ๋์ผ๋ฉด ์ํ ์
๋ฐ์ดํธ ๊ฑด๋๋
setState({ data, loading: false, error: null });
}
})
.catch(error => {
if (!cancelled) {
setState({ data: null, loading: false, error });
}
});
return () => {
cancelled = true; // ํด๋ฆฐ์
: deps ๋ณ๊ฒฝ or ์ธ๋ง์ดํธ ์ ์ด์ ์์ฒญ ์ทจ์
};
}, deps);
return state;
}
export default useAsync;์ฌ์ฉ ์์ โ ๋ณต๋ถ ์ง์ฅ ํ์ถ:
// โ
PostList.tsx โ ์ด์ ๋จ ๋ ์ค๋ก ๋ฐ์ดํฐ ํจ์นญ ๋
function PostList() {
const { data: posts, loading, error } = useAsync(fetchPosts, []);
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
return <ul>{posts!.map(p => <PostItem key={p.id} post={p} />)}</ul>;
}
// โ
UserList.tsx โ ์์ ํ ๋์ผํ ํจํด, ๋ฒ๊ทธ ์์
function UserList() {
const { data: users, loading, error } = useAsync(fetchUsers, []);
// ...
}
// โ
deps๊ฐ ์๋ ๊ฒฝ์ฐ: userId ๋ฐ๋๋ฉด ์๋ ์ฌ์์ฒญ
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading } = useAsync(
() => fetchUser(userId), // userId ํด๋ก์ ์บก์ฒ
[userId] // userId ๋ฐ๋๋ฉด ์ฌ์คํ
);
}๊ฒฝ์ ์กฐ๊ฑด(Race Condition) ๋ฐฉ์ง ์๋ฆฌ:
// โ cancelled ํ๋๊ทธ ์๋ ๊ฒฝ์ฐ์ ์ฌ์
// userId=1 ์์ฒญ(๋๋ฆผ) โ userId=2 ์์ฒญ(๋น ๋ฆ) โ userId=2 ๊ฒฐ๊ณผ ํ์
// โ userId=1 ๊ฒฐ๊ณผ ๋ฆ๊ฒ ๋์ฐฉ โ userId=1 ๋ฎ์ด์!
// ํ๋ฉด์๋ userId=2์ธ๋ฐ ๋ฐ์ดํฐ๋ userId=1 โ ๋ฒ๊ทธ!
// โ
cancelled ํ๋๊ทธ๊ฐ ์์ผ๋ฉด:
// userId=2 ๋ก deps ๋ณ๊ฒฝ โ ํด๋ฆฐ์
์คํ(cancelled=true)
// โ userId=1 ์์ฒญ์ด ๋ค๋ฆ๊ฒ ์๋ฃ๋ผ๋ setState ๋ฌด์๋จ โ ์์ !์ค์ต ํ ์ฒดํฌ๋ฆฌ์คํธ:
-
useAsync์cancelledํ๋๊ทธ๊ฐ ์์ผ๋ฉด ์ด๋ค ๋ฒ๊ทธ๊ฐ ์๊ธฐ๋์ง ์ค๋ช ํ ์ ์๋ค -
deps๋ฐฐ์ด์ ์ญํ ์useEffect์ ๊ทธ๊ฒ๊ณผ ๋์ผํ๊ฒ ์ดํดํ๊ณ ์๋ค -
useAsync(fetchPosts, [])๋ฅผuseAsync(fetchPosts)๋ก ๋ฐ๊พธ๋ฉด ์ด๋ป๊ฒ ๋ค๋ฅธ์ง ์๋ค
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"useAsync๋ ๋ชจ๋ ๋ฐ์ดํฐ ํจ์นญ์ ๋ณด์ผ๋ฌํ๋ ์ดํธ(loading, error, data)๋ฅผ ๋จ ํ ์ค๋ก ์์ถํด์ค๋ค. ํด๋ฆฐ์ ์cancelledํ๋๊ทธ๊ฐ ๊ฒฝ์ ์กฐ๊ฑด์ ๋ง๋ ํต์ฌ์ด์ผ."
๐พ ์ค๋ฌด ํ ํจํด 2: useLocalStorage ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
localStorage์ React ์ํ๋ฅผ ๋๊ธฐํํ๋ ํ ์ ์ค๊ณํ ์ ์๋ค- ๊ฒ์ผ๋ฅธ ์ด๊ธฐํ(Lazy Initialization)์ ํ์์ฑ์ ์ดํดํ๋ค
useLocalStorage ๋ useState ์ฒ๋ผ ์ฐ์ง๋ง, ๊ฐ์ด ๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ์๋ ์ ์ง๋ผ์.
// hooks/useLocalStorage.ts
import { useState } from 'react';
function useLocalStorage<T>(key: string, initialValue: T) {
// ๊ฒ์ผ๋ฅธ ์ด๊ธฐํ(Lazy Init): ํจ์๋ฅผ ๋๊ธฐ๋ฉด ์ด๊ธฐ ๋ ๋๋ง ๋๋ง ์คํ๋จ
// โ ๋งค ๋ ๋๋ง๋ค localStorage๋ฅผ ์ฝ์ง ์์๋ ๋จ (์ฑ๋ฅ ์ต์ ํ)
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
// localStorage ์ ๊ทผ ๋ถ๊ฐ(SSR, ํ๋ผ์ด๋น ๋ชจ๋) ์ ์ด๊ธฐ๊ฐ ์ฌ์ฉ
return initialValue;
}
});
// useState์ setter์ฒ๋ผ ํจ์ํ ์
๋ฐ์ดํธ๋ ์ง์
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch {
console.warn(`localStorage์ '${key}' ์ ์ฅ ์คํจ`);
}
};
return [storedValue, setValue] as const; // useState์ฒ๋ผ [๊ฐ, setter] ๋ฐฐ์ด ๋ฐํ
}
export default useLocalStorage;์ฌ์ฉ ์์:
// โ
๋คํฌ๋ชจ๋ ์ค์ ์๊ตฌ ์ ์ฅ
function ThemeToggle() {
const [isDark, setIsDark] = useLocalStorage('theme', false);
// useState์ ์์ ํ ๋์ผํ ์ธํฐํ์ด์ค!
return (
<button onClick={() => setIsDark(prev => !prev)}>
{isDark ? 'โ๏ธ ๋ผ์ดํธ ๋ชจ๋' : '๐ ๋คํฌ ๋ชจ๋'}
</button>
);
}
// โ
๊ฒ์ ํ์คํ ๋ฆฌ ์ ์ฅ
function SearchHistory() {
const [history, setHistory] = useLocalStorage<string[]>('search-history', []);
const addSearch = (query: string) => {
setHistory(prev => [query, ...prev.slice(0, 9)]); // ์ต๋ 10๊ฐ ์ ์ง
};
}์ ๊ฒ์ผ๋ฅธ ์ด๊ธฐํ๊ฐ ์ค์ํ๊ฐ:
// โ ๊ฒ์ผ๋ฅธ ์ด๊ธฐํ ์์ด (๋์ ์)
const [value, setValue] = useState(localStorage.getItem(key)); // ๋งค ๋ ๋๋ง๋ค ์คํ!
// โ ์ปดํฌ๋ํธ๊ฐ 1์ด์ 60๋ฒ ๋ ๋๋๋ฉด localStorage๋ 60๋ฒ ์ฝ์
// โ
๊ฒ์ผ๋ฅธ ์ด๊ธฐํ ์ฌ์ฉ (์ข์ ์)
const [value, setValue] = useState(() => localStorage.getItem(key)); // ์ฒ์ ํ ๋ฒ๋ง ์คํ๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"useLocalStorage๋useState์ ์์ ํ ๋์ฒด์ ์ผ. ์ธํฐํ์ด์ค๊ฐ ๋์ผํ๋๊น ๊ธฐ์กด ์ฝ๋๋ฅผ ๋ฐ๊พธ์ง ์๊ณ ์์์ฑ(Persistence)๋ง ์ถ๊ฐํ ์ ์์ด."
โฑ๏ธ ์ค๋ฌด ํ ํจํด 3: useDebounce ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋๋ฐ์ด์ค๊ฐ ์ ํ์ํ์ง, ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์ค๋ช ํ ์ ์๋ค
useDebounce์useThrottle์ ์ฐจ์ด๋ฅผ ๊ตฌ๋ถํ ์ ์๋ค
๊ฒ์์ฐฝ์ ํ์ดํํ ๋๋ง๋ค API๋ฅผ ํธ์ถํ๋ฉด ์๋ฒ๊ฐ ํญ๊ฒฉ ๋ง์์. ๋๋ฐ์ด์ค(Debounce) ๋ "๋ง์ง๋ง ์ ๋ ฅ์ผ๋ก๋ถํฐ N์ด ํ์ ์คํ"ํ๋ ํจํด์ด์์.
// hooks/useDebounce.ts
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
// delay ํ์ ๊ฐ์ ์
๋ฐ์ดํธํ๋ ํ์ด๋จธ ์ค์
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// ํด๋ฆฐ์
: ์ value๊ฐ ๋ค์ด์ค๋ฉด ์ด์ ํ์ด๋จธ ์ทจ์ โ ์ฌ์ค์ ํ์ด๋จธ ๋ฆฌ์
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue; // delay๊ฐ ์ง๋ ํ์์ผ ๋ฐ๋๋ ๊ฐ
}
export default useDebounce;๋์ ์๋ฆฌ ํ์๋ผ์ธ:
์ฌ์ฉ์ ์
๋ ฅ: "๋ฆฌ" โ "๋ฆฌ์ก" โ "๋ฆฌ์กํธ" โ (500ms ์นจ๋ฌต) โ ์ค์ API ํธ์ถ
โ โ โ
ํ์ด๋จธ ์ทจ์ ํ์ด๋จธ ์ทจ์ ํ์ด๋จธ ์์!
์ฌ์ฉ ์์:
// โ
์ค์๊ฐ ๊ฒ์ โ ํ์ดํ ๋ฉ์ถ๊ณ 500ms ํ API ํธ์ถ
function SearchBar() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500); // ์ค์ API ํธ์ถ์ ์ธ ๊ฐ
// debouncedQuery๊ฐ ๋ฐ๋ ๋๋ง API ํธ์ถ (ํ์ดํ ์ค์ ํธ์ถ ์ ํจ)
const { data: results, loading } = useAsync(
() => searchPosts(debouncedQuery),
[debouncedQuery]
);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)} // ์ฆ์ ๋ฐ์ (UI๋ ๋น ๋ฅด๊ฒ)
placeholder="๊ฒ์๊ธ ๊ฒ์..."
/>
{loading && <Spinner />}
{results?.map(post => <PostItem key={post.id} post={post} />)}
</div>
);
}๋๋ฐ์ด์ค vs ์ฐ๋กํ(Throttle) ๋น๊ต:
| ๋๋ฐ์ด์ค(Debounce) | ์ฐ๋กํ(Throttle) | |
|---|---|---|
| ๋์ | ๋ง์ง๋ง ์ ๋ ฅ ํ N์ด ๋ค ์คํ | N์ด๋ง๋ค ์ต๋ 1๋ฒ ์คํ |
| ์ฉ๋ | ๊ฒ์์ด ์ ๋ ฅ, ํผ ์๋์ ์ฅ | ์คํฌ๋กค ์ด๋ฒคํธ, ๋ฆฌ์ฌ์ด์ฆ |
| ํน์ง | ์ฐ์ ์ ๋ ฅ ์ค์ ์คํ ์ ๋จ | ์ค๊ฐ์ค๊ฐ ์คํ๋จ |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"๋๋ฐ์ด์ค๋ '๋ง์ง๋ง ์ด์๋ง ๋ฐ์ฌ'๊ณ , ์ฐ๋กํ์ '์ผ์ ๊ฐ๊ฒฉ์ผ๋ก ๋ฐ์ฌ'. ๊ฒ์์ฐฝ์ ๋๋ฐ์ด์ค, ์คํฌ๋กค์ ์ฐ๋กํ์ด ์ ์์ด์ผ."
๐๏ธ ์ค๋ฌด ํ ํจํด 4: useIntersectionObserver ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
IntersectionObserverAPI๋ฅผ React ํ ์ผ๋ก ๊ฐ์ธ๋ ๋ฐฉ๋ฒ์ ์ ์ ์๋ค- ๋ฌดํ ์คํฌ๋กค(Infinite Scroll) ํธ๋ฆฌ๊ฑฐ๋ฅผ ๊ตฌํํ ์ ์๋ค
๋ฌดํ ์คํฌ๋กค ์ "๋ชฉ๋ก ๋งจ ์๋ ์์๊ฐ ํ๋ฉด์ ๋ณด์ด๋ฉด ๋ค์ ํ์ด์ง๋ฅผ ๋ก๋"ํ๋ ํจํด์ด์์. IntersectionObserver โ ๊ต์ฐจ ๊ด์ฐฐ์(๊ต์ฐจ ๊ฐ์์) โ ๋ฅผ ํ
์ผ๋ก ์ถ์ํํด์.
๐ ์ฉ์ด:
IntersectionObserverโ ํน์ DOM ์์๊ฐ ๋ทฐํฌํธ(ํ๋ฉด)์ ๊ต์ฐจ(๊ฒน์น๋์ง)๋ฅผ ๊ฐ์ํ๋ ๋ธ๋ผ์ฐ์ ๋ด์ฅ API. ์คํฌ๋กค ์ด๋ฒคํธ๋ณด๋ค ํจ์ฌ ์ฑ๋ฅ์ด ์ข์์.
// hooks/useIntersectionObserver.ts
import { useEffect, useRef, useState } from 'react';
interface UseIntersectionObserverOptions {
threshold?: number; // 0~1: ์์๊ฐ ์ผ๋ง๋ ๋ณด์ฌ์ผ ๊ต์ฐจ๋ก ์ธ์ํ ์ง (0.1 = 10%)
rootMargin?: string; // ๋ทฐํฌํธ ์ฌ๋ฐฑ (ex: '0px 0px 100px' = ์๋์ชฝ 100px ๋ฏธ๋ฆฌ ๊ฐ์ง)
}
function useIntersectionObserver(options: UseIntersectionObserverOptions = {}) {
const [isIntersecting, setIsIntersecting] = useState(false);
const ref = useRef<HTMLDivElement>(null); // ๊ฐ์ํ DOM ์์์ ๋ถ์ผ ref
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting); // ํ๋ฉด์ ๋ณด์ด๋ฉด true
}, options);
observer.observe(element); // ๊ฐ์ ์์
return () => observer.unobserve(element); // ํด๋ฆฐ์
: ๊ฐ์ ํด์
}, []);
return { ref, isIntersecting };
}
export default useIntersectionObserver;๋ฌดํ ์คํฌ๋กค ๊ตฌํ ์์:
// โ
๊ฒ์๊ธ ๋ฌดํ ์คํฌ๋กค
function PostFeed() {
const [page, setPage] = useState(1);
const [allPosts, setAllPosts] = useState<Post[]>([]);
const { ref: bottomRef, isIntersecting } = useIntersectionObserver({
threshold: 0.1, // 10%๋ง ๋ณด์ฌ๋ ๊ฐ์ง
rootMargin: '0px 0px 200px', // ๋ฐ๋ฅ 200px ์ ์ ๋ฏธ๋ฆฌ ๊ฐ์ง (๋ถ๋๋ฌ์ด ๋ก๋ฉ)
});
const { data: newPosts, loading } = useAsync(
() => fetchPosts({ page }),
[page]
);
// ์ ๋ฐ์ดํฐ ๋์ฐฉ ์ ๊ธฐ์กด ๋ชฉ๋ก์ ์ถ๊ฐ
useEffect(() => {
if (newPosts) {
setAllPosts(prev => [...prev, ...newPosts]);
}
}, [newPosts]);
// ํ๋จ ์์๊ฐ ํ๋ฉด์ ๋ณด์ด๋ฉด ๋ค์ ํ์ด์ง ๋ก๋
useEffect(() => {
if (isIntersecting && !loading) {
setPage(prev => prev + 1);
}
}, [isIntersecting, loading]);
return (
<div>
{allPosts.map(post => <PostItem key={post.id} post={post} />)}
{loading && <Spinner />}
<div ref={bottomRef} style={{ height: 1 }} /> {/* ๊ฐ์ ๋์ (๋์ ์ ๋ณด์) */}
</div>
);
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"useIntersectionObserver๋ ์คํฌ๋กค ์ด๋ฒคํธ์ ๋์ฒด์์ผ. '์ด ์์๊ฐ ํ๋ฉด์ ๋ณด์ด๋๊ฐ?' ๋ฅผ ์ฑ๋ฅ ์ข๊ฒ ๊ฐ์งํ๊ณ , ๋ฌดํ ์คํฌ๋กคยท๋ ์ด์ง ์ด๋ฏธ์ง ๋ก๋ฉ์ ๋๋ฃจ ์ฐ์ฌ."
๐ ํ ๋ฐํ๊ฐ API ์ค๊ณ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ํ ๋ฐํ๊ฐ์ ๋ฐฐ์ด๋ก ํ ์ง, ๊ฐ์ฒด๋ก ํ ์ง ํ๋จ ๊ธฐ์ค์ ๊ฐ๊ฒ ๋๋ค
๋ฐฐ์ด ๋ฐํ (Tuple):
// ๋ฐฐ์ด ๋ฐํ: useState ์คํ์ผ โ ์ด๋ฆ์ ์์ ๋กญ๊ฒ ๋ฐ๊ฟ ์ ์์
const [theme, setTheme] = useLocalStorage('theme', 'dark');
const [query, setQuery] = useLocalStorage('query', '');
// ์ด๋ฆ ์ถฉ๋ ์์ด ๋ ๋ฒ ์ธ ์ ์๋ค๋ ๊ฒ ๋ฐฐ์ด ๋ฐํ์ ๊ฐ์ ๊ฐ์ฒด ๋ฐํ (Named):
// ๊ฐ์ฒด ๋ฐํ: ์ด๋ฆ์ด ๊ณ ์ ๋จ, ํ์ํ ๊ฒ๋ง ๊ตฌ์กฐ๋ถํด ๊ฐ๋ฅ
const { data, loading, error } = useAsync(fetchPosts);
const { data: user } = useAsync(() => fetchUser(id)); // ์ด๋ฆ ์ถฉ๋ ์ ๋ณ์นญ ์ฌ์ฉ์ธ์ ์ด๋ ์ชฝ์ ์ธ๊น?
| ์ํฉ | ์ ํ | ์ด์ |
|---|---|---|
| ๋ฐํ๊ฐ์ด 2๊ฐ (๊ฐ + setter) | ๋ฐฐ์ด | useState ์ฒ๋ผ ์ด๋ฆ ์์ |
| ๋ฐํ๊ฐ์ด 3๊ฐ ์ด์ | ๊ฐ์ฒด | ์ด๋ฆ ๋ช ์๋ก ๊ฐ๋ ์ฑ ํฅ์ |
| ๊ฐ์ ํ ์ ํ ์ปดํฌ๋ํธ์์ 2๋ฒ ์ฌ์ฉ | ๋ฐฐ์ด | ๋ณ์นญ ์์ด ์์ ๋ก์ด ์ด๋ฆ |
| ๋ฐํ๊ฐ ์ผ๋ถ๋ง ์ ํํด์ ์ธ ๋ | ๊ฐ์ฒด | ๊ตฌ์กฐ๋ถํด๋ก ํ์ํ ๊ฒ๋ง |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"๋ฐํ๊ฐ์ด 2๊ฐ๋ฉด ๋ฐฐ์ด(์ด๋ฆ ์์ ), 3๊ฐ ์ด์์ด๋ฉด ๊ฐ์ฒด(๋ช ์์ฑ). ๊ฐ์ ํ ์ ๋ ๋ฒ ์จ์ผ ํ๋ฉด ๋ฌด์กฐ๊ฑด ๋ฐฐ์ด."
๐ ํ ํฉ์ฑ ํจํด ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์์ ํ ๋ค์ ์กฐํฉํด ๋ ๋ณต์กํ ํ ์ ๋ง๋๋ ํฉ์ฑ ํจํด์ ์ ์ฉํ ์ ์๋ค
ํ ์ ๋ค๋ฅธ ํ ์ ํธ์ถํ ์ ์์ด์. ์ด ํน์ฑ์ ์ด์ฉํด ์์ ํ ๋ค์ ์์ ๋ณต์กํ ๊ธฐ๋ฅ์ ๋ง๋๋ ๊ฒ ํ ํฉ์ฑ(Hook Composition) ์ด์์.
// โ
๋ ์ด์ด๋ณ ํ
ํฉ์ฑ: ๊ฒ์ ๊ธฐ๋ฅ ์ ์ฒด๋ฅผ ํ ํ
์ผ๋ก
function useSearch(initialQuery = '') {
// Layer 1: ๊ฒ์์ด ์ํ (localStorage์ ์๊ตฌ ์ ์ฅ)
const [query, setQuery] = useLocalStorage('last-search', initialQuery);
// Layer 2: ๋๋ฐ์ด์ค ์ ์ฉ (ํ์ดํ ๋ฉ์ถ๊ณ 300ms ํ ์ค์ ๊ฒ์)
const debouncedQuery = useDebounce(query, 300);
// Layer 3: ์ค์ API ํธ์ถ (๋๋ฐ์ด์ค๋ ๊ฐ์ผ๋ก)
const { data: results, loading, error } = useAsync(
() => debouncedQuery ? searchPosts(debouncedQuery) : Promise.resolve([]),
[debouncedQuery]
);
return {
query, // ์ฆ์ ๋ฐ์ํ๋ ์
๋ ฅ๊ฐ (UI ํ์์ฉ)
setQuery, // ๊ฒ์์ด ๋ณ๊ฒฝ
results, // ๊ฒ์ ๊ฒฐ๊ณผ
loading,
error,
};
}
// ์ฌ์ฉ: ๋ณต์กํ ๋ก์ง์ด ํ ์ค๋ก
function SearchPage() {
const { query, setQuery, results, loading } = useSearch();
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{loading ? <Spinner /> : results?.map(p => <PostItem key={p.id} post={p} />)}
</div>
);
}์ด๋ ๊ฒ useLocalStorage โ useDebounce โ useAsync ๋ฅผ ์กฐํฉํ๋ฉด, ๊ฐ ํ
์ ๋
๋ฆฝ์ ์ผ๋ก ํ
์คํธ ๊ฐ๋ฅํ๋ฉด์๋ useSearch ๋ผ๋ ๊ณ ์์ค ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค ์ ์์ด์.
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"ํ ํฉ์ฑ์ ๋ ๊ณ ์กฐ๋ฆฝ์ด์ผ. ์์ ํ (๋ธ๋ก)๋ค์ ๋ ๋ฆฝ์ ์ผ๋ก ๋ง๋ค๊ณ , ํฐ ํ (๊ตฌ์กฐ๋ฌผ)์ ๊ทธ๊ฒ๋ค์ ์์ ๋ง๋ค์ด. ๊ฐ ๋ธ๋ก์ ๋ฐ๋ก ํ ์คํธ ๊ฐ๋ฅํด."
โ๏ธ ํ vs ์ปดํฌ๋ํธ ๊ฒฝ๊ณ์ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- "์ด๊ฑธ ํ ์ผ๋ก ๋ง๋ค๊น, ์ปดํฌ๋ํธ๋ก ๋ง๋ค๊น"๋ฅผ ๋ช ํํ๊ฒ ํ๋จํ ์ ์๋ค
๊ฐ์ฅ ํํ ํผ๋์ "๋ก์ง ์ฌ์ฌ์ฉ = ํ , UI ์ฌ์ฌ์ฉ = ์ปดํฌ๋ํธ" ๋ผ๋ ์์น์ ๋ฌด์ํ ๋ ๋ฐ์ํด์.
// โ ์๋ชป๋ ์: ํ
์ด JSX๋ฅผ ๋ฐํํจ
function useLoadingState(isLoading: boolean) {
if (isLoading) {
return <div className="spinner">Loading...</div>; // ํ
์ด JSX ๋ฐํ โ ๊ท์น ์๋ฐ!
}
return null;
}
// โ
์ฌ๋ฐ๋ฅธ ๋ถ๋ฆฌ
// 1) ๋ก์ง(์ํ)์ ํ
์ผ๋ก
function useLoadingState() {
const [isLoading, setIsLoading] = useState(false);
return { isLoading, setIsLoading };
}
// 2) UI๋ ์ปดํฌ๋ํธ๋ก
function LoadingSpinner({ visible }: { visible: boolean }) {
if (!visible) return null;
return <div className="spinner">Loading...</div>;
}
// 3) ์ปดํฌ๋ํธ์์ ์กฐํฉ
function MyPage() {
const { isLoading } = useLoadingState();
return <LoadingSpinner visible={isLoading} />;
}"Render Props vs Custom Hook" ๊ฒฝ๊ณ:
// ๐ก Render Prop ํจํด (๊ตฌํ, ํ
์ด์ ์๋)
<DataLoader render={data => <PostList data={data} />} />
// ๐ก ์ปค์คํ
ํ
ํจํด (ํ๋์ , ๋ ๋จ์)
const { data } = useData();
return <PostList data={data} />;
// โ ๋์ผํ ๋ก์ง ์ฌ์ฌ์ฉ์ด์ง๋ง ํ
์ด ํจ์ฌ ์ฝ๊ธฐ ์ฌ์๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
"ํ ์ ๋ก์ง(How), ์ปดํฌ๋ํธ๋ UI(What). ํ ์ดreturn <div>๋ฅผ ํ๋ค๋ฉด ๊ทธ๊ฑด ์ปดํฌ๋ํธ์ผ."
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F ๋ก ๋ฉ์์ง ์ผ๋ถ๋ฅผ ๊ฒ์ํด๋ด. ๋๋ถ๋ถ ์ฌ๊ธฐ ์์ด.
โ React Hook "useXxx" is called conditionally
์ธ์ ๋์ค๋๊ฐ?
function MyComponent({ isLoggedIn }) {
if (!isLoggedIn) return null; // โ ์ฌ๊ธฐ์ early return
const { data } = useUserData(); // ๐ฅ ์กฐ๊ฑด๋ฌธ ์ดํ์ ํ
ํธ์ถ โ ๊ท์น ์๋ฐ
}์์ธ: ํ ์ ํญ์ ๊ฐ์ ์์๋ก ํธ์ถ๋์ด์ผ ํด์. ์กฐ๊ฑด๋ฌธ/๋ฐ๋ณต๋ฌธ ์์ ํ ์ด ๋ค์ด๊ฐ๋ฉด ์ ๋ผ์.
ํด๊ฒฐ์ฑ :
// โ
ํ
์ ์กฐ๊ฑด๋ฌธ ์ด์ ์ ํธ์ถ, ์กฐ๊ฑด ์ฒ๋ฆฌ๋ ๊ทธ ํ์
function MyComponent({ isLoggedIn }) {
const { data } = useUserData(); // ํญ์ ํธ์ถ
if (!isLoggedIn) return null; // ๊ทธ ๋ค์์ ์กฐ๊ฑด ์ฒ๋ฆฌ
return <div>{data?.name}</div>;
}โ Warning: Can't perform a React state update on an unmounted component
์ธ์ ๋์ค๋๊ฐ?
useEffect(() => {
fetchData().then(data => {
setState(data); // ๐ฅ ์ด๋ฏธ ์ธ๋ง์ดํธ๋๋๋ฐ setState ํธ์ถ!
});
}, []);์์ธ: ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ๋ ํ ๋น๋๊ธฐ ์ฝ๋ฐฑ์ด ์๋ฃ๋์ด setState ๋ฅผ ํธ์ถํ ๋ ๋ฐ์ํด์.
ํด๊ฒฐ์ฑ
: useAsync ํ
์ cancelled ํ๋๊ทธ ํจํด์ ์ฌ์ฉํด์:
useEffect(() => {
let cancelled = false;
fetchData().then(data => {
if (!cancelled) setState(data); // ์ธ๋ง์ดํธ ํ๋ฉด ๊ฑด๋๋
});
return () => { cancelled = true; };
}, []);โ useLocalStorage ์์ SSR ์๋ฌ: window is not defined
์ธ์ ๋์ค๋๊ฐ?
ReferenceError: window is not defined
์์ธ: Next.js ๊ฐ์ SSR ํ๊ฒฝ์์๋ ์๋ฒ์์ window ๊ฐ์ฒด๊ฐ ์์ด์.
ํด๊ฒฐ์ฑ :
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue; // SSR ํ๊ฒฝ ์ฒดํฌ
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
์ค๋ ๋ฐฐ์ด ํต์ฌ์ ํ๋์ ์ ๋ฆฌํด๋ณผ๊น? ์ค๋ฌด์์ ๊ธธ์ ์์์ ๋ ์ด๊ฒ๋ง ๋ด๋ ๋ผ.
๐ ํ ์ถ์ถ ํ๋จ ๊ธฐ์ค
| ์ํฉ | ์ ํ |
|---|---|
useState + useEffect ์กฐํฉ์ด 2๊ณณ ์ด์ ๋ณต๋ถ๋จ | โ ์ปค์คํ ํ |
| ๊ฐ์ JSX ๊ตฌ์กฐ๊ฐ 2๊ณณ ์ด์ ๋ณต๋ถ๋จ | โ ์ปดํฌ๋ํธ |
| ํ ์๋ ์์ ํจ์ (๊ณ์ฐ, ๋ณํ) | โ ์ ํธ ํจ์ |
| JSX๋ฅผ ๋ฐํํ๊ณ ์ถ์ ํ | โ ํ ์๋, ์ปดํฌ๋ํธ๋ก |
๐ ์ค๋ฌด ํ 4์ข ์์ฝ
| ํ | ์ญํ | ๋ฐํ |
|---|---|---|
useAsync(fn, deps) | ๋น๋๊ธฐ ๋ฐ์ดํฐ ํจ์นญ | { data, loading, error } |
useLocalStorage(key, init) | ์์์ฑ ์๋ ์ํ | [value, setter] (๋ฐฐ์ด) |
useDebounce(value, delay) | ๋น ๋ฅธ ๊ฐ ๋ณํ๋ฅผ ์ง์ฐ | debouncedValue |
useIntersectionObserver() | DOM ๊ฐ์์ฑ ๊ฐ์ง | { ref, isIntersecting } |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| โ ๋์ ์ | โ ์ข์ ์ | ์ด์ |
|---|---|---|
| ํ ์์์ JSX ๋ฐํ | ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌ | ํ ๊ท์น ์๋ฐ |
| ์กฐ๊ฑด๋ฌธ ์์์ ํ ํธ์ถ | ํ ๋จผ์ ํธ์ถ ํ ์กฐ๊ฑด ์ฒ๋ฆฌ | ํ ์์ ๊ท์น ์๋ฐ |
| ํด๋ฆฐ์ ์๋ ๋น๋๊ธฐ ํ | cancelled ํ๋๊ทธ ์ฌ์ฉ | ๋ฉ๋ชจ๋ฆฌ ๋์/๋ฒ๊ทธ |
| ๊ฒ์ผ๋ฅธ ์ด๊ธฐํ ์์ด localStorage ์ฝ๊ธฐ | useState(() => ...) ์ฌ์ฉ | ๋งค ๋ ๋๋ง๋ค IO ๋ฐ์ |
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
๊ฒ์๊ธ, ์ ์ , ๋๊ธ ๋ชฉ๋ก ์ธ ๊ตฐ๋ฐ์ ์ ๋ถ setLoading(false)๊ฐ ๊ผฌ์ฌ์ ์ผ๊ทผํ๋ ๋ ์ด ๋ ์ค๋ฅธ๋ค. ํ
์ ๊ทธ๋ฅ '๋ณต๋ถ ์์ ๊ธฐ' ์ฉ๋์ธ ์ค ์์๋๋ฐ, ๋ก์ง ์์ฒด๋ฅผ ์บก์ ์์์ ์์ ํ๊ฒ ๋ณดํธํด ์ฃผ๋ ๊ฑฐ์๋ค.
๐ก "๋ก์ง(How)์ ์บก์์ ๋ด์ผ๋ฉด ์ปค์คํ ํ ์ด๊ณ , UI(What)๋ฅผ ์บก์์ ๋ด์ผ๋ฉด ์ปดํฌ๋ํธ๋ค. ํ ์ด ํ๊ทธ(JSX)๋ฅผ ๋ฑ์ด๋ด๋ ์๊ฐ ๊ท์น ์๋ฐ!"
useAsync ํ๋์ ์บ์ฌ ํ๋๊ทธ(ํด๋ฆฐ์
) ์ธํ
์ ํด ๋์ผ๋ ๋ ์ด์ ๊ฒฝ์ ์กฐ๊ฑด(Race Condition)์ ์ ๊ฒฝ ์ธ ํ์๊ฐ ์์ด์ง ๊ฒ ์ ์ผ ์์ํ๋ค. ์กฐ๋ฆฝ ๋งค๋ด์ผ ๋น์ ๊ฐ ๋ฑ์ด๋ค. ๋ด์ผ ๋น์ฅ ํ์ฌ ์ฝ๋์ ๋๋ ค ์๋ ๋๋ฌ์ด useEffect + fetch ํธ๋ฆฌ๋ค์ ๋ชจ์กฐ๋ฆฌ ํ
ํ๋๋ก ๋นจ์๋ค์ฌ ๋ฒ๋ฆฌ๊ฒ ๋ค.
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์๋ ์ค ์ปค์คํ ํ ์ผ๋ก ์ถ์ถํด์ผ ํ๋ ๊ฒ์?
- A)
<UserCard>์ปดํฌ๋ํธ์์ ์ฐ์ด๋ ์ฌ์ฉ์ ์ด๋ฆ ํ์ JSX - B) 3๊ฐ ์ปดํฌ๋ํธ์์ ๋์ผํ๊ฒ ๋ฐ๋ณต๋๋
useState + useEffect + fetchํจํด - C) ๋ ์ง ํ์์ ๋ณํํ๋
formatDate(date)ํจ์ - D) ๋ฒํผ ํด๋ฆญ ์ API๋ฅผ ํธ์ถํ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ ํจ์
โ ์ ๋ต: B
- A: JSX โ ์ปดํฌ๋ํธ๋ก ์ถ์ถ
- B:
useState + useEffect๋ฐ๋ณต ํจํด โ ์ปค์คํ ํ ์ผ๋ก ์ถ์ถ โ- C: ํ ์๋ ์์ ํจ์ โ ์ ํธ ํจ์
- D: ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ํ ์์ผ๋ฉด ์ผ๋ฐ ํจ์๋ก ์ถฉ๋ถ
๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "Hook์ด ๋ค์ด๊ฐ๋ ๋ก์ง ์ฌ์ฌ์ฉ = ํ . JSX ์ฌ์ฌ์ฉ = ์ปดํฌ๋ํธ. Hook ์๋ ๋ก์ง = ์ ํธ ํจ์."
Q2. ์๋ ๋น์นธ์ ์ฑ์๋ณด์.
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = ________(() => { // 1๋ฒ ๋น์นธ
setDebouncedValue(value);
}, delay);
return () => __________(timer); // 2๋ฒ ๋น์นธ (ํด๋ฆฐ์
)
}, [value, delay]);
return debouncedValue;
}โ ์ ๋ต: 1๋ฒ:
setTimeout, 2๋ฒ:clearTimeoutํด์ค:
setTimeout์ผ๋ก ์ง์ฐ ์คํ์ ์์ฝํ๊ณ , ํด๋ฆฐ์ ์์clearTimeout์ผ๋ก ์ด์ ํ์ด๋จธ๋ฅผ ์ทจ์ํด์. ์ value ๊ฐ ๋ค์ด์ฌ ๋๋ง๋ค ์ด์ ํ์ด๋จธ๊ฐ ์ทจ์๋์ด ๊ฒฐ๊ณผ์ ์ผ๋ก "๋ง์ง๋ง ๊ฐ๋ง" ์ฒ๋ฆฌ๋ผ์.๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "๋๋ฐ์ด์ค = setTimeout + clearTimeout. ํ์ด๋จธ๋ฅผ ๊ณ์ ๋ฆฌ์ ํด์ ๋ง์ง๋ง ๊ฐ๋ง ์ด์๋จ๊ฒ ํ๋ค."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
์ปค์คํ ํ ์ด ์ "์ปดํฌ๋ํธ ๋ถ๋ฆฌ"์ ๋ค๋ฅธ์ง, ์์๋ฅผ ๋ค์ด ํ ๋ฌธ๋จ์ผ๋ก ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"์ปดํฌ๋ํธ ๋ถ๋ฆฌ๋ ํ๋ฉด์ ์ผ๋ถ(UI)๋ฅผ ๋ฐ๋ก ๋ผ์ด๋ด๋ ๊ฑฐ์ผ. ์ปค์คํ ํ ์ ํ๋ฉด์๋ ๋ณด์ด์ง ์๋ '๋์ ๋ฐฉ์(๋ก์ง)'์ ๋ฐ๋ก ๋ผ์ด๋ด๋ ๊ฑฐ์ผ. ์๋ฅผ ๋ค์ด ๊ฒ์์ฐฝ UI๋ฅผ ์ฌ์ฌ์ฉํ๊ณ ์ถ์ผ๋ฉด
<SearchInput>์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ๊ณ , ๊ฒ์ API ํธ์ถ ๋ก์ง์ ์ฌ์ฌ์ฉํ๊ณ ์ถ์ผ๋ฉดuseSearch()ํ ์ผ๋ก ๋ถ๋ฆฌํด. ํ ์ JSX๋ฅผ ๋ฐํํ์ง ์๊ณ ๋ฐ์ดํฐ๋ ํจ์๋ฅผ ๋ฐํํ๋ค๋ ๊ฒ ํต์ฌ ์ฐจ์ด์ผ."
๐ ๋ ์์๋ณด๊ธฐ
- React ๊ณต์ ๋ฌธ์ โ ์ปค์คํ ํ ์ผ๋ก ๋ก์ง ์ฌ์ฌ์ฉํ๊ธฐ
- useHooks.com โ ์ค๋ฌด ์ปค์คํ ํ ์ปฌ๋ ์
- TanStack Query โ
useAsync์ ํ๋ก๋์ ๊ธ ์์ฑํ - React Query ํจํด ๊ฐ์ด๋ โ 19๋ฒ ๋ฌธ์