React Custom Hook은 어떻게 작동할까?

2025. 8. 10. 18:27·Frontend/Tech Insight
728x90
SMALL

왜 이 글을 작성하게 되었나?

평소 React 개발하며 Hook을 코드 분리와 책임 분리 등을 위해 자연스럽게 사용해 왔습니다. Hook이라는 기능이 생긴 후로 정말 편리하게 활용해왔는데요, 최근 면접에서 "useABC 같은 Custom Hook이 어떻게 동작하는 건지, 자바스크립트의 어떤 원리로 가능한 건지" 질문을 받았을 때 제대로 답변하지 못했습니다. 😂
사용하기만 해봤지 내부 동작 원리는 깊이 생각해보지 못했던 것 같습니다. 그래서 이번 기회에 어떤 원리로 동작하는지 정리해보겠습니다.

Hook이란?

Custom Hook의 동작 원리를 이해하기 전에, 먼저 Hook이 무엇인지 간단히 살펴보겠습니다. React Hook은 2019년 React 16.8에서 도입된 기능으로, 함수형 컴포넌트에서도 상태와 생명주기를 다룰 수 있게 해주는 특별한 함수들입니다.
가장 기본적인 Hook인 useState를 예로 들면:

function Counter() {
  const [count, setCount] = useState(0); // 상태를 "기억"하는 코드

  return (
    <button onClick={() => setCount(count + 1)}>
      클릭 횟수: {count}
    </button>
  );
}

여기서 특이한 점은 Counter 함수가 여러 번 호출되어도 count 값이 사라지지 않는다는 것입니다!
자세한 설명을 아래 코드 예제로 비교해보겠습니다:

// 일반 JavaScript 함수
function normalFunction() {
  let number = 0; // 함수가 실행될 때마다 0으로 초기화
  console.log(number); // 항상 0이 출력됨
  number = 5;
  // 함수 실행이 끝나면 number 변수는 사라짐
}

normalFunction(); // 0 출력
normalFunction(); // 0 출력 (이전 실행의 number=5는 사라짐)
normalFunction(); // 0 출력

// 하지만 React의 `useState는 다릅니다
function Counter() {
  const [count, setCount] = useState(0); // 매번 호출되지만...
  console.log(count); // 0, 1, 2, 3... 이전 값을 기억하고 있음!

  return <button onClick={() => setCount(count + 1)}>클릭</button>;
}

위 코드를 봤을 때 이런 질문들이 생길 수 있습니다:

  • useState(0)을 매번 호출하는데, 왜 count는 0으로 초기화되지 않고 이전 값을 기억할까요?
  • Counter 함수가 끝날 때마다 모든 지역 변수가 사라져야 하는데, 어떻게 count 값이 어딘가에 저장되어 있다가 다음번에 다시 불러와질까요?

위 코드처럼 React에서 어떤 원리로 이전 상태를 기억할 수 있게 해주는지, Custom Hook에서는 어떻게 활용되는지를 알아보겠습니다.

Custom Hook은 어떻게 동작하는가?


빠밤! Custom Hook은 "use"로 시작하는 이름의 일반적인 JavaScript 함수였습니다...!
Custom hook 예제:

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

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}

Custom Hook의 원리

Custom Hook이 동작하는 핵심 원리는 클로저(Closure) 라는 JavaScript의 개념입니다. 면접 질문으로도 자주 등장하는 개념인데요, 클로저는 함수가 생성될 때의 환경을 기억하고, 나중에 그 환경에 접근할 수 있게 해주는 특성입니다.
간단한 예시로 클로저의 원리를 이해해보겠습니다:

function createPersonalVault() {
  let secret = "비밀번호123"; // 이 변수는 함수가 끝나도 사라지지 않습니다
  let accessCount = 0;

  return {
    getSecret: function() {
      accessCount++; // 클로저를 통해 외부 변수에 접근
      console.log(`${accessCount}번째 접근`);
      return secret;
    },
    changeSecret: function(newSecret) {
      secret = newSecret; // 외부 변수를 변경
    }
  };
}

const myVault = createPersonalVault();
console.log(myVault.getSecret()); // "비밀번호123" (1번째 접근)
myVault.changeSecret("새로운비밀번호");
console.log(myVault.getSecret()); // "새로운비밀번호" (2번째 접근)

위 코드에서 createPersonalVault 함수가 실행이 끝났음에도 불구하고, 반환된 객체의 메서드들은 여전히 secret과 accessCount 변수에 접근할 수 있습니다. 이게 클로저의 기능으로 가능한 동작입니다

Custom Hook에서 클로저가 작동하는 방식

