React 독학 가이드 11 - Hooks 완벽 가이드

learning by Seven Fingers Studio 20분
ReactHooksuseStateuseEffectuseMemouseCallback

React 16.8에서 Hooks가 도입되면서 함수형 컴포넌트가 완전히 달라졌어요. 클래스 없이도 상태 관리, 생명주기, 최적화까지 다 할 수 있게 됐죠. 이번 글에서 주요 Hooks를 전부 정리해볼게요.

Hooks의 규칙

먼저 Hooks 사용 규칙을 알아야 해요. 안 지키면 에러 나요!

규칙 1: 최상위에서만 호출

// ❌ 조건문 안에서 사용 금지
if (condition) {
  const [state, setState] = useState(0);
}

// ❌ 반복문 안에서 사용 금지
for (let i = 0; i < 3; i++) {
  const [state, setState] = useState(0);
}

// ⭕ 컴포넌트 최상위에서 호출
function Component() {
  const [state, setState] = useState(0);  // OK
  // ...
}

규칙 2: React 함수 안에서만 호출

// ❌ 일반 함수에서 사용 금지
function normalFunction() {
  const [state, setState] = useState(0);
}

// ⭕ React 컴포넌트나 커스텀 Hook에서만
function Component() {
  const [state, setState] = useState(0);  // OK
}

function useCustomHook() {
  const [state, setState] = useState(0);  // OK
}

1. useState - 상태 관리

가장 기본이 되는 Hook이에요.

import { useState } from 'react';

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

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
      <button onClick={() => setCount(prev => prev - 1)}>감소</button>
    </div>
  );
}

실행 결과:

카운트: 5

useState로 상태를 관리하여 버튼 클릭 시 화면이 업데이트됩니다

다양한 타입

const [text, setText] = useState("");            // 문자열
const [num, setNum] = useState(0);               // 숫자
const [isOpen, setIsOpen] = useState(false);     // 불린
const [list, setList] = useState([]);            // 배열
const [user, setUser] = useState({ name: "" });  // 객체

지연 초기화

초기값 계산이 무거우면 함수로 전달하세요:

// ❌ 매 렌더링마다 실행됨
const [data, setData] = useState(expensiveCalculation());

// ⭕ 첫 렌더링에만 실행됨
const [data, setData] = useState(() => expensiveCalculation());

2. useEffect - 부수 효과 처리

컴포넌트 생명주기와 관련된 작업을 해요.

import { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, []);  // 빈 배열 = 마운트 시 1번만 실행

  if (loading) return <p>로딩 중...</p>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

의존성 배열

// 마운트 + 매 렌더링마다 실행
useEffect(() => {
  console.log('렌더링됨');
});

// 마운트 시 1번만 실행
useEffect(() => {
  console.log('마운트됨');
}, []);

// 마운트 + count가 바뀔 때마다 실행
useEffect(() => {
  console.log('count:', count);
}, [count]);

// 마운트 + a 또는 b가 바뀔 때 실행
useEffect(() => {
  console.log('a 또는 b 변경');
}, [a, b]);

클린업 함수

컴포넌트가 언마운트되거나 effect가 재실행되기 전에 정리 작업:

useEffect(() => {
  const timer = setInterval(() => {
    console.log('틱');
  }, 1000);  // 클린업: 언마운트 시 또는 다음 effect 전에 실행
  return () => {
    clearInterval(timer);
  };
}, []);

이벤트 리스너 정리

useEffect(() => {
  const handleResize = () => {
    console.log(window.innerWidth);
  };

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

3. useRef - DOM 접근 & 값 저장

렌더링 없이 값을 저장하거나 DOM에 접근해요.

import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();  // 자동 포커스
  }, []);

  return <input ref={inputRef} />;
}

렌더링 없이 값 저장

function Timer() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current += 1;
    console.log(countRef.current);  // 화면은 안 바뀜
  };

  return <button onClick={handleClick}>클릭</button>;
}

4. useMemo - 값 메모이제이션

비싼 계산 결과를 캐싱해요.

import { useMemo, useState } from 'react';

