← 홈페이지 5강 목록으로
🪝
EPISODE 05
use* · useFetch · useLocalStorage · useDebounce · React.memo · useMemo · useCallback · useRef · v6 · useParams · useNavigate · Outlet · 404 · 전역 상태 · Zustand · slice · devtools

커스텀 훅 · 성능 최적화 · React Router · 상태 관리 라이브러리

use로 시작하는 커스텀 훅의 개념과 ESLint 규칙, 공통 로직 추출의 이점, 그리고 useFetch/useLocalStorage/useDebounce 직접 구현을 다룹니다. 불필요한 리렌더링을 방지하는 React.memo, 비싼 계산을 메모하는 useMemo, 함수 참조를 유지하는 useCallback, 그리고 렌더링 없이 값을 저장하는 useRef를 익힙니다. react-router-dom v6의 핵심(BrowserRouter/Routes/Route/Link), useParams/useNavigate/useLocation 훅, 중첩 라우트(Outlet), 404 처리, 그리고 보호된 라우트 패턴까지. 전역 상태가 필요한 시점, Zustand vs Redux 비교, create로 store 만들기, 선택적 구독으로 성능 최적화, slice 패턴, devtools 미들웨어를 익힙니다.

Reactcustom hookuseFetchmemouseMemouseCallbackReact RouterroutingOutletZustand전역 상태Redux
소요 시간
⏱ 45분 + 45~60분 + 45~60분 + 45~60분
난이도
📊 중급
선수 조건
🎯 react-04
결과물
재사용 가능한 useFetch/useLocalStorage/useDebounce / Profiler로 측정한 병목을 정확히 최적화 / 다중 페이지 + 중첩 레이아웃 + 404를 가진 SPA / Zustand로 만든 cart/user 전역 store

이 강의에서 배우는 것

  • 1커스텀 훅이 use로 시작해야 하는 이유를 안다
  • 2중복 로직을 커스텀 훅으로 추출한다
  • 3useFetch — Promise/AbortController 패턴을 적용한다
  • 4useLocalStorage — JSON 직렬화·복원을 처리한다
  • 5useDebounce — setTimeout/clearTimeout 패턴을 안다
  • 6불필요한 리렌더링이 발생하는 3가지 원인을 안다
  • 7React.memo로 컴포넌트 메모이제이션을 한다
  • 8useMemo로 비싼 계산 결과를 메모한다
  • 9useCallback과 React.memo를 함께 써서 자식 리렌더를 막는다
  • 10useRef로 DOM 접근/이전 값 저장을 한다
  • 11BrowserRouter / Routes / Route 로 라우팅을 설정한다
  • 12Link / NavLink로 페이지 전환을 한다
  • 13useParams / useNavigate / useLocation 훅을 사용한다
  • 14Outlet으로 중첩 레이아웃을 구성한다
  • 15Navigate로 보호된 라우트를 만든다
  • 16전역 상태가 필요한 시점을 판단한다
  • 17Zustand와 Redux의 차이를 안다
  • 18create로 store를 만들고 set으로 상태를 변경한다
  • 19선택자로 필요한 상태만 구독해 리렌더링을 줄인다
  • 20slice 패턴으로 관심사를 분리한다
  • 21devtools 미들웨어로 디버깅한다

커스텀 훅 (Custom Hook)

use로 시작하는 커스텀 훅의 개념과 ESLint 규칙, 공통 로직 추출의 이점, 그리고 useFetch/useLocalStorage/useDebounce 직접 구현을 다룹니다.

1. 커스텀 훅이란

jsx
// ✅ use로 시작
function useWindowSize() { ... }
function useFetch(url) { ... }

// ❌ use로 시작하지 않으면 훅 규칙 검사 X
function getWindowSize() { ... }
  • React는 use로 시작하는 함수에서만 훅 규칙 검사
  • ESLint react-hooks/rules-of-hooks가 정상 동작
  • 다른 개발자가 즉시 훅임을 인식

2. 로직 추출의 이점

추출 전 — 중복

jsx
function ComponentA() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  // ... 같은 패턴 반복
}

추출 후 — 한 줄로 해결

jsx
function ComponentA() {
  const { data, isLoading, error } = useFetch('/api/users');
}

function ComponentB() {
  const { data, isLoading, error } = useFetch('/api/products');
}

