프로그래밍/ReactJS

React 커스텀 훅: 로직 재사용의 마법을 경험하다!

shimdh 2025. 10. 19. 01:02
728x90

React 개발자라면 누구나 공감할 텐데요, 코드의 재사용성과 유지보수성은 프로젝트를 성공으로 이끄는 핵심 요소입니다. 특히 대규모 애플리케이션에서 동일한 로직이 여러 컴포넌트에 반복적으로 등장할 때, 코드가 점점 복잡해지고 디버깅이 악몽이 되곤 하죠. 이런 문제를 해결해주는 React의 숨겨진 보석이 바로 커스텀 훅(Custom Hooks) 입니다. 커스텀 훅은 단순한 함수가 아니라, 로직을 추상화하고 공유할 수 있는 강력한 도구로, 개발 생산성을 폭발적으로 높여줍니다. 이 글에서는 커스텀 훅의 기본 개념부터 실제 예제까지 단계적으로 탐구하며, 왜 이것이 "마법"처럼 느껴지는지 함께 느껴보겠습니다.

커스텀 훅이란 무엇인가요?

커스텀 훅은 이름에서 알 수 있듯이, "use"로 시작하는 JavaScript 함수 입니다. 이 함수 내부에서는 React의 내장 훅(예: useState, useEffect, useContext 등)을 자유롭게 호출할 수 있어요. 본질적으로 커스텀 훅은 상태 관리와 사이드 이펙트를 캡슐화 하는 역할을 하며, 컴포넌트의 로직을 UI 렌더링과 분리합니다.

예를 들어, 복잡한 데이터 페칭 로직이나 폼 유효성 검사 같은 공통 작업을 훅으로 추출하면, 메인 컴포넌트는 깔끔하게 유지됩니다. 결과적으로 개발자는 불필요한 boilerplate 코드에 매몰되지 않고, UI 디자인과 사용자 경험에 더 집중할 수 있게 되죠. 게다가 코드 가독성이 높아져 팀 협업 시도 훨씬 수월해집니다. React Hooks 규칙(함수 컴포넌트나 다른 훅 안에서만 호출 가능)을 준수하기만 하면, 누구나 쉽게 만들 수 있어요!

728x90

왜 커스텀 훅을 사용해야 할까요?

커스텀 훅의 매력은 단순히 "편리함"이 아닙니다. 실제 프로젝트에서 얻는 실질적인 이점은 다음과 같아요. 아래에서 세 가지 주요 이유를 자세히 살펴보겠습니다.

1. 재사용성: 코드 중복은 이제 그만!