Custom Hook도 마찬가지 원리로 동작합니다. useCounter를 다시 살펴보면:

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue); // React가 관리하는 상태

  // 이 함수들은 클로저를 통해 count와 setCount에 접근합니다
  const increment = () => setCount(count + 1);     // 클로저: count 값에 접근
  const decrement = () => setCount(count - 1);     // 클로저: count 값에 접근
  const reset = () => setCount(initialValue);      // 클로저: initialValue에 접근

  return { count, increment, decrement, reset };
}

여기서 increment, decrement, reset 함수들은 useCounter 함수가 실행을 마친 후에도 여전히 그 함수 내부의 변수들(count, setCount, initialValue)에 접근할 수 있습니다. 이것이 클로저 덕분에 가능한 동작입니다.

Custom Hook은 독립적인 인스턴스를 생성합니다

Custom Hook의 특성 중 하나는 호출될 때마다 완전히 독립적인 인스턴스가 생성된다는 점입니다.

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

  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);

  return { count, increment, decrement };
}

// 두 개의 독립적인 카운터를 만들어보겠습니다
function TwoCounters() {
  const counter1 = useCounter(0);   // 첫 번째 독립적인 인스턴스
  const counter2 = useCounter(10);  // 두 번째 독립적인 인스턴스

  return (
    <div>
      <div>
        카운터1: {counter1.count}
        <button onClick={counter1.increment}>+</button>
        <button onClick={counter1.decrement}>-</button>
      </div>
      <div>
        카운터2: {counter2.count}
        <button onClick={counter2.increment}>+</button>
        <button onClick={counter2.decrement}>-</button>
      </div>
    </div>
  );
}

위 코드에서 useCounter를 두 번 호출했지만, 각각은 완전히 독립적으로 동작합니다. counter1의 버튼을 클릭해도 counter2의 값은 전혀 영향받지 않습니다. 왜 그럴까요?
그 이유는 각각의 useCounter 호출이 자신만의 useState를 가지기 때문입니다. React는 각 Hook 호출을 순서에 따라 구분하여 관리합니다. (React에서 Hook이 어떻게 동작하는지에 관해서는 나중에..)

Custom Hook시 주의점과 규칙

Hook을 반복문으로 렌더링된 컴포넌트에서 사용할 때의 주의점

이 부분이 실제 개발에서 발생할 수 있는 문제입니다. 다음과 같은 상황을 생각해보겠습니다:

function useItemManager() {
 const [items, setItems] = useState(
 [
  { id: 1, data: 'a' },
  { id: 2, data: 'b' },
  { id: 3, data: 'c' }
 ]
 );
 ...
 return { items }
}


function DangerousComponentItem() {
  // 각 아이템마다 독립적인 Hook 인스턴스를 가집니다
  const { removeItem } = useItemManager([item]);

  return (
    <div>
      <span>{item.name}</span>
      <button onClick={() => removeItem(item.id)}>
        이 아이템 삭제
      </button>
      <button onClick={() => onRemove(item.id)}>
        전체에서 삭제
      </button>
    </div>
  );
}

function DangerousComponent() {
  const { items } = useItemManager();
  
  return (
    <div>
      {items.map((item, index) => (
        <DangerousComponentItem key={index} item={item} />
      ))}
    </div>
  );
}

이 코드의 문제점을 단계별로 살펴보겠습니다:

1단계: 초기 상태
아이템 배열:[
  { id: 1, data: 'a' },
  { id: 2, data: 'b' },
  { id: 3, data: 'c' }
 ]
각 컴포넌트의 Hook 상태:
- index 0 (id: 1): useItemManager()
- index 1 (id: 2): useItemManager()  
- index 2 (id: 3): useItemManager()
2단계: 2번째 배열에서 3번을 삭제, 3번에서 2번을 삭제
아이템 배열: [A, C]
React가 보는 상황:
- index 1 (id: 2): [{id: 1}, {id: 2}]
- index 2 (id: 3): [{id: 1}, {id: 3}]

결과: 의도한 결과로는 id: 1만 남아야 하지만, 각 인스턴스에서 상태를 공유하지 않아서 DangerousComponent에는 3개 배열이 계속 보이게 됩니다.

올바른 사용 방법

// 방법 1: 부모 컴포넌트에서 상태 관리 후 props로 전달
function App() {
  const { data, loading, error } = useApiData('/api/users');
  const userId = data?.userId;

  return (
    <div>
      <UserProfile userId={userId} data={data} loading={loading} />  
      <UserSettings userId={userId} data={data} loading={loading} /> 
    </div>
  );
}

// 방법 2: Context API 활용
const UserContext = createContext();

function UserProvider({ children }) {
  const userData = useApiData('/api/users');

  return (
    <UserContext.Provider value={userData}>
      {children}
    </UserContext.Provider>
  );
}

function UserProfile() {
  const { data, loading, error } = useContext(UserContext);

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

  return <div>{data?.name}</div>;
}