3. useFetch

jsx
function useFetch(url) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;
    const controller = new AbortController();
    setIsLoading(true);

    fetch(url, { signal: controller.signal })
      .then(r => { if (!r.ok) throw new Error('실패'); return r.json(); })
      .then(setData)
      .catch(err => { if (err.name !== 'AbortError') setError(err.message); })
      .finally(() => setIsLoading(false));

    return () => controller.abort();
  }, [url]);

  return { data, isLoading, error };
}

4. useLocalStorage

jsx
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue];
}

5. useDebounce

jsx
function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

성능 최적화

불필요한 리렌더링을 방지하는 React.memo, 비싼 계산을 메모하는 useMemo, 함수 참조를 유지하는 useCallback, 그리고 렌더링 없이 값을 저장하는 useRef를 익힙니다.

1. 리렌더링 원인

  1. state가 바뀔 때
  2. props가 바뀔 때
  3. 부모 컴포넌트가 리렌더링될 때 — 자식도 함께
jsx
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+{count}</button>
      <Child />  {/* count와 무관하지만 함께 리렌더링 */}
    </>
  );
}

2. React.memo

jsx
// props가 바뀌지 않으면 리렌더링 건너뜀
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
  console.log('렌더링');
  return <div>{data}</div>;
});
⚠️

모든 컴포넌트에 무조건 적용 X — memo 자체도 비교 비용. 렌더링 비용이 큰 컴포넌트에만.

3. useMemo

jsx
// ❌ 매 렌더링마다 필터링
const filteredProducts = products.filter(p => p.name.includes(filterKeyword));

// ✅ 의존성이 바뀔 때만 재계산
const filteredProducts = useMemo(
  () => products.filter(p => p.name.includes(filterKeyword)),
  [products, filterKeyword]
);

4. useCallback

jsx
function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ❌ 매 렌더링마다 새 함수 → React.memo 무력화
  const handleClick = () => setCount(c => c + 1);

  // ✅ 항상 같은 함수 참조
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <MemoizedButton onClick={handleClick}>{count}</MemoizedButton>
    </>
  );
}

const MemoizedButton = React.memo(function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
});

5. useRef

DOM 접근

jsx
function InputFocus() {
  const inputRef = useRef(null);
  const focusInput = () => inputRef.current.focus();

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>포커스</button>
    </>
  );
}

렌더링 없이 값 저장

jsx
function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(0);

  useEffect(() => {
    prevCountRef.current = count;  // ref 변경은 리렌더링 X
  });

  return <p>현재: {count}, 이전: {prevCountRef.current}</p>;
}

6. React DevTools Profiler

  1. React Developer Tools 확장 설치 (Chrome/Firefox)
  2. DevTools → Profiler 탭
  3. 녹화 시작 → 앱 조작 → 녹화 중지
  4. 어떤 컴포넌트가 얼마나 자주, 오래 렌더링되는지 확인
  5. 병목을 React.memo / useMemo / useCallback으로 최적화
도구대상사용 시점
React.memo컴포넌트렌더링 비용 크고 props가 자주 안 바뀔 때
useMemo계산 결과비싼 계산 반복
useCallback함수memo 컴포넌트에 함수 prop 전달 시
useRefDOM/값렌더링 없이 값 유지
💡

성능 최적화는 문제가 생겼을 때 하세요. 미리 하면 코드만 복잡해집니다.

React Router

react-router-dom v6의 핵심(BrowserRouter/Routes/Route/Link), useParams/useNavigate/useLocation 훅, 중첩 라우트(Outlet), 404 처리, 그리고 보호된 라우트 패턴까지.

1. 설치 & 핵심 컴포넌트

bash
npm install react-router-dom
컴포넌트역할
<BrowserRouter>앱 전체를 감싸는 라우터
<Routes>여러 <Route>를 담는 컨테이너
<Route>URL 패턴 ↔ 컴포넌트
<Link>페이지 이동 (a 태그 대체)
<NavLink>현재 경로 일치 시 active
<Navigate>리다이렉트
<Outlet>중첩 라우트 자식 렌더링 위치

2. 기본 라우팅

jsx
// main.jsx
import { BrowserRouter } from 'react-router-dom';
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