코드 중복은 개발자의 최대 적입니다. 커스텀 훅은 DRY(Don't Repeat Yourself) 원칙 을 완벽히 구현해줘요. 예를 들어, API 호출, 사용자 인증, 또는 무한 스크롤 같은 로직을 한 번만 작성하면 여러 컴포넌트에서 재사용할 수 있습니다. 이로 인해 코드 베이스가 슬림해지고, 버그 수정 시 한 곳만 고치면 모든 곳에 반영되죠. 결과? 개발 속도가 2배 이상 빨라집니다!

2. 관심사의 분리: 깔끔하고 독립적인 코드

UI 컴포넌트와 비즈니스 로직을 섞지 않도록 관심사 분리( Separation of Concerns) 를 촉진합니다. 컴포넌트는 "무엇을 보여줄까?"에만 집중하고, "데이터를 어떻게 처리할까?"는 훅이 맡아요. 이 덕분에 컴포넌트가 더 읽기 쉽고, 유지보수가 용이해집니다. 디버깅도 간단해지며, 단위 테스트가 훨씬 직관적입니다. 만약 로직이 변경되더라도 UI는 건드릴 필요가 없어요!

3. 캡슐화: 모듈성 향상과 쉬운 이해

관련 로직을 하나의 훅으로 묶음으로써 모듈성 을 높입니다. 복잡한 코드를 작은, 재사용 가능한 단위로 분해하면 코드의 응집도가 올라가고, 컴포넌트 간 결합도는 낮아집니다. 새로운 개발자가 프로젝트에 합류할 때, "이 기능은 이 훅에서 처리돼요"라고 설명하기만 하면 금방 이해할 수 있어요. 장기적으로 아키텍처의 안정성을 강화하는 데 큰 역할을 합니다.

이 외에도 커스텀 훅은 TypeScript와 잘 어울려 타입 안전성을 더해주고, React의 함수형 패러다임을 강화합니다. 이제 이론은 그만! 실제로 만들어 보죠.

커스텀 훅 만들기: useFetch 예제

API에서 데이터를 가져오는 흔한 작업을 예로 들어, 간단한 useFetch 커스텀 훅을 구현해 보겠습니다. 이 훅은 URL을 입력받아 데이터를 페칭하고, 로딩/에러 상태를 관리합니다. (React 18+ 기준으로 작성되었어요.)

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`Network response was not ok: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]); // URL 변경 시 재실행

  return { data, loading, error };
};

export default useFetch;

이 훅의 핵심 동작

  • 인자: url – 페칭할 API 엔드포인트.
  • 상태 관리: data(데이터), loading(로딩 플래그), error(에러 메시지).
  • 사이드 이펙트: useEffect로 URL 변경 시 자동 페칭. try-catch-finally로 오류 처리.
  • 반환값: 컴포넌트에서 바로 사용할 수 있는 객체.

이 훅 하나로 데이터 페칭의 모든 골칫거리를 해결했어요. 이제 이를 사용하는 컴포넌트를 보죠.

커스텀 훅 사용하기: DataDisplayComponent

훅을 실제 컴포넌트에서 써보는 건 어떨까요? 아래는 useFetch를 활용한 간단한 데이터 표시 컴포넌트입니다. (훅 파일은 hooks/useFetch.js에 저장했다고 가정해요.)

import React from 'react';
import useFetch from './hooks/useFetch'; // 훅 임포트

const DataDisplayComponent = ({ apiUrl }) => {
  const { data, loading, error } = useFetch(apiUrl); // 훅 호출!

  if (loading) return <p>로딩 중...</p>;
  if (error) return <p>오류 발생: {error}</p>;

  return (
    <div>
      <h1>가져온 데이터:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default DataDisplayComponent;

사용 포인트

  • 간결함: 한 줄로 페칭 로직 전체를 처리. 컴포넌트는 상태에만 반응할 뿐입니다.
  • 조건부 렌더링: loadingerror로 UI를 동적으로 변경. 사용자 경험(UX)이 부드러워집니다.
  • 확장성: apiUrl props로 유연하게 재사용 가능.

여러 컴포넌트와 로직 공유하기

커스텀 훅의 진가는 여러 곳에서 쓰일 때 빛납니다. 사용자 목록과 제품 목록을 가져오는 두 컴포넌트를 예로 들어보죠. 둘 다 useFetch를 공유하지만, URL만 다릅니다.

// 사용자 목록 컴포넌트
const UserListComponent = () => {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <p>사용자 목록 로딩 중...</p>;
  if (error) return <p>오류: {error}</p>;

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

// 제품 목록 컴포넌트
const ProductListComponent = () => {
  const { data: products, loading, error } = useFetch('/api/products');

  if (loading) return <p>제품 목록 로딩 중...</p>;
  if (error) return <p>오류: {error}</p>;

  return (
    <div>
      {products?.map(product => (
        <div key={product.id}>
          {product.name} - {product.price}원
        </div>
      ))}
    </div>
  );
};

이처럼 하나의 훅으로 여러 컴포넌트가 로직을 공유하면, 코드가 얼마나 효율적일까요? 만약 캐싱이나 재시도 로직을 추가하고 싶다면, 훅 파일만 수정하면 모든 컴포넌트에 적용됩니다!

커스텀 훅의 고급 팁과 마무르기

커스텀 훅을 더 강력하게 만들기 위한 팁 몇 가지를 공유할게요:

  • 의존성 배열 주의: useEffect의 배열을 잘 관리해 불필요한 재렌더링을 피하세요.
  • TypeScript 통합: 제네릭을 사용해 타입 안전성을 더하세요. (예: useFetch<T>(url: string): { data: T | null; ... })
  • 조합 훅: useFetch + useLocalStorage처럼 여러 훅을 결합해 복잡한 기능을 만드세요.
  • 테스트: react-hooks-testing-library로 훅을 독립적으로 테스트하세요.
728x90