여러 컴포넌트 간 상태를 동기화하려면, 생성된 커스텀 훅의 인스턴스를 prop으로 넘겨주거나 Context API를 사용해 같은 인스턴스를 바라보도록 해야 합니다.

Custom Hook을 효과적으로 활용하기

  1. 관련된 상태와 로직을 하나로 묶기
    Custom Hook의 가장 큰 장점은 서로 관련된 상태와 함수를 하나로 묶어서 재사용할 수 있다는 것입니다.
// 모달 관련 로직을 하나의 Hook으로 묶기
function useModal() {
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => setIsOpen(true);
  const closeModal = () => setIsOpen(false);
  const toggleModal = () => setIsOpen(prev => !prev);

  // ESC 키로 모달 닫기 기능도 포함
  useEffect(() => {
    const handleEscape = (e) => {
      if (e.key === 'Escape') {
        closeModal();
      }
    };

    if (isOpen) {
      document.addEventListener('keydown', handleEscape);
      return () => document.removeEventListener('keydown', handleEscape);
    }
  }, [isOpen]);

  return {
    isOpen,
    openModal,
    closeModal,
    toggleModal
  };
}

이렇게 모달과 관련된 모든 상태와 로직을 하나의 Hook으로 묶으면, 어떤 컴포넌트에서든 쉽게 재사용할 수 있습니다.

  1. 복잡한 상태 로직 캡슐화하기
    복잡한 상태 관리 로직을 Custom Hook으로 캡슐화하면, 컴포넌트는 UI 렌더링에만 집중할 수 있습니다.
// 폼 검증 로직을 Custom Hook으로 분리
function useFormValidation(initialValues, validationRules) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const setValue = (name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));

    // 값이 변경되면 해당 필드의 에러를 즉시 검증
    if (validationRules[name]) {
      const error = validationRules[name](value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };

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

  const isValid = Object.values(errors).every(error => !error);

  return {
    values,
    errors,
    touched,
    setValue,
    setTouchedField,
    isValid
  };
}

이런 식으로 복잡한 폼 검증 로직을 Hook으로 분리하면, 컴포넌트에서는 다음과 같이 간단하게 사용할 수 있습니다:

function LoginForm() {
  const { values, errors, touched, setValue, setTouchedField, isValid } = 
    useFormValidation(
      { email: '', password: '' },
      {
        email: (value) => !value.includes('@') ? '유효한 이메일을 입력하세요' : null,
        password: (value) => value.length < 6 ? '비밀번호는 6자 이상이어야 합니다' : null
      }
    );

  return (
    <form>
      <input 
        type="email"
        value={values.email}
        onChange={(e) => setValue('email', e.target.value)}
        onBlur={() => setTouchedField('email')}
      />
      {touched.email && errors.email && (
        <span style={{color: 'red'}}>{errors.email}</span>
      )}

      <input 
        type="password"
        value={values.password}
        onChange={(e) => setValue('password', e.target.value)}
        onBlur={() => setTouchedField('password')}
      />
      {touched.password && errors.password && (
        <span style={{color: 'red'}}>{errors.password}</span>
      )}

      <button type="submit" disabled={!isValid}>로그인</button>
    </form>
  );
}
저작자표시 (새창열림)

'Frontend > Tech Insight' 카테고리의 다른 글

Feature-based vs Layer-based 어떤 구조를 사용해야 할까?  (0) 2025.12.14
React에서 다국어(i18n) 처리하기 (With ICU format)  (2) 2025.06.08
Tailwind에서 조건부 클래스와 충돌 방지를 동시에 해결하기  (0) 2025.05.20
React에서 DOMPurify로 XSS 공격 방어하기  (0) 2025.04.24
Bun: 빠르고 혁신적인 JavaScript 런타임Bun이란?  (0) 2025.02.16
'Frontend/Tech Insight' 카테고리의 다른 글
  • Feature-based vs Layer-based 어떤 구조를 사용해야 할까?
  • React에서 다국어(i18n) 처리하기 (With ICU format)
  • Tailwind에서 조건부 클래스와 충돌 방지를 동시에 해결하기
  • React에서 DOMPurify로 XSS 공격 방어하기
끄적끄적 개발자
끄적끄적 개발자
생각나는 대로 끄적끄적, 코드 노트
  • 끄적끄적 개발자
    코딩을 끄적끄적
    끄적끄적 개발자
  • 전체
    오늘
    어제
    • 분류 전체보기 (47)
      • Frontend (43)
        • Tech Insight (24)
        • Dev Practice (7)
        • React Hook Form (4)
        • Module Federation (3)
        • Clone coding (5)
      • UIUX (2)
      • ETC (2)
  • 인기 글

  • 태그

    개발/정보
    개발/코드품질
    개발/기술
    회고
    개발/언어
    UI/UX
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
끄적끄적 개발자
React Custom Hook은 어떻게 작동할까?
상단으로

티스토리툴바