C
React/Hooks/Lesson 12

커스텀 Hooks

30분·theory
이 챕터
4/5
JavaScript

커스텀 Hooks

💡 왜 배워야 할까요?

🎯 같은 로직을 여러 컴포넌트에서 반복 작성하지 않고 재사용할 수 있습니다.
💼 복잡한 컴포넌트를 간단하게 만들어 코드를 읽기 쉽게 합니다.
팀 프로젝트에서 개발 속도를 크게 높이는 고급 기술입니다.
🏢 실무에서는
실무에서는 '사용자 정보 가져오기', '폼 유효성 검증', '로컬스토리지 관리' 같은 반복되는 로직을 커스텀 Hook으로 만들어 여러 페이지에서 재사용합니다. 대규모 팀 프로젝트에서는 공통 Hook 라이브러리를 만들어 모두가 함께 사용합니다.

개념

커스텀 훅은 컴포넌트에서 상태 로직을 분리하여 재사용 가능하고 테스트 가능한 단위로 만드는 React 19의 핵심 패턴입니다. 현업에서 컴포넌트 복잡도를 줄이고 팀 차원의 생산성을 높이는 필수 기술입니다.

왜 중요한가?

대규모 서비스에서 여러 컴포넌트가 동일한 상태 로직(API 호출, 폼 검증, 로컬스토리지 등)을 반복 구현하면 코드 중복과 버그가 급증합니다. 예를 들어, 로그인 상태 관리 로직이 10개 컴포넌트에 흩어져 있으면 인증 방식 변경 시 10곳을 모두 수정해야 합니다.

핵심 개념

커스텀 훅은 마치 공통 도구함과 같습니다. 여러 방에서 사용하는 드라이버, 렌치 같은 도구를 각 방마다 따로 두지 않고 도구함에 모아두듯이, 여러 컴포넌트에서 쓰이는 상태 로직을 커스텀 훅으로 분리하여 중앙 관리합니다.

핵심 포인트

  • use로 시작하는 함수명으로 React 내장 훅들을 조합하여 생성
  • 상태와 상태 조작 함수를 객체 또는 배열로 반환하여 재사용성 극대화
  • 컴포넌트 로직과 완전히 분리되어 단위 테스트가 매우 용이함
💻 안티패턴 - 컴포넌트 내부에 모든 로직 포함
function UserProfile() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch('/api/user');
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError('사용자 정보를 불러올 수 없습니다');
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, []);

  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러: {error}</div>;
  return <div>{user?.name}</div>;
}
💻 2025 권장 패턴 - 테스트 가능한 커스텀 훅
// hooks/useApi.ts
function useApi<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : '알 수 없는 오류');
    } finally {
      setLoading(false);
    }
  }, [url]);

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

  return { data, loading, error, refetch: fetchData } as const;
}

// 컴포넌트에서 사용
function UserProfile() {
  const { data: user, loading, error } = useApi<User>('/api/user');

  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러: {error}</div>;
  return <div>{user?.name}</div>;
}

// __tests__/useApi.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from '../hooks/useApi';

test('API 호출 성공 시 데이터를 반환한다', async () => {
  global.fetch = jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ id: 1, name: '김개발' })
  });

  const { result } = renderHook(() => useApi('/api/user'));

  await waitFor(() => {
    expect(result.current.data).toEqual({ id: 1, name: '김개발' });
  });
});
💻 실무 패턴 - 폼 관리 커스텀 훅
// hooks/useForm.ts
type ValidationRule<T> = {
  [K in keyof T]?: (value: T[K]) => string | null;
};

function useForm<T extends Record<string, any>>(
  initialValues: T,
  validationRules?: ValidationRule<T>
) {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});

  const setValue = useCallback((name: keyof T, value: any) => {
    setValues(prev => ({ ...prev, [name]: value }));
    
    // 실시간 검증
    if (validationRules?.[name]) {
      const error = validationRules[name](value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  }, [validationRules]);

  const setTouched = useCallback((name: keyof T) => {
    setTouched(prev => ({ ...prev, [name]: true }));
  }, []);

  const isValid = useMemo(() => {
    return Object.values(errors).every(error => !error);
  }, [errors]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    setValue,
    setTouched,
    isValid,
    reset
  } as const;
}

// 사용 예시
function LoginForm() {
  const { values, errors, setValue, isValid } = useForm(
    { email: '', password: '' },
    {
      email: (value) => !value.includes('@') ? '이메일 형식이 올바르지 않습니다' : null,
      password: (value) => value.length < 8 ? '비밀번호는 8자 이상이어야 합니다' : null
    }
  );

  return (
    <form>
      <input 
        value={values.email}
        onChange={(e) => setValue('email', e.target.value)}
      />
      {errors.email && <span>{errors.email}</span>}
      
      <button type="submit" disabled={!isValid}>
        로그인
      </button>
    </form>
  );
}

💡 ⚠️ 흔한 실수

  • 훅 내부에서 조건부로 다른 훅을 호출하여 React 훅 규칙 위반 (항상 동일한 순서로 훅 호출해야 함)
  • 의존성 배열을 잘못 설정하여 무한 리렌더링 발생 (useCallback, useEffect 의존성 관리 실수)
  • 너무 많은 책임을 하나의 커스텀 훅에 몰아넣어 단일 책임 원칙 위반 (useEverything 같은 거대 훅)

💡 🎯 면접 준비

Q: 커스텀 훅과 일반 함수의 차이점은 무엇인가요?
Q: 커스텀 훅을 어떻게 테스트하나요?
Q: 언제 커스텀 훅을 만들어야 한다고 생각하나요?

힌트: 1) 커스텀 훅은 React 내장 훅을 사용할 수 있고 컴포넌트 생명주기에 참여한다 2) renderHook과 act를 이용한 단위 테스트 가능 3) 두 개 이상의 컴포넌트에서 동일한 상태 로직이 반복될 때 추출을 고려한다고 답변

⚛️ React 패턴 — 커스텀 Hooks

커스텀 Hooks을 React에서 어떻게 쓰는지 코드와 함께 단계별로 익혀보세요.
1 📦 1. State 선언
useState로 컴포넌트 내부 상태 만들기
const [count, setCount] = useState(0);
2 👁️ 2. 렌더링
state 값을 JSX에 표시
return <h1>현재 카운트: {count}</h1>;
3 🔄 3. 업데이트
setState 호출 → 리렌더링 트리거
<button onClick={() => setCount(count + 1)}>+1</button>
4 💡 4. 핵심 원칙
state는 immutable — 직접 수정 X, 항상 setter로

🎮 커스텀 Hooks — 단계별 이해

각 단계를 클릭해 내용을 읽고 ✓ 이해했어요 버튼으로 진행률을 확인하세요.
🖥️ 실행 결과 — 렌더링된 React 컴포넌트
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — 결과 먼저 확인 후 코드 편집기에서 직접 수정해볼 수 있습니다.

확인 퀴즈

커스텀 Hook을 만들 때 지켜야 하는 네이밍 규칙은?
💡 커스텀 Hook은 반드시 "use"로 시작해야 해요. 이 규칙 덕분에 React가 Hook 규칙 위반을 eslint-plugin-react-hooks로 자동 감지할 수 있어요.
먼저 읽으면 좋은 개념: SyntheticEvent — 이벤트 타입
다음 추천: useContext
커스텀 Hooks - React