// App.jsx
import { Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/blog" element={<BlogPage />} />
      <Route path="/blog/:id" element={<BlogPostPage />} />  {/* 동적 */}
      <Route path="*" element={<NotFoundPage />} />          {/* 404 */}
    </Routes>
  );
}

3. Link / NavLink

jsx
<Link to="/blog">블로그</Link>

<NavLink
  to="/blog"
  style={({ isActive }) => ({
    color: isActive ? '#3b82f6' : '#374151',
    fontWeight: isActive ? 'bold' : 'normal',
  })}
>
  블로그
</NavLink>

4. useParams

jsx
import { useParams } from 'react-router-dom';

// path="/blog/:id"
function BlogPostPage() {
  const { id } = useParams();  // 항상 문자열
  return <h1>게시물 #{id}</h1>;
}

5. useNavigate

jsx
import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();

  const handleLogin = async () => {
    await login();
    navigate('/dashboard');
    navigate(-1);                                  // 뒤로
    navigate('/login', { replace: true });        // 히스토리 교체
  };
}

6. useLocation

jsx
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();
  // location.pathname: '/blog/42'
  // location.search:   '?page=1'
  // location.hash:     '#section1'
  // location.state:    navigate()로 전달한 데이터
}

7. Outlet — 중첩 라우트

jsx
<Routes>
  <Route path="/blog" element={<BlogLayout />}>
    <Route index element={<BlogListPage />} />
    <Route path=":id" element={<BlogPostPage />} />
  </Route>
</Routes>

function BlogLayout() {
  return (
    <div>
      <nav>블로그 사이드바</nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

8. Navigate — 보호된 라우트

jsx
import { Navigate } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const isLoggedIn = useAuthStore(state => state.isLoggedIn);
  if (!isLoggedIn) return <Navigate to="/login" replace />;
  return children;
}

상태 관리 라이브러리 (Zustand)

전역 상태가 필요한 시점, Zustand vs Redux 비교, create로 store 만들기, 선택적 구독으로 성능 최적화, slice 패턴, devtools 미들웨어를 익힙니다.

1. 전역 상태가 필요한 시점

상황해결책
컴포넌트 내부useState
부모-자식 전달props
2~3단계 이상 props drillinguseContext
여러 페이지/컴포넌트 공유전역 상태 관리
복잡한 비동기 + 공유 상태전역 상태 관리

장바구니, 로그인 사용자 정보, 알림, 언어 설정 등이 전형적.

2. Zustand vs Redux

항목ZustandRedux Toolkit
설치 크기~1KB큰 편
학습 곡선낮음높음
보일러플레이트거의 없음어느 정도
DevTools미들웨어로기본 내장
비동기자유롭게createAsyncThunk
적합 규모소~중대규모 팀

3. Zustand 기본

bash
npm install zustand
jsx
import { create } from 'zustand';

const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

function Counter() {
  const { count, increment, decrement } = useCounterStore();
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

4. 선택적 구독

jsx
// count만 구독 — count가 바뀔 때만 리렌더링
function CountDisplay() {
  const count = useCounterStore((state) => state.count);
  return <p>{count}</p>;
}

// increment만 구독 — 절대 리렌더링 안 됨 (함수는 변하지 않음)
function IncrementButton() {
  const increment = useCounterStore((state) => state.increment);
  return <button onClick={increment}>+</button>;
}

5. slice 패턴

jsx
// store/slices/cartSlice.js
export const createCartSlice = (set, get) => ({
  cart: [],
  addToCart: (item) => set((state) => ({ cart: [...state.cart, item] })),
  removeFromCart: (id) => set((state) => ({ cart: state.cart.filter(i => i.id !== id) })),
  getTotal: () => get().cart.reduce((sum, item) => sum + item.price, 0),
});

// store/useAppStore.js
import { create } from 'zustand';
import { createCartSlice } from './slices/cartSlice';
import { createUserSlice } from './slices/userSlice';

const useAppStore = create((...args) => ({
  ...createCartSlice(...args),
  ...createUserSlice(...args),
}));

6. devtools 미들웨어

jsx
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((s) => ({ count: s.count + 1 }), false, 'increment'),
    }),
    { name: 'MyStore' }
  )
);

Redux DevTools 확장과 연동돼 상태 변화를 시각적으로 확인.

예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침