STUDY/[ React ]

Hook이란? + 커스텀 Hook을 만들어보자

Lim임 2025. 11. 27. 00:42

리액트를 사용하게 되는 큰 의미 중 하나인 React Hook.

도대체 뭐길래 꼭 사용해야하는지, 어떻게 쓰는지 알아보도록 하겠다

Hook이란?

리액트가 컴포넌트를 다시 실행해도 기억하는 기능을 가진 특별한 함수

 

리액트의 컴포넌트는 사실 함수라서 실행하면 변수들이 다 사라져야 해요.

useState같은 Hook은 그걸 기억하게 해준다!

 

 

일반함수

let count = 0
function MyComponent() {
  count = count + 1
  console.log(count)
}
// => 일반 함수는 매 실행마다 다시 시작해야 하기 때문에 UI 상태 기억 불가

 

리액트 Hook

const [count, setCount] = useState(0)
// 리렌더될 때도 count가 초기화되지 않고 남아있다!

 

자주쓰는 React Hook 정리

 

자주쓰는 React Hook 정리

React에서 가장 많이 쓰이는 Hook & 특징 정리useState — 상태 관리의 시작점React에서 가장 기본이 되는 Hook.컴포넌트가 가지고 있어야 하는 “변하는 값”을 저장.렌더링에 영향을 줌.setState가 호출

tin814.tistory.com

 

간단한 Hook의 예시

useCounter Hook

import { useState } from 'react';

export const useCounter = (initialValue: number = 0) => {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(prev => prev + 1); // +1하기
  const decrement = () => setCount(prev => prev - 1); // -1하기
  const reset = () => setCount(initialValue); // initialValue로 초기화
  
  return { count, increment, decrement, reset };
};

 

사용법:

function MyComponent() {
  const { count, increment, decrement, reset } = useCounter(10);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button> // increment
      <button onClick={decrement}>-</button> // decrement
      <button onClick={reset}>Reset</button> // reset
    </div>
  );
}
// 다양한 로직들을 사용할 수 있도록 로직을 hook으로 묶음

 

useState+ useEffect를 사용한 CustomHook 생성 ; useLocalStorage

import { useState, useEffect } from 'react';

export const useLocalStorage = <T,>(key: string, initialValue: T) => {
  // localStorage에서 초기값 가져오기
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });
  
  // 값이 변경될 때마다 localStorage 업데이트
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  
  return [value, setValue] as const;
};

 

사용법:

function MyComponent() {
  const [name, setName] = useLocalStorage('userName', '');
  
  return (
    <input 
      value={name} 
      onChange={(e) => setName(e.target.value)} 
    />
  );
}

커스텀 훅 만드는 단계별 가이드

Step 1: 파일 생성

# hooks 폴더에 생성
src/hooks/useCounter.ts

 

Step 2: 간단한 예시 - useCounter

import { useState } from 'react';

export const useCounter = (initialValue: number = 0) => {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
};

 

사용법:

