Skip to content
This page has been auto-translated and may contain errors.View in English

Hooks

컴포넌트가 드롭다운 열림 상태를 기억해야 하거나, 렌더링 후 서버와 동기화하기 위해 코드를 실행해야 할 때가 있습니다. 둘 다 Hook의 역할입니다. 여러분은 이미 두 개를 사용했습니다: useState는 컴포넌트에 메모리를 주고, useEffect는 렌더링 후 코드를 실행해서 React 밖의 세상과 연결합니다. 이 둘이 가장 많이 쓰이는 Hook이고, 나머지 built-in Hook들도 같은 패턴을 따릅니다. 컴포넌트 내에서 함수를 호출하면 React의 특정 기능에 연결되는 것입니다. 그 함수의 이름은 항상 use로 시작하는데, 이 접두사 자체가 Hook의 정의입니다.

자주 쓰이는 built-in Hook들

처음부터 모든 Hook을 알아야 하는 건 아니지만, 필요할 때 올바른 도구를 인식할 수 있도록 어떤 Hook들이 있는지 아는 것이 도움됩니다. 자주 나오는 것들입니다:

  • **useRef**는 렌더링을 일으키지 않으면서도 렌더링을 거쳐 남아있는 가변 값을 보관합니다. 또한 input에 포커스를 맞추는 것처럼 DOM 엘리먼트에 직접 접근해야 할 때도 이 Hook을 사용합니다.
  • **useContext**는 상위 provider로부터 공유된 데이터를 읽어서, 깊이 중첩된 컴포넌트가 모든 단계마다 prop을 전달받지 않고도 값에 접근할 수 있게 합니다.
  • **useReducer**는 reducer 함수를 통해 state를 관리하는데, 각 업데이트는 현재 state에서 다음 state로의 전환을 나타냅니다. state 변화가 복잡해져서 단순한 useState 하나로는 부족할 때 좋습니다.
  • **useMemo**는 계산된 값을 렌더링 사이에 캐시했다가, 입력값이 바뀔 때만 다시 계산합니다.
  • **useCallback**은 함수에도 같은 작업을 합니다. 매번 새로운 함수 인스턴스를 만드는 대신 렌더링 사이에 같은 함수를 돌려줍니다.

useMemouseCallback은 실제 성능 이야기가 있을 때 더 자세히 다룹니다. Refs와 DOM에서 useRef를 자세히 봅니다. useContextuseReducer는 이 장에서 가장 깊이 있게 다루니, 필요할 때 여기로 돌아오세요. 지금은 패턴만 기억하세요: Hook은 특정 React 기능으로 들어가는 통로이고, 이름을 보면 Hook임을 알 수 있습니다.

Hook의 규칙

Hook에는 두 가지 규칙이 있으며, React는 이를 따를 것을 기대합니다.

첫째, Hook을 컴포넌트의 최상위 레벨에서만 호출하세요. 조건문, 반복문, 중첩 함수 안에서 호출하면 안 됩니다. 컴포넌트가 사용하는 모든 Hook은 매 렌더링마다, 같은 순서로, 매번 실행되어야 합니다.

jsx
function Profile({ isLoggedIn }) {
  // 틀렸음: 이 Hook은 때때로만 실행됨
  if (isLoggedIn) {
    const [name, setName] = useState('')
  }

  // 맞음: 최상위 레벨에서 조건 없이 호출
  const [name, setName] = useState('')
  // ...그 다음 로직 안에서 isLoggedIn을 사용
}

둘째, Hook은 React 함수 컴포넌트나 다른 custom hook에서만 호출하세요. 일반 JavaScript 함수는 Hook을 사용할 수 없습니다. React가 state를 붙일 컴포넌트 렌더링이 없기 때문입니다.

자신만의 Hook 만들기

여러 컴포넌트가 브라우저 창의 너비를 알아야 한다고 가정해봅시다. 같은 useStateuseEffect 코드를 각 컴포넌트에 복사하는 대신, 한 번만 패키징할 수 있습니다:

jsx
import { useState, useEffect } from 'react'

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return width
}

이제 어떤 컴포넌트든 const width = useWindowWidth()를 호출하면 창이 리사이즈될 때 업데이트되는 값을 얻습니다. useWindowWidth는 custom hook입니다: 다른 Hook들을 호출하는 함수 말입니다. 그게 전부인 패턴이고, 함수이기 때문에 상태를 가진 로직에 이름을 붙이고, 한 곳에 두고, 여러 컴포넌트에서 재사용할 수 있습니다. use 접두사는 React의 도구와 코드를 읽는 다음 사람이 이것이 Hook임을 알 수 있게 합니다. Custom Hook은 이미 있는 Hook들을 묶어서 재사용 가능하게 만드는 것일 뿐, React에 새로운 능력을 더하지는 않습니다.

