React Hooks 완벽 가이드 - useState부터 커스텀 훅까지

React Hooks는 React 16.8에서 도입된 기능으로, 함수형 컴포넌트에서도 상태 관리와 생명주기 기능을 사용할 수 있게 해줍니다. 이 글에서는 React Hooks의 기본 개념부터 고급 활용법까지 자세히 다루겠습니다.

React Hooks를 사용하는 이유

기존 클래스 컴포넌트에서는 생명주기 메서드가 복잡하고, 관련 로직이 여러 메서드에 분산되는 문제가 있었습니다. Hooks를 사용하면 이러한 문제를 해결할 수 있습니다.

React 컴포넌트 생명주기
클래스 컴포넌트와 Hooks의 생명주기 비교

Hooks의 장점

  1. 간결한 코드: 클래스 문법 없이 상태 관리 가능
  2. 로직 재사용: 커스텀 훅으로 로직 추출 및 공유
  3. 관심사 분리: 관련 코드를 한 곳에 모아 관리
  4. 테스트 용이성: 순수 함수 형태로 테스트하기 쉬움

기본 Hooks

React Hooks 개요
주요 React Hooks와 사용 목적

useState - 상태 관리

useState는 컴포넌트에 상태를 추가하는 가장 기본적인 Hook입니다.

import { useState } from 'react';

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <button onClick={() => setCount(prev => prev - 1)}>
        Decrement
      </button>
    </div>
  );
}
이전 상태를 기반으로 새 상태를 계산할 때는 함수형 업데이트를 사용하세요. setCount(prev => prev + 1) 형태가 더 안전합니다.

객체와 배열 상태 관리

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUser(prev => ({
      ...prev,
      [name]: value
    }));
  };

  return (
    <form>
      <input
        name="name"
        value={user.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="email"
        value={user.email}
        onChange={handleChange}
        placeholder="Email"
      />
    </form>
  );
}

useEffect - 부수 효과 처리

useEffect는 컴포넌트가 렌더링된 후 실행되는 부수 효과를 처리합니다.

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Failed to fetch user:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

useEffect 의존성 배열

useEffect(() => {
}, []);

useEffect(() => {
}, [dep1, dep2]);

useEffect(() => {
});

Cleanup 함수

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return <div>Seconds: {seconds}</div>;
}

useContext - 전역 상태 접근

useContext를 사용하면 Props drilling 없이 데이터를 전달할 수 있습니다.

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

const ThemeContext = createContext();

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

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

function ThemedButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff'
      }}
    >
      Toggle Theme
    </button>
  );
}

상태 흐름 이해하기

React 상태 흐름
React의 단방향 데이터 흐름과 렌더링 과정

성능 최적화 Hooks

useMemo - 값 메모이제이션

계산 비용이 높은 연산의 결과를 캐싱합니다.

import { useMemo, useState } from 'react';

function ExpensiveList({ items, filter }) {
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

useCallback - 함수 메모이제이션

함수의 참조 동일성을 유지하여 불필요한 리렌더링을 방지합니다.

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

const Button = memo(({ onClick, children }) => {
  console.log(`Button "${children}" rendered`);
  return <button onClick={onClick}>{children}</button>;
});

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

  const handleClick = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <p>Count: {count}</p>
      <Button onClick={handleClick}>Increment</Button>
    </div>
  );
}
useMemouseCallback은 성능 최적화를 위한 도구입니다. 모든 곳에 사용하면 오히려 메모리 사용량이 증가할 수 있으니, 실제로 성능 문제가 있는 경우에만 적용하세요.

useReducer - 복잡한 상태 관리

Redux와 유사한 패턴으로 복잡한 상태 로직을 관리합니다.

import { useReducer } from 'react';

const initialState = {
  items: [],
  loading: false,
  error: null
};

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, items: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };
    default:
      return state;
  }
}

function TodoList() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const addTodo = (text) => {
    dispatch({
      type: 'ADD_ITEM',
      payload: { id: Date.now(), text, completed: false }
    });
  };

  return (
    <div>
      {state.loading && <p>Loading...</p>}
      {state.error && <p>Error: {state.error}</p>}
      <ul>
        {state.items.map(item => (
          <li key={item.id}>
            {item.text}
            <button
              onClick={() => dispatch({
                type: 'REMOVE_ITEM',
                payload: item.id
              })}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

커스텀 Hooks

로직을 재사용 가능한 함수로 추출할 수 있습니다.

useFetch 커스텀 훅

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, {
          signal: controller.signal
        });
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const json = await response.json();
        setData(json);
        setError(null);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

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

  return { data, loading, error };
}

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

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

useLocalStorage 커스텀 훅

import { useState, useEffect } from 'react';

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

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

  return [storedValue, setValue];
}

function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);

  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      <input
        type="range"
        min="12"
        max="24"
        value={fontSize}
        onChange={e => setFontSize(Number(e.target.value))}
      />
    </div>
  );
}

Hooks 규칙

React Hooks를 올바르게 사용하기 위한 두 가지 규칙이 있습니다.

  1. 최상위에서만 Hook 호출: 반복문, 조건문, 중첩 함수 내에서 Hook을 호출하지 마세요.

  2. React 함수에서만 Hook 호출: 일반 JavaScript 함수에서 Hook을 호출하지 마세요.

function Component() {
  if (condition) {
    const [state, setState] = useState();
  }

  const [state, setState] = useState();
  if (condition) {
  }
}

마무리

React Hooks는 함수형 컴포넌트에서 강력한 기능을 제공합니다. 핵심 포인트를 정리하면 다음과 같습니다.

  1. useState로 상태 관리, useEffect로 부수 효과 처리
  2. useContext로 전역 상태 접근
  3. useMemo, useCallback으로 성능 최적화
  4. 커스텀 훅으로 로직 재사용
  5. Hooks 규칙을 반드시 준수

다음 글에서는 React의 고급 패턴과 상태 관리 라이브러리 비교에 대해 다루겠습니다.

카테고리: 웹 개발
마지막 수정: 2026년 01월 08일