function ExpensiveComponent({ items }) {
  const [filter, setFilter] = useState('');

  // items가 바뀔 때만 재계산
  const sortedItems = useMemo(() => {
    console.log('정렬 중...');  // 이게 자주 찍히면 안 됨
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
  }, [items]);

  const filteredItems = sortedItems.filter(item =>
    item.name.includes(filter)
  );

  return (
    <div>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="검색"
      />
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

언제 useMemo를 쓸까?

  • 복잡한 계산 (정렬, 필터, 집계 등)
  • 렌더링마다 새 객체/배열을 만들어서 자식에게 전달할 때
  • 최적화가 필요할 때 (먼저 측정하고 쓰세요!)

5. useCallback - 함수 메모이제이션

함수를 캐싱해요. 자식 컴포넌트에 함수를 전달할 때 유용해요.

import { useCallback, useState, memo } from 'react';

// memo로 감싼 자식 컴포넌트
const Button = memo(({ onClick, children }) => {
  console.log('Button 렌더링:', children);
  return <button onClick={onClick}>{children}</button>;
});

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

  // count가 바뀔 때만 새 함수 생성
  const handleIncrease = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />

      <p>Count: {count}</p>
      <Button onClick={handleIncrease}>증가</Button>
    </div>
  );
}

useCallback이 없으면 text가 바뀔 때마다 Button도 다시 렌더링돼요.

6. useContext - 전역 상태 공유

Props 드릴링 없이 데이터를 공유해요.

import { createContext, useContext, useState } from 'react';

// 1. Context 생성
const ThemeContext = createContext();

// 2. Provider 컴포넌트
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. 사용하는 컴포넌트
function ThemeButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff'
      }}
    >
      현재 테마: {theme}
    </button>
  );
}

// 4. App에서 사용
function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>테마 예제</h1>
        <ThemeButton />
      </div>
    </ThemeProvider>
  );
}

7. useReducer - 복잡한 상태 관리

useState보다 복잡한 상태 로직에 사용해요.

import { useReducer } from 'react';

// 리듀서 함수
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    case 'SET':
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>리셋</button>
      <button onClick={() => dispatch({ type: 'SET', payload: 100 })}>
        100으로 설정
      </button>
    </div>
  );
}

실행 결과:

Count: 42

useReducer로 복잡한 상태 로직을 action 기반으로 관리합니다

실전 예제: Todo 앱

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'TOGGLE':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'DELETE':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    dispatch({ type: 'ADD', text });
    setText('');
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input value={text} onChange={e => setText(e.target.value)} />
        <button type="submit">추가</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}
              style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>
              삭제
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

8. 커스텀 Hook 만들기

반복되는 로직을 재사용 가능한 Hook으로 만들어요.

useLocalStorage

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// 사용
function App() {
  const [name, setName] = useLocalStorage('name', '');
  return <input value={name} onChange={e => setName(e.target.value)} />;
}

useWindowSize

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;
}

// 사용
function App() {
  const { width, height } = useWindowSize();
  return <p>화면 크기: {width} x {height}</p>;
}

useFetch

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// 사용
function UserList() {
  const { data, loading, error } = useFetch('/api/users');

  if (loading) return <p>로딩 중...</p>;
  if (error) return <p>에러 발생!</p>;
  return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

Hooks 요약 표

Hook용도언제 쓰나?
useState상태 관리값이 바뀌면 화면 업데이트 필요할 때
useEffect부수 효과API 호출, 이벤트 리스너, 타이머
useRefDOM 접근, 값 저장렌더링 없이 값 유지할 때
useMemo값 캐싱비싼 계산 최적화
useCallback함수 캐싱자식에게 함수 전달 최적화
useContext전역 상태Props 드릴링 없이 데이터 공유
useReducer복잡한 상태상태 로직이 복잡할 때

마무리

이 시리즈로 React의 기초를 모두 다뤘어요!

  1. React 소개
  2. 프로젝트 생성
  3. 컴포넌트
  4. JSX
  5. Props
  6. 이벤트 처리
  7. State
  8. State와 Props
  9. 사용자 입력
  10. useRef
  11. Hooks 총정리

여기까지 따라왔으면 React로 간단한 앱은 만들 수 있을 거예요. 다음 단계로는 React Router, 상태 관리 라이브러리(Redux, Zustand), 서버 통신(React Query) 등을 공부해보세요.

실제로 뭔가 만들어보는 게 가장 빨리 배우는 방법이에요. 할 일 앱, 날씨 앱, 블로그 등 작은 프로젝트부터 시작해보세요!


React 시리즈 탐색:

← 블로그 목록으로