그 목록에서 무엇을 선택할지 몇 가지 팁입니다. 타이머 ID나 이전 prop처럼 변해도 리렌더를 유발하면 안 되는 값을 보관하거나, DOM 노드에 직접 접근해야 할 때는 useRef를 선택하세요. 현재 테마나 로그인 사용자처럼 여러 레이어의 중첩 컴포넌트에 도달해야 하는 값이 있고, 모든 단계마다 prop을 통과시키면 관심 없는 컴포넌트까지 건드려야 할 때는 useContext를 선택하세요. 단일 state가 함께 업데이트되는 여러 부분 값을 가지거나, 다음 state가 단순한 새 값이 아니라 특정 action에 의존할 때는 useReducer를 선택하세요. reducer가 그 전환 로직을 한 함수에 유지하기 때문입니다. 실제 성능 저하를 발견했을 때만 useMemouseCallback을 선택하세요. 보통 비용이 많이 드는 계산이 매 렌더링마다 다시 실행되거나, 함수 identity가 다른 곳의 memoization을 깨뜨릴 때(예: React.memo된 자식이나 다른 Hook의 dependency array)입니다.

Custom hook을 이름 짓고 추출하는 것도 자신의 패턴을 따릅니다. 이름은 항상 use로 시작해서 React의 도구와 코드를 읽는 사람이 다른 Hook을 호출해도 된다는 걸 알 수 있게 합니다. 같은 useStateuseEffect 조합이나 다른 Hook들이 한 개 이상의 컴포넌트에서 반복되는 걸 발견하면, 그게 추출의 신호입니다: 로직을 자신의 use 접두사 함수로 뽑아내고, 컴포넌트가 실제로 필요한 것(값, setter, 둘 다)을 반환하고, 각 컴포넌트에서 배선을 반복하는 대신 그 함수를 호출하세요.

규칙들은 자의적이지 않습니다. React는 state 변수의 이름을 모르고, Hook이 호출되는 순서로 컴포넌트의 Hook들을 추적합니다. 첫 번째 Hook, 두 번째 Hook, 세 번째 Hook 식으로요. 다음 렌더링에서 같은 목록을 같은 순서로 순회하고 각 호출을 지난번에 설정한 state로 매칭합니다. 매 렌더링마다 순서를 동일하게 유지하면 매칭이 유지됩니다. Hook을 조건문 뒤에 놓으면 그 조건이 바뀌는 순간 목록이 변해서 React가 잘못된 state를 잘못된 호출에 맞추고 컴포넌트가 이상한 방식으로 망가집니다. 최상위 레벨에서 조건 없이 호출하는 것이 안정적인 순서를 보장합니다.

useMemouseCallback은 별도의 주목이 필요합니다. 이들은 수동 최적화입니다: 계산된 값이나 함수를 보관해서 매 렌더링마다 작업을 다시 하거나 새로운 인스턴스를 만들지 않도록 React에 말하는 것입니다. 복잡성이 더해지고, 실제로 도움이 되지 않는 곳에도 손을 뻗고 싶은 유혹이 있습니다. React Compiler는 빌드 타임에 이런 것들을 자동으로 memoize해서 점점 불필요하게 만들고 있으므로, 직접 사용하는 것을 반사 작용이 아니라 실제 비용을 측정해서 의도적으로 하는 것으로 다루세요.

JunoHook은 컴포넌트가 React 기능을 사용하게 함 Hook은 이름이 use로 시작하는 함수이고, 컴포넌트를 메모리나 effects 같은 React 기능에 연결합니다. 이미 useStateuseEffect를 만났고, 나머지도 같은 모양입니다. 기억할 핵심은: Hook을 컴포넌트의 맨 위에서만 호출하고, if나 반복문 안에서는 절대 호출하지 않으면, 매 렌더링마다 같은 방식으로 실행됩니다.
JunoHook은 컴포넌트가 React 기능을 사용하게 함useStateuseEffect 외에도 렌더링을 거쳐 남는 값을 위한 useRef, prop drilling을 건너뛰기 위한 useContext, 구조화된 state 전환을 위한 useReducer, 캐싱을 위한 useMemo/useCallback이 있습니다. 컴포넌트나 다른 Hook의 최상위 레벨에서 호출하고, 항상 같은 순서로 호출하세요. 상태를 가진 로직을 반복하는 자신을 발견하면, 다른 Hook들을 호출하는 use 함수인 custom hook으로 뽑아내고 재사용하세요.
JunoHook은 컴포넌트가 React 기능을 사용하게 함 React는 이름이 아니라 호출 순서로 Hook을 state에 매칭하므로, 매 렌더링마다 순서가 동일해야 합니다. 이 한 가지 사실에서 두 규칙이 모두 나옵니다. Custom Hook은 조합입니다: Hook을 호출하는 함수이므로, 같은 순서 규칙이 내부에도 적용됩니다. 그리고 useMemo/useCallback을 측정되고 의도적인 최적화로 다루세요. React Compiler가 꾸준히 손으로 작성한 버전들을 불필요하게 만들고 있으니까요.

다음: React로 생각하기. 컴포넌트, prop, state, effect가 디자인을 UI로 바꾸는 과정으로 어떻게 함께 작동하는지 봅니다.