← 가이드 목록으로 ← Back to guides

React Hooks와 컴포넌트 패턴 완벽 정리 React Hooks and Component Patterns Complete Guide

React 16.8에서 도입된 Hooks는 함수형 컴포넌트에서 상태 관리와 생명주기를 다룰 수 있게 해주었습니다. 현재 React 개발의 표준이 된 Hooks 패턴을 체계적으로 알아봅니다.

Hooks, introduced in React 16.8, allow functional components to handle state management and lifecycle. Let's systematically explore the Hooks patterns that have become the standard in React development.

useState: 상태 관리의 기본 useState: Basics of State Management

컴포넌트 내에서 변하는 값(상태)을 관리합니다. 상태가 변경되면 컴포넌트가 다시 렌더링됩니다.

Manages changing values (state) within a component. When state changes, the component re-renders.

import { useState } from 'react';

function Counter() {
  // [현재값, 설정함수] = useState(초기값)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        증가
      </button>
    </div>
  );
}
import { useState } from 'react';

function Counter() {
  // [currentValue, setter] = useState(initialValue)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}
// 객체 상태 관리 시 스프레드 연산자 사용
const [user, setUser] = useState({ name: '', age: 0 });

// ❌ 잘못된 방법 (원본 객체 직접 수정)
// user.name = 'Kim';

// ✅ 올바른 방법 (새 객체 생성)
setUser({ ...user, name: 'Kim' });

// 이전 상태 기반 업데이트 (함수형 업데이트)
setCount(prev => prev + 1);
// Use spread operator when managing object state
const [user, setUser] = useState({ name: '', age: 0 });

// ❌ Wrong way (directly mutating original object)
// user.name = 'Kim';

// ✅ Correct way (create new object)
setUser({ ...user, name: 'Kim' });

// Update based on previous state (functional update)
setCount(prev => prev + 1);

useEffect: 부수 효과 처리 useEffect: Handling Side Effects

데이터 fetching, 구독, DOM 조작 등 렌더링 외의 작업을 처리합니다. 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount를 대체합니다.

Handles operations outside rendering such as data fetching, subscriptions, and DOM manipulation. Replaces componentDidMount, componentDidUpdate, and componentWillUnmount from class components.

useEffect 실행 시점 When useEffect Runs

  • useEffect(() => {}, []) - 마운트 시 1회 실행
  • useEffect(() => {}, [dep]) - dep 변경 시마다 실행
  • useEffect(() => {}) - 매 렌더링마다 실행 (주의!)
  • useEffect(() => {}, []) - Runs once on mount
  • useEffect(() => {}, [dep]) - Runs when dep changes
  • useEffect(() => {}) - Runs on every render (caution!)
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // 데이터 패칭
    async function fetchUser() {
      setLoading(true);
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      setUser(data);
      setLoading(false);
    }
    fetchUser();
  }, [userId]); // userId가 바뀔 때마다 실행

  if (loading) return <p>Loading...</p>;
  return <p>{user.name}</p>;
}
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // Data fetching
    async function fetchUser() {
      setLoading(true);
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      setUser(data);
      setLoading(false);
    }
    fetchUser();
  }, [userId]); // Runs whenever userId changes

  if (loading) return <p>Loading...</p>;
  return <p>{user.name}</p>;
}
// 클린업 함수: 이벤트 리스너, 구독 등 정리
useEffect(() => {
  const handleResize = () => console.log(window.innerWidth);
  window.addEventListener('resize', handleResize);

  // 클린업 함수 반환
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);
// Cleanup function: cleans up event listeners, subscriptions, etc.
useEffect(() => {
  const handleResize = () => console.log(window.innerWidth);
  window.addEventListener('resize', handleResize);

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

useRef: DOM 접근과 값 유지 useRef: DOM Access and Value Persistence

import { useRef } from 'react';

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

  const focusInput = () => {
    inputRef.current.focus();
  };

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

// 렌더링과 무관한 값 저장 (리렌더링 안 됨)
const countRef = useRef(0);
countRef.current += 1; // 렌더링 유발 안 함
import { useRef } from 'react';

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

  const focusInput = () => {
    inputRef.current.focus();
  };

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

// Store value unrelated to rendering (no re-render)
const countRef = useRef(0);
countRef.current += 1; // Doesn't trigger re-render

useMemo와 useCallback: 성능 최적화 useMemo and useCallback: Performance Optimization

import { useMemo, useCallback } from 'react';

// useMemo: 값의 메모이제이션
const expensiveValue = useMemo(() => {
  return heavyCalculation(data);
}, [data]); // data가 바뀔 때만 재계산

// useCallback: 함수의 메모이제이션
const handleClick = useCallback(() => {
  console.log('Clicked!', count);
}, [count]); // count가 바뀔 때만 함수 재생성
import { useMemo, useCallback } from 'react';

// useMemo: Value memoization
const expensiveValue = useMemo(() => {
  return heavyCalculation(data);
}, [data]); // Recalculates only when data changes

// useCallback: Function memoization
const handleClick = useCallback(() => {
  console.log('Clicked!', count);
}, [count]); // Recreates function only when count changes

⚠️ 최적화 남용 주의 ⚠️ Beware of Over-Optimization

useMemouseCallback은 비용이 있습니다. 모든 곳에 사용하지 말고, 실제 성능 문제가 있는 경우에만 적용하세요.

useMemo and useCallback have costs. Don't use them everywhere—only apply when there are real performance issues.

Custom Hooks: 로직 재사용 Custom Hooks: Logic Reuse

반복되는 상태 로직을 커스텀 훅으로 추출하여 재사용할 수 있습니다. 훅 이름은 반드시 use로 시작해야 합니다.

You can extract repetitive state logic into custom hooks for reuse. Hook names must start with use.

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

// 사용
const [theme, setTheme] = useLocalStorage('theme', 'dark');
// useLocalStorage custom hook
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];
}

// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark');
// useFetch 커스텀 훅
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

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

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

  return { data, loading, error };
}

컴포넌트 패턴 Component Patterns

// 조건부 렌더링
{isLoggedIn && <Dashboard />}
{isLoggedIn ? <Dashboard /> : <Login />}

// 리스트 렌더링
{items.map(item => (
  <Item key={item.id} data={item} />
))}

// Props 전달
<Button
  variant="primary"
  onClick={handleClick}
  disabled={loading}
>
  Submit
</Button>
// Conditional rendering
{isLoggedIn && <Dashboard />}
{isLoggedIn ? <Dashboard /> : <Login />}

// List rendering
{items.map(item => (
  <Item key={item.id} data={item} />
))}

// Props passing
<Button
  variant="primary"
  onClick={handleClick}
  disabled={loading}
>
  Submit
</Button>

💡 왜 이 패턴들이 타자 연습에 포함되었는가? 💡 Why are these patterns included in typing practice?

useState, useEffect, 조건부 렌더링, 리스트 맵핑은 React 개발에서 가장 자주 작성하는 코드입니다. 이 패턴들을 손에 익히면 React 컴포넌트를 더 빠르게 작성할 수 있습니다.

useState, useEffect, conditional rendering, and list mapping are the most frequently written code in React development. Mastering these patterns helps you write React components faster.