function MyComponent() {
  const { count, increment, decrement, reset } = useCounter(10);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

커스텀 훅 작성 규칙

해야 할 것:

- 항상 "use"로 시작하는 이름 사용
- 다른 훅들을 조합하여 사용 가능
- 재사용 가능한 로직 추출
- 필요한 값과 함수만 반환

 

하지 말아야 할 것:

- 조건문 안에서 훅 호출
- 반복문 안에서 훅 호출
- 일반 함수에서 훅 호출

 


왜 커스텀 훅을 조건문, 반복문, 일반 함수 안에서 훅을 호출하면 안되는 걸까?

결론

React는 컴포넌트가 렌더링될 때 Hook이 ‘항상 같은 순서로 호출된다’는 것을 전제로 내부 구조를 짜놨다.

그리고 만약

  • 조건문 안에서 호출 → 실행될 때도 있고 안 될 때도 있음
  • 반복문 안에서 호출 → 실행 횟수가 달라짐
  • 일반 함수 안에서 호출 → 호출 시점이 일정하지 않음

이렇게 되면 Hook이 “순서 기반”으로 움직이는 React 내부 시스템이 완전히 꼬여버림.


React가 Hook을 어떻게 관리하길래?

React는 컴포넌트를 렌더링할 때
각 Hook의 값을 순서대로 배열에 저장해두고 사용

 

예시로 컴포넌트가 있다고 해보자:

function Component() {
  const a = useState(1);     // Hook #1
  const b = useState(2);     // Hook #2
  const c = useEffect(() => {}, []);  // Hook #3
}

 

React 내부에서는 대략 이렇게 저장함:

 
Hook index 0 -> useState(1)
Hook index 1 -> useState(2)
Hook index 2 -> useEffect(...)

 

그리고 다음 렌더링 때도 같은 순서라고 가정하고 재사용함.


문제 상황 발생 예시

조건문 안에서 훅 호출하면?

 
if (props.visible) {
  const [a, setA] = useState(0);  // ❌ 조건문 안
}
const [b, setB] = useState(0);

 

첫 번째 렌더

visible = true
Hook #1: a
Hook #2: b

 

두 번째 렌더

visible = false
Hook #1: b ← ⚠ 순서가 달라짐!

 

React는 “Hook #1은 useState가 되어야 한다”,
근데 이번 렌더링에서는 첫 번째 useState가 호출되지 않았음.

=>그래서 상태가 완전히 뒤바뀌거나, 에러가 터짐.


반복문 안에서 훅 호출

 
for (let i = 0; i < items.length; i++) {
  const [value, setValue] = useState(0); // ❌ 반복문
}

첫 번째 렌더

items.length = 3 → Hook 3개 생성

두 번째 렌더

items.length = 1 → Hook 1개만 생성

 

➡ Hook 개수 자체가 달라짐 → React 내부 인덱스 꼬임
➡ 상태, memoized 값, effect 순서가 전부 잘못 매칭되어 버림


일반 함수 안에서 Hook 호출

 
function doSomething() {
  const [a, setA] = useState(0);  // ❌ 함수를 호출할 때마다 실행됨
}

function Component() {
  doSomething();
  const [b, setB] = useState(0);
}

문제:

  • doSomething()이 언제 호출될지 React는 모름
  • 렌더링마다 호출될 수도, 안 될 수도 있음
  • React의 “Hook 순서 체계”가 보장되지 않음

➡ 결국 React 내부 Hook 배열 인덱스가 예측 불가능해짐.


그래서 나온 규칙:

Hooks must be called unconditionally at the top level

이 규칙을 지키면 React는 다음을 보장할 수 있다:

  • Hook의 순서가 항상 같음
  • 상태가 다른 Hook과 섞이지 않음
  • 메모이제이션이 정확한 Hook에 연결됨
  • cleanup이 올바른 effect와 매칭됨

커스텀 훅을 만드는 핵심 규칙

커스텀 훅은 Hook을 감싸서 로직을 재사용하는 도구.
그래서 커스텀 훅도:

컴포넌트 최상단에서만 호출 가능
조건/반복/일반 함수 내부에서는 호출 불가


❗ 커스텀 훅 내부에서는 자유롭게 Hook 사용 가능
→ 단, 그 커스텀 훅 자체는 “항상 동일한 위치에서 호출”되어야 함.


쉽게 비유하면…

React는 Hook을 "줄 서서 기다리는 사람들"로 관리함.

  • 첫 번째 사람: useState(카운터)
  • 두 번째 사람: useState(토글)
  • 세 번째 사람: useEffect(데이터 fetch)

근데 어떤 렌더링에서 갑자기 첫 번째 사람이 없거나,
2명만 있다가 다음 렌더링은 5명이 오면?

“얘가 아까 그 사람인가..?”
“useState였나 useEffect였나…?”

React는 혼란에 빠짐.

그래서 "줄(호출 순서)을 항상 일정하게 유지"해야 함.