React Hooks 심화 과정

Custom Hooks, useCallback, useMemo 등 고급 Hooks 마스터

React중급
6시간
4개 항목
학습 진행률0 / 4 (0%)

학습 항목

1. React
React|Hooks|기초|완벽|가이드
퀴즈튜토리얼
2. React
React|Custom|Hooks|완벽|가이드
퀴즈튜토리얼
3. React
React|Hooks|완벽|가이드
퀴즈튜토리얼
4. React
React|Hooks|완벽|가이드|초급자용
퀴즈튜토리얼
1 / 4

이미지 로딩 중...

React Hooks 기초 완벽 가이드 - 슬라이드 1/9

React Hooks 완벽 가이드

React Hooks는 함수형 컴포넌트에서 상태 관리와 생명주기 기능을 사용할 수 있게 해주는 강력한 기능입니다. 이 가이드에서는 실무에서 자주 사용되는 Hooks들을 예제와 함께 상세히 알아봅니다.


목차

  1. useState - 상태 관리의 기본
  2. useEffect - 사이드 이펙트 처리
  3. useContext - 전역 상태 공유
  4. useRef - DOM 접근과 값 보관
  5. useMemo - 연산 결과 메모이제이션
  6. useCallback - 함수 메모이제이션
  7. useReducer - 복잡한 상태 로직 관리
  8. Custom Hooks - 로직 재사용

1. useState - 상태 관리의 기본

시작하며

여러분이 사용자의 입력을 받아 화면에 표시하거나, 버튼 클릭에 따라 UI를 변경해야 할 때 어떻게 하시나요? 예를 들어 좋아요 버튼을 누르면 하트 아이콘이 채워지고, 다시 누르면 비워지는 기능을 만든다고 생각해보세요.

이런 문제는 웹 개발에서 가장 기본적이면서도 중요한 과제입니다. 사용자와 상호작용하는 모든 UI는 상태를 관리해야 하고, 상태가 변경되면 화면도 자동으로 업데이트되어야 합니다.

바로 이럴 때 필요한 것이 useState입니다. useState를 사용하면 함수형 컴포넌트에서도 간단하게 상태를 관리하고, 상태가 변경될 때마다 자동으로 컴포넌트를 다시 렌더링할 수 있습니다.

개요

간단히 말해서, useState는 함수형 컴포넌트 안에서 상태(state)를 생성하고 관리할 수 있게 해주는 Hook입니다. React 16.8 이전에는 상태를 사용하려면 반드시 클래스 컴포넌트를 작성해야 했습니다.

하지만 클래스 컴포넌트는 코드가 복잡하고, this 바인딩 문제도 있었죠. useState 덕분에 이제는 함수형 컴포넌트에서도 간결하고 직관적으로 상태를 관리할 수 있습니다.

기존에는 constructor에서 state를 초기화하고, this.setState()를 호출해야 했다면, 이제는 단 한 줄로 상태를 선언하고 업데이트할 수 있습니다. useState의 핵심 특징은 세 가지입니다.

첫째, 배열 구조분해를 통해 상태 값과 업데이트 함수를 동시에 받습니다. 둘째, 초기값을 자유롭게 설정할 수 있습니다.

셋째, 상태가 변경되면 자동으로 컴포넌트가 리렌더링됩니다. 이러한 특징들이 개발자 경험을 크게 향상시켜줍니다.

코드 예제

import React, { useState } from 'react';

function Counter() {
  // count라는 상태와 setCount라는 업데이트 함수를 생성
  const [count, setCount] = useState(0);

  // 버튼 클릭 시 count를 1씩 증가
  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={increment}>증가</button>
    </div>
  );
}

설명

이것이 하는 일: useState는 컴포넌트 내부에 상태 변수를 만들고, 그 상태가 변경될 때마다 컴포넌트를 자동으로 다시 렌더링하여 UI를 최신 상태로 유지합니다. 첫 번째로, const [count, setCount] = useState(0)에서 useState를 호출합니다.

이때 0은 count의 초기값입니다. useState는 배열을 반환하는데, 첫 번째 요소는 현재 상태값(count), 두 번째 요소는 상태를 업데이트하는 함수(setCount)입니다.

배열 구조분해 문법을 사용해서 이 두 값을 한 번에 받아옵니다. 그 다음으로, setCount(count + 1)이 실행되면서 count 상태가 업데이트됩니다.

내부적으로 React는 새로운 상태값을 저장하고, 컴포넌트를 다시 렌더링하도록 스케줄링합니다. 이때 중요한 점은 상태 업데이트가 비동기로 처리된다는 것입니다.

마지막으로, 컴포넌트가 리렌더링되면서 새로운 count 값이 화면에 표시됩니다. React는 이전 렌더링 결과와 새로운 렌더링 결과를 비교해서 실제로 변경된 부분만 DOM에 반영합니다.

이를 통해 최적의 성능을 유지합니다. 여러분이 이 코드를 사용하면 사용자 인터랙션에 반응하는 동적인 UI를 쉽게 만들 수 있습니다.

폼 입력 관리, 토글 버튼, 모달 열고 닫기 등 거의 모든 상호작용 기능에 useState가 활용됩니다.

실전 팁

💡 여러 개의 상태가 필요하면 useState를 여러 번 호출하세요. 관련된 상태끼리 묶지 말고 독립적으로 관리하는 것이 유지보수에 좋습니다.

💡 이전 상태를 기반으로 업데이트할 때는 setCount(count + 1) 대신 setCount(prev => prev + 1) 함수형 업데이트를 사용하세요. 비동기 업데이트 문제를 방지할 수 있습니다.

💡 객체나 배열을 상태로 사용할 때는 반드시 새로운 객체/배열을 만들어서 전달하세요. 기존 객체를 직접 수정하면 React가 변경을 감지하지 못합니다.

💡 초기값 계산이 복잡하다면 useState(() => expensiveComputation())처럼 함수를 전달하세요. 초기 렌더링 시에만 한 번 실행됩니다.

💡 상태가 너무 많아지면 useReducer로 전환하는 것을 고려하세요. 복잡한 상태 로직을 더 명확하게 관리할 수 있습니다.


2. useEffect - 사이드 이펙트 처리

시작하며

여러분이 컴포넌트가 화면에 나타날 때 서버에서 데이터를 가져오거나, 타이머를 시작하거나, 브라우저 타이틀을 변경해야 하는 상황을 생각해보세요. 예를 들어 사용자 프로필 페이지에 들어가면 자동으로 사용자 정보를 API에서 불러와야 합니다.

이런 작업들은 컴포넌트의 주요 목적인 '화면 렌더링'과는 별개의 부가적인 작업입니다. 이를 사이드 이펙트(side effect)라고 부르는데, 적절히 관리하지 않으면 메모리 누수나 무한 루프 같은 심각한 문제가 발생할 수 있습니다.

바로 이럴 때 필요한 것이 useEffect입니다. useEffect를 사용하면 컴포넌트의 생명주기에 맞춰 사이드 이펙트를 안전하고 효율적으로 처리할 수 있습니다.

개요

간단히 말해서, useEffect는 함수형 컴포넌트에서 사이드 이펙트(데이터 fetching, 구독, DOM 조작 등)를 처리할 수 있게 해주는 Hook입니다. 클래스 컴포넌트에서는 componentDidMount, componentDidUpdate, componentWillUnmount 세 개의 생명주기 메서드를 사용했습니다.

useEffect는 이 세 가지 기능을 하나로 통합했습니다. 데이터 가져오기, 이벤트 리스너 등록, 타이머 설정 같은 작업을 한 곳에서 깔끔하게 처리할 수 있죠.

기존에는 여러 생명주기 메서드에 로직이 흩어져 있어서 관련된 코드를 찾기 어려웠다면, 이제는 관련 로직을 useEffect 하나에 모아서 관리할 수 있습니다. useEffect의 핵심 특징은 네 가지입니다.

첫째, 렌더링 후에 실행됩니다. 둘째, 의존성 배열로 실행 시점을 제어할 수 있습니다.

셋째, cleanup 함수를 반환해서 정리 작업을 수행할 수 있습니다. 넷째, 여러 개의 useEffect를 사용해서 관심사를 분리할 수 있습니다.

이러한 특징들이 코드의 가독성과 유지보수성을 크게 향상시킵니다.

코드 예제

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // userId가 변경될 때마다 사용자 데이터를 가져옴
  useEffect(() => {
    setLoading(true);

    // API 호출
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });

    // cleanup 함수: 컴포넌트 언마운트 시 실행
    return () => {
      console.log('cleanup');
    };
  }, [userId]); // userId가 변경될 때만 실행

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

설명

이것이 하는 일: useEffect는 컴포넌트가 렌더링된 후에 특정 작업을 수행하고, 필요한 경우 정리(cleanup) 작업도 자동으로 처리합니다. 첫 번째로, 컴포넌트가 렌더링되면 React는 useEffect에 전달된 함수를 실행 큐에 추가합니다.

이때 중요한 점은 화면 업데이트를 먼저 하고, 그 다음에 effect를 실행한다는 것입니다. 이를 통해 사용자는 빠르게 화면을 볼 수 있고, 무거운 작업이 렌더링을 블로킹하지 않습니다.

그 다음으로, 의존성 배열 [userId]를 체크합니다. 이전 렌더링의 userId와 현재 userId를 비교해서, 값이 변경되었을 때만 effect를 실행합니다.

만약 의존성 배열을 비워두면 첫 렌더링 후 한 번만 실행되고, 아예 생략하면 매 렌더링마다 실행됩니다. 이 메커니즘을 이해하는 것이 무한 루프를 방지하는 핵심입니다.

세 번째로, effect 함수 내부에서 비동기 작업(fetch)을 수행하고 그 결과로 상태를 업데이트합니다. 이때 cleanup 함수를 반환하면, React는 다음 effect를 실행하기 전이나 컴포넌트가 언마운트될 때 이 cleanup 함수를 먼저 호출합니다.

이벤트 리스너 해제, 타이머 취소, 구독 해제 같은 정리 작업을 여기서 처리합니다. 마지막으로, 상태가 업데이트되면서 컴포넌트가 다시 렌더링되고, 새로운 데이터가 화면에 표시됩니다.

전체 과정이 자동으로 이루어지기 때문에 개발자는 비즈니스 로직에만 집중할 수 있습니다. 여러분이 이 코드를 사용하면 데이터 페칭, 구독 관리, 타이머 설정 등의 작업을 안전하게 처리할 수 있습니다.

메모리 누수 걱정 없이 컴포넌트의 전체 생명주기를 효과적으로 관리할 수 있습니다.

실전 팁

💡 의존성 배열을 정확하게 작성하세요. ESLint의 exhaustive-deps 규칙을 활성화하면 빠뜨린 의존성을 자동으로 찾아줍니다.

💡 effect 내부에서 비동기 함수를 직접 사용할 수 없습니다. async/await를 사용하려면 내부에 별도 함수를 만들어서 호출하세요.

💡 이벤트 리스너나 구독을 설정했다면 반드시 cleanup 함수에서 해제하세요. 그렇지 않으면 메모리 누수가 발생합니다.

💡 여러 개의 useEffect로 관심사를 분리하세요. 하나의 useEffect에 모든 로직을 넣지 말고, 데이터 페칭용, 이벤트 리스너용 등으로 나누는 것이 좋습니다.

💡 useEffect 내부에서 상태를 업데이트할 때는 무한 루프에 주의하세요. 업데이트하는 상태를 의존성 배열에 넣으면 무한 루프가 발생할 수 있습니다.


3. useContext - 전역 상태 공유

시작하며

여러분이 앱의 여러 컴포넌트에서 사용자 인증 정보나 테마 설정을 사용해야 할 때, 부모에서 자식으로, 또 그 자식으로 props를 계속 전달하는 것이 얼마나 번거로운지 경험해보셨나요? 예를 들어 최상위 App 컴포넌트에서 5단계 아래 있는 버튼 컴포넌트까지 사용자 정보를 전달하려면 중간의 모든 컴포넌트가 그 props를 받아서 다시 전달해야 합니다.

이런 문제를 prop drilling이라고 부르는데, 코드가 복잡해지고 유지보수가 어려워집니다. 중간 컴포넌트들은 실제로 그 데이터를 사용하지도 않으면서 단지 전달만 하기 때문에, props 인터페이스가 불필요하게 복잡해지죠.

바로 이럴 때 필요한 것이 useContext입니다. Context를 사용하면 컴포넌트 트리 전체에서 데이터를 공유할 수 있어서, 중간 컴포넌트를 거치지 않고도 필요한 곳에서 직접 데이터에 접근할 수 있습니다.

개요

간단히 말해서, useContext는 React Context의 값을 읽고 구독할 수 있게 해주는 Hook입니다. Context는 컴포넌트 트리 전체에 데이터를 공유하는 메커니즘입니다.

전통적인 props 전달 방식은 부모-자식 관계가 명확할 때는 좋지만, 깊이 중첩된 구조에서는 비효율적입니다. Redux 같은 상태 관리 라이브러리를 도입하기에는 과한 경우도 있죠.

useContext는 이 중간 지점에서 완벽한 해결책을 제공합니다. 기존에는 Context.Consumer를 사용해서 render props 패턴으로 값을 읽었다면, 이제는 useContext Hook으로 함수 호출 한 번으로 값을 가져올 수 있습니다.

코드가 훨씬 간결하고 읽기 쉬워집니다. useContext의 핵심 특징은 세 가지입니다.

첫째, createContext로 Context를 생성하고, Provider로 값을 제공합니다. 둘째, 어떤 깊이의 컴포넌트에서도 useContext로 값을 읽을 수 있습니다.

셋째, Context 값이 변경되면 해당 Context를 사용하는 모든 컴포넌트가 자동으로 리렌더링됩니다. 이러한 특징들이 전역 상태 관리를 간단하게 만들어줍니다.

코드 예제

import React, { createContext, useContext, useState } from 'react';

// Context 생성
const ThemeContext = createContext();

function App() {
  const [theme, setTheme] = useState('light');

  // Context Provider로 값 제공
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Content />
    </ThemeContext.Provider>
  );
}

// 깊이 중첩된 컴포넌트에서 Context 사용
function ThemedButton() {
  // useContext로 theme 값을 직접 가져옴
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button
      style={{ background: theme === 'light' ? '#fff' : '#333' }}
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      테마 변경
    </button>
  );
}

설명

이것이 하는 일: useContext는 Context Provider가 제공하는 값을 읽고, 그 값이 변경되면 자동으로 컴포넌트를 리렌더링하여 최신 상태를 유지합니다. 첫 번째로, createContext()를 호출해서 Context 객체를 생성합니다.

이 객체는 Provider와 Consumer 컴포넌트를 포함하고 있습니다. 기본값을 전달할 수도 있지만, 대부분의 경우 Provider에서 실제 값을 제공하기 때문에 생략합니다.

그 다음으로, 상위 컴포넌트에서 <ThemeContext.Provider>로 트리를 감싸고 value prop으로 공유할 데이터를 전달합니다. 이 Provider 하위의 모든 컴포넌트는 이 값에 접근할 수 있습니다.

Provider는 여러 개 중첩될 수 있고, 가장 가까운 Provider의 값이 사용됩니다. 세 번째로, 하위 컴포넌트에서 useContext(ThemeContext)를 호출하면 가장 가까운 Provider의 value를 반환합니다.

내부적으로 React는 컴포넌트 트리를 거슬러 올라가며 해당 Context의 Provider를 찾습니다. 만약 Provider가 없으면 createContext에 전달한 기본값을 사용합니다.

마지막으로, Provider의 value가 변경되면 React는 해당 Context를 구독하고 있는 모든 컴포넌트를 찾아서 리렌더링합니다. 이때 중간에 있는 컴포넌트들은 리렌더링되지 않아도 됩니다.

성능 최적화를 위해 value 객체는 useMemo로 메모이제이션하는 것이 좋습니다. 여러분이 이 코드를 사용하면 사용자 인증 정보, 테마 설정, 언어 설정, 알림 시스템 등 앱 전체에서 공유해야 하는 데이터를 깔끔하게 관리할 수 있습니다.

코드의 복잡도를 크게 줄이고, 컴포넌트 간 결합도도 낮출 수 있습니다.

실전 팁

💡 Context의 value가 객체라면 useMemo로 감싸서 불필요한 리렌더링을 방지하세요. value가 변경되지 않았는데도 객체가 매번 새로 생성되면 모든 구독자가 리렌더링됩니다.

💡 여러 종류의 데이터는 별도의 Context로 분리하세요. 하나의 Context에 모든 것을 넣으면 일부만 변경되어도 전체가 리렌더링됩니다.

💡 Provider를 별도 컴포넌트로 만들어서 재사용하세요. 상태 로직과 Context 제공 로직을 캡슐화하면 코드가 더 깔끔해집니다.

💡 useContext를 사용하는 컴포넌트는 반드시 Provider 하위에 있어야 합니다. 그렇지 않으면 기본값이 사용되거나 에러가 발생할 수 있습니다.

💡 너무 많은 것을 Context에 넣지 마세요. 자주 변경되거나 많은 컴포넌트가 구독하는 데이터는 Redux나 Zustand 같은 전문 상태 관리 라이브러리를 고려하세요.


4. useRef - DOM 접근과 값 보관

시작하며

여러분이 페이지가 로드되자마자 입력 필드에 자동으로 포커스를 주거나, 스크롤 위치를 직접 제어하거나, 이전 값과 현재 값을 비교해야 하는 상황을 만나본 적 있나요? 예를 들어 채팅 앱에서 새 메시지가 도착하면 자동으로 스크롤을 맨 아래로 내려야 합니다.

이런 작업들은 DOM에 직접 접근하거나, 리렌더링되어도 유지되는 값을 저장해야 합니다. 하지만 useState를 사용하면 값이 변경될 때마다 리렌더링이 발생해서 성능 문제가 생길 수 있고, 일반 변수를 사용하면 리렌더링 시 값이 초기화되는 문제가 있습니다.

바로 이럴 때 필요한 것이 useRef입니다. useRef는 리렌더링을 유발하지 않으면서도 값을 계속 유지할 수 있고, DOM 요소에 직접 접근할 수 있는 통로를 제공합니다.

개요

간단히 말해서, useRef는 렌더링과 무관하게 값을 저장하거나 DOM 요소에 직접 접근할 수 있게 해주는 Hook입니다. React는 선언적 프로그래밍을 지향하지만, 때로는 명령적으로 DOM을 제어해야 할 때가 있습니다.

포커스 관리, 애니메이션 트리거, 외부 라이브러리 통합 같은 경우가 그렇죠. useRef는 이런 명령적 작업을 안전하게 수행할 수 있는 탈출구를 제공합니다.

기존에는 createRef를 사용했지만 매 렌더링마다 새로운 ref가 생성되었다면, useRef는 컴포넌트의 전체 생명주기 동안 동일한 ref 객체를 유지합니다. 이것이 중요한 차이점입니다.

useRef의 핵심 특징은 네 가지입니다. 첫째, current 프로퍼티에 값을 저장하고 읽을 수 있습니다.

둘째, 값이 변경되어도 리렌더링이 발생하지 않습니다. 셋째, 리렌더링되어도 값이 유지됩니다.

넷째, DOM 요소의 ref 속성에 전달하면 실제 DOM 노드를 참조할 수 있습니다. 이러한 특징들이 성능 최적화와 명령적 DOM 조작을 가능하게 합니다.

코드 예제

import React, { useRef, useEffect } from 'react';

function SearchInput() {
  // DOM 참조를 위한 ref 생성
  const inputRef = useRef(null);
  // 이전 검색어를 저장하기 위한 ref
  const prevSearchRef = useRef('');

  useEffect(() => {
    // 컴포넌트 마운트 시 입력 필드에 포커스
    inputRef.current.focus();
  }, []);

  const handleSearch = (value) => {
    // 이전 검색어와 비교 (리렌더링 없이)
    if (prevSearchRef.current !== value) {
      console.log(`검색어 변경: ${prevSearchRef.current} -> ${value}`);
      prevSearchRef.current = value;
      // 실제 검색 로직...
    }
  };

  return <input ref={inputRef} onChange={(e) => handleSearch(e.target.value)} />;
}

설명

이것이 하는 일: useRef는 컴포넌트의 전체 생명주기 동안 유지되는 변경 가능한 참조 객체를 생성하고, 그 값이 변경되어도 리렌더링을 트리거하지 않습니다. 첫 번째로, useRef(null)을 호출하면 { current: null } 형태의 객체가 생성됩니다.

이 객체는 컴포넌트가 언마운트될 때까지 동일한 참조를 유지합니다. 초기값으로 null을 전달했지만, 어떤 값이든 전달할 수 있습니다.

그 다음으로, JSX의 ref 속성에 inputRef를 전달하면, React는 해당 DOM 요소가 생성될 때 inputRef.current에 실제 DOM 노드를 할당합니다. 이제 inputRef.current를 통해 네이티브 DOM API(focus, scrollIntoView 등)를 직접 호출할 수 있습니다.

이것이 선언적 React에서 명령적 작업을 수행하는 안전한 방법입니다. 세 번째로, prevSearchRef.current = value처럼 ref의 값을 변경해도 컴포넌트가 리렌더링되지 않습니다.

이것이 useState와의 핵심 차이점입니다. 이전 값 추적, 타이머 ID 저장, 렌더링 횟수 카운트 같은 작업에 유용합니다.

마지막으로, useEffect 내부에서 inputRef.current.focus()를 호출하면 컴포넌트가 마운트된 직후 입력 필드에 포커스가 설정됩니다. DOM이 완전히 생성된 후에 접근해야 하므로 useEffect를 사용합니다.

여러분이 이 코드를 사용하면 포커스 관리, 스크롤 제어, 애니메이션 트리거, 타이머 관리 등 다양한 작업을 효율적으로 처리할 수 있습니다. 불필요한 리렌더링을 방지하면서도 필요한 데이터를 유지할 수 있어서 성능 최적화에도 큰 도움이 됩니다.

실전 팁

💡 ref.current를 렌더링 로직에서 읽거나 쓰지 마세요. 리렌더링을 트리거하지 않아서 화면과 데이터가 불일치할 수 있습니다. 이벤트 핸들러나 effect에서만 사용하세요.

💡 함수 컴포넌트에 ref를 전달하려면 forwardRef로 감싸야 합니다. 그렇지 않으면 ref가 prop으로 전달되지 않습니다.

💡 조건부 렌더링되는 요소에 ref를 사용할 때는 current가 null일 수 있다는 것을 항상 체크하세요. 옵셔널 체이닝(?.)을 사용하면 안전합니다.

💡 ref로 상태를 저장하면 디버깅이 어렵습니다. React DevTools에 표시되지 않기 때문입니다. 가능하면 useState를 사용하고, 성능 문제가 있을 때만 useRef로 최적화하세요.

💡 외부 라이브러리(차트, 맵, 에디터 등)와 통합할 때 ref가 매우 유용합니다. 라이브러리 인스턴스를 ref에 저장하면 리렌더링과 무관하게 API를 호출할 수 있습니다.


5. useMemo - 연산 결과 메모이제이션

시작하며

여러분이 대량의 데이터를 필터링하거나 정렬하거나 복잡한 계산을 수행하는 컴포넌트를 만들 때, 매번 렌더링될 때마다 같은 계산을 반복하는 것이 얼마나 비효율적인지 느껴본 적 있나요? 예를 들어 1만 개의 상품 목록을 가격순으로 정렬하는데, 사용자가 필터를 변경하지도 않았는데 부모 컴포넌트가 리렌더링될 때마다 정렬을 다시 수행한다면 성능이 크게 저하됩니다.

이런 문제는 특히 복잡한 UI나 대용량 데이터를 다루는 앱에서 사용자 경험을 크게 해칩니다. 버튼 하나 클릭하는데 화면이 버벅거린다면 사용자는 금방 이탈할 것입니다.

바로 이럴 때 필요한 것이 useMemo입니다. useMemo는 비용이 큰 계산 결과를 캐시해서, 의존성이 변경되지 않는 한 이전 결과를 재사용합니다.

불필요한 재계산을 방지해서 성능을 크게 향상시킬 수 있습니다.

개요

간단히 말해서, useMemo는 계산 비용이 큰 함수의 결과를 메모이제이션(캐싱)해서, 의존성이 변경될 때만 다시 계산하도록 하는 Hook입니다. React는 기본적으로 부모 컴포넌트가 리렌더링되면 모든 자식 컴포넌트도 리렌더링됩니다.

이것이 대부분의 경우 문제가 되지 않지만, 무거운 연산이 있다면 병목이 될 수 있습니다. useMemo는 이런 병목을 제거하는 최적화 도구입니다.

기존에는 클래스 컴포넌트에서 shouldComponentUpdate나 PureComponent로 최적화했다면, 함수형 컴포넌트에서는 useMemo와 useCallback으로 더 세밀하게 최적화할 수 있습니다. useMemo의 핵심 특징은 세 가지입니다.

첫째, 계산 함수와 의존성 배열을 받습니다. 둘째, 의존성이 변경되지 않으면 이전에 계산된 값을 반환합니다.

셋째, 의존성이 변경되면 계산 함수를 다시 실행하고 새 값을 캐시합니다. 이러한 특징들이 불필요한 재계산을 방지하고 성능을 최적화합니다.

코드 예제

import React, { useState, useMemo } from 'react';

function ProductList({ products, filterCategory }) {
  const [sortOrder, setSortOrder] = useState('asc');

  // 무거운 연산: 필터링 + 정렬 (products나 filterCategory가 변경될 때만 재계산)
  const filteredAndSorted = useMemo(() => {
    console.log('필터링 및 정렬 계산 중...');

    // 1. 카테고리로 필터링
    const filtered = products.filter(p => p.category === filterCategory);

    // 2. 가격순 정렬
    const sorted = filtered.sort((a, b) =>
      sortOrder === 'asc' ? a.price - b.price : b.price - a.price
    );

    return sorted;
  }, [products, filterCategory, sortOrder]); // 이 값들이 변경될 때만 재계산

  return (
    <div>
      <button onClick={() => setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc')}>
        정렬 순서 변경
      </button>
      {filteredAndSorted.map(product => (
        <div key={product.id}>{product.name} - {product.price}원</div>
      ))}
    </div>
  );
}

설명

이것이 하는 일: useMemo는 계산 함수를 실행하고 그 결과를 저장했다가, 다음 렌더링에서 의존성을 비교해서 변경되지 않았으면 저장된 값을 반환하고, 변경되었으면 함수를 다시 실행합니다. 첫 번째로, 컴포넌트가 처음 렌더링될 때 useMemo는 전달받은 계산 함수를 실행합니다.

이 예제에서는 products 배열을 필터링하고 정렬하는 작업이죠. 배열의 크기가 크다면 이 작업은 상당한 시간이 걸릴 수 있습니다.

계산이 완료되면 그 결과를 내부적으로 캐시합니다. 그 다음으로, 컴포넌트가 리렌더링될 때 useMemo는 의존성 배열 [products, filterCategory, sortOrder]의 각 값을 이전 값과 비교합니다.

Object.is 알고리즘을 사용해서 얕은 비교를 수행합니다. 만약 모든 값이 동일하다면 계산 함수를 실행하지 않고 캐시된 결과를 즉시 반환합니다.

콘솔에 '필터링 및 정렬 계산 중...'이 출력되지 않는 것을 보면 재계산이 일어나지 않았다는 것을 알 수 있습니다. 세 번째로, 의존성 중 하나라도 변경되면 useMemo는 계산 함수를 다시 실행하고 새로운 결과를 반환합니다.

동시에 이 새로운 결과를 캐시에 저장해서 다음 렌더링에서 사용할 준비를 합니다. 이 방식으로 필요할 때만 재계산하는 지연 평가(lazy evaluation)를 구현합니다.

마지막으로, 메모이제이션된 값은 일반 변수처럼 사용할 수 있습니다. JSX에서 렌더링하거나, 다른 계산의 입력으로 사용하거나, 자식 컴포넌트에 prop으로 전달할 수 있습니다.

특히 자식 컴포넌트에 전달할 때, 참조가 안정적이어서 자식의 불필요한 리렌더링을 방지하는 효과도 있습니다. 여러분이 이 코드를 사용하면 대용량 데이터 처리, 복잡한 필터링/정렬, 수학 계산, 차트 데이터 변환 등의 작업을 최적화할 수 있습니다.

특히 사용자 인터랙션이 많은 앱에서 부드러운 사용자 경험을 제공하는 데 필수적입니다.

실전 팁

💡 모든 계산에 useMemo를 사용하지 마세요. 메모이제이션 자체도 비용이 있습니다. 프로파일러로 측정해서 실제로 병목이 되는 부분에만 적용하세요.

💡 의존성 배열에 객체나 배열을 넣을 때는 주의하세요. 매번 새로 생성되면 useMemo가 무용지물이 됩니다. 필요하다면 해당 객체/배열도 useMemo로 감싸세요.

💡 useMemo는 의미론적 보장이 아니라 성능 힌트입니다. React는 메모리 확보를 위해 캐시를 버릴 수 있습니다. useMemo 없이도 코드가 정확히 동작하도록 작성하세요.

💡 계산 함수 내부에서 사용하는 모든 외부 값을 의존성 배열에 포함하세요. 빠뜨리면 오래된 값(stale closure)을 참조하는 버그가 발생합니다.

💡 useMemo의 결과를 다른 컴포넌트에 prop으로 전달할 때, 그 컴포넌트를 React.memo로 감싸면 최적화 효과가 배가됩니다.


6. useCallback - 함수 메모이제이션

시작하며

여러분이 자식 컴포넌트에 콜백 함수를 prop으로 전달할 때, 부모가 리렌더링될 때마다 자식도 함께 리렌더링되는 것을 본 적 있나요? 예를 들어 할 일 목록에서 각 항목마다 삭제 버튼이 있고, 체크박스 하나만 클릭해도 모든 항목이 리렌더링된다면 성능이 크게 저하됩니다.

이 문제의 원인은 JavaScript에서 함수가 객체이기 때문에, 매 렌더링마다 새로운 함수가 생성되어 참조가 달라진다는 것입니다. React.memo로 자식 컴포넌트를 감싸도, prop으로 전달되는 함수가 매번 달라지면 최적화가 무효화됩니다.

바로 이럴 때 필요한 것이 useCallback입니다. useCallback은 함수를 메모이제이션해서, 의존성이 변경되지 않는 한 동일한 함수 참조를 유지합니다.

이를 통해 자식 컴포넌트의 불필요한 리렌더링을 방지할 수 있습니다.

개요

간단히 말해서, useCallback은 함수를 메모이제이션해서, 의존성이 변경될 때만 새로운 함수를 생성하고 그렇지 않으면 이전 함수를 재사용하는 Hook입니다. 함수형 컴포넌트에서는 렌더링될 때마다 내부의 모든 함수가 새로 생성됩니다.

대부분의 경우 이것이 문제가 되지 않지만, 최적화된 자식 컴포넌트에 함수를 prop으로 전달할 때는 문제가 됩니다. useCallback은 이 문제를 해결합니다.

useCallback은 사실 useMemo의 특수한 케이스입니다. useCallback(fn, deps)useMemo(() => fn, deps)와 동일합니다.

하지만 함수 메모이제이션은 매우 흔한 패턴이라서 별도의 Hook으로 제공됩니다. useCallback의 핵심 특징은 세 가지입니다.

첫째, 함수와 의존성 배열을 받습니다. 둘째, 의존성이 변경되지 않으면 이전 함수를 반환합니다.

셋째, 의존성이 변경되면 새 함수를 생성하고 캐시합니다. 이러한 특징들이 참조 동일성을 유지하고 불필요한 리렌더링을 방지합니다.

코드 예제

import React, { useState, useCallback, memo } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '장보기', done: false },
    { id: 2, text: '운동하기', done: false }
  ]);

  // useCallback으로 함수를 메모이제이션
  // todos가 변경될 때만 새 함수 생성
  const handleToggle = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  }, []); // setTodos는 안정적이므로 의존성에 포함 안 해도 됨

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

// React.memo로 최적화: props가 같으면 리렌더링 안 함
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
  console.log(`렌더링: ${todo.text}`);
  return (
    <div>
      <input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>삭제</button>
    </div>
  );
});

설명

이것이 하는 일: useCallback은 함수를 캐시해서, 다음 렌더링에서 의존성을 비교하고 변경되지 않았으면 캐시된 함수를 반환하여 참조 동일성을 유지합니다. 첫 번째로, 컴포넌트가 처음 렌더링될 때 useCallback은 전달받은 함수를 그대로 반환하고 내부적으로 캐시합니다.

이 함수는 특정 메모리 주소를 가지고 있고, 이 주소가 함수의 '참조'입니다. JavaScript에서는 함수가 객체이기 때문에 내용이 같아도 참조가 다르면 다른 것으로 간주됩니다.

그 다음으로, 컴포넌트가 리렌더링될 때 useCallback은 의존성 배열을 이전 값과 비교합니다. 모든 의존성이 동일하다면 캐시된 함수(이전과 동일한 참조)를 반환합니다.

이것이 핵심입니다. 자식 컴포넌트는 prop으로 받은 함수의 참조가 같기 때문에, React.memo가 "props가 변경되지 않았다"고 판단하고 리렌더링을 건너뜁니다.

세 번째로, 의존성이 변경되면 useCallback은 새로운 함수를 생성하고 반환합니다. 이 새 함수는 업데이트된 의존성 값을 클로저로 캡처합니다.

예를 들어 의존성 배열에 [filter]가 있고 filter가 변경되면, 새 함수는 업데이트된 filter 값을 사용합니다. 마지막으로, 이 예제에서는 useState의 함수형 업데이트 prev => ...를 사용했습니다.

이렇게 하면 최신 상태를 의존성 없이 참조할 수 있어서, useCallback의 의존성 배열을 비워둘 수 있습니다. 이것이 실무에서 useCallback을 효과적으로 사용하는 패턴입니다.

여러분이 이 코드를 사용하면 리스트 렌더링, 폼 핸들러, 이벤트 리스너 등에서 성능을 최적화할 수 있습니다. 특히 항목이 많은 리스트에서 각 항목이 콜백을 받는 경우, useCallback과 React.memo의 조합은 필수적입니다.

실전 팁

💡 useCallback은 React.memo, useMemo, useEffect의 의존성 배열과 함께 사용할 때 의미가 있습니다. 그렇지 않으면 오히려 메모리만 낭비합니다.

💡 함수 내부에서 상태를 참조할 때는 함수형 업데이트를 사용하세요. 의존성 배열을 단순하게 유지할 수 있고, 오래된 클로저 문제를 피할 수 있습니다.

💡 이벤트 핸들러를 useCallback으로 감싸기 전에 정말 최적화가 필요한지 측정하세요. 대부분의 앱에서는 최적화 없이도 충분히 빠릅니다.

💡 useCallback으로 감싼 함수를 다른 useCallback의 의존성으로 사용할 수 있습니다. 이렇게 함수들을 체인으로 연결할 수 있습니다.

💡 외부 라이브러리의 이벤트 리스너를 등록할 때 useCallback을 사용하면, 리스너를 제거하고 다시 등록하는 것을 방지할 수 있습니다.


7. useReducer - 복잡한 상태 로직 관리

시작하며

여러분이 폼 상태를 관리하는데 여러 개의 useState를 사용하다가, 각 필드의 유효성 검사, 제출 상태, 에러 메시지 등을 추가하면서 코드가 점점 복잡해진 경험이 있나요? 예를 들어 회원가입 폼에서 이메일, 비밀번호, 이름, 전화번호를 관리하고, 각각의 유효성 검사와 에러 상태를 처리하려면 useState가 10개 넘게 필요할 수 있습니다.

이런 상황에서는 상태 업데이트 로직이 여러 이벤트 핸들러에 흩어지고, 관련된 상태들이 함께 업데이트되어야 하는데 실수로 놓치기 쉽습니다. 코드가 길어지고 버그가 발생하기 쉬워집니다.

바로 이럴 때 필요한 것이 useReducer입니다. useReducer는 Redux에서 영감을 받은 Hook으로, 복잡한 상태 로직을 예측 가능하고 테스트하기 쉬운 방식으로 관리할 수 있게 해줍니다.

개요

간단히 말해서, useReducer는 여러 개의 하위 값을 포함하는 복잡한 상태 로직을 관리하거나, 다음 상태가 이전 상태에 의존적일 때 useState 대신 사용하는 Hook입니다. useState는 간단한 상태에는 완벽하지만, 상태 업데이트 로직이 복잡하거나 여러 상태가 서로 연관되어 있으면 관리가 어려워집니다.

useReducer는 상태 업데이트 로직을 컴포넌트 외부로 분리해서, 로직을 재사용하고 테스트하기 쉽게 만듭니다. Redux를 사용해본 적이 있다면 익숙한 패턴입니다.

상태(state), 액션(action), 리듀서(reducer)라는 세 가지 개념으로 상태 관리를 구조화합니다. 하지만 Redux와 달리 전역 스토어 없이 컴포넌트 레벨에서 사용합니다.

useReducer의 핵심 특징은 네 가지입니다. 첫째, 현재 상태와 액션을 받아서 새 상태를 반환하는 reducer 함수를 사용합니다.

둘째, dispatch 함수로 액션을 전달해서 상태를 업데이트합니다. 셋째, 복잡한 업데이트 로직을 컴포넌트에서 분리할 수 있습니다.

넷째, 액션을 기록하면 상태 변경을 추적하고 디버깅하기 쉽습니다. 이러한 특징들이 복잡한 상태 관리를 체계적으로 만들어줍니다.

코드 예제

import React, { useReducer } from 'react';

// 초기 상태
const initialState = {
  email: '',
  password: '',
  isSubmitting: false,
  errors: {}
};

// Reducer 함수: 액션에 따라 상태를 업데이트
function formReducer(state, action) {
  switch (action.type) {
    case 'FIELD_CHANGE':
      return {
        ...state,
        [action.field]: action.value,
        errors: { ...state.errors, [action.field]: null }
      };
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true, errors: {} };
    case 'SUBMIT_SUCCESS':
      return { ...initialState };
    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false, errors: action.errors };
    default:
      return state;
  }
}

function LoginForm() {
  // useReducer로 복잡한 폼 상태 관리
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleSubmit = async (e) => {
    e.preventDefault();
    dispatch({ type: 'SUBMIT_START' });

    try {
      await loginAPI(state.email, state.password);
      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (error) {
      dispatch({ type: 'SUBMIT_ERROR', errors: error.fields });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={state.email}
        onChange={(e) => dispatch({
          type: 'FIELD_CHANGE',
          field: 'email',
          value: e.target.value
        })}
      />
      {state.errors.email && <span>{state.errors.email}</span>}
      <button disabled={state.isSubmitting}>로그인</button>
    </form>
  );
}

설명

이것이 하는 일: useReducer는 현재 상태와 reducer 함수를 사용해서, dispatch로 전달된 액션을 처리하고 새로운 상태를 계산하여 컴포넌트를 리렌더링합니다. 첫 번째로, useReducer(formReducer, initialState)를 호출하면 초기 상태와 dispatch 함수를 받습니다.

dispatch는 액션 객체를 받아서 reducer 함수를 호출하는 함수입니다. 이때 액션 객체는 일반적으로 { type: '액션타입', ...추가데이터 } 형태를 가집니다.

type 필드로 어떤 작업을 수행할지 구분합니다. 그 다음으로, 사용자가 입력 필드를 변경하면 dispatch({ type: 'FIELD_CHANGE', field: 'email', value: e.target.value })가 호출됩니다.

dispatch는 이 액션 객체와 현재 상태를 formReducer 함수에 전달합니다. reducer는 action.type을 확인하고 'FIELD_CHANGE' 케이스로 분기합니다.

세 번째로, reducer 함수는 새로운 상태 객체를 반환합니다. 스프레드 연산자 ...state로 기존 상태를 복사하고, [action.field]: action.value로 변경된 필드만 업데이트합니다.

동시에 해당 필드의 에러도 초기화합니다. 이렇게 하나의 액션으로 여러 상태를 원자적으로 업데이트할 수 있습니다.

마지막으로, React는 반환된 새 상태로 state를 업데이트하고 컴포넌트를 리렌더링합니다. 폼 제출 시에는 'SUBMIT_START', 'SUBMIT_SUCCESS', 'SUBMIT_ERROR' 같은 액션을 순차적으로 dispatch해서 전체 제출 플로우를 관리합니다.

각 단계마다 isSubmitting, errors 같은 관련 상태들이 일관성 있게 업데이트됩니다. 여러분이 이 코드를 사용하면 복잡한 폼, 위저드 UI, 게임 상태, 쇼핑 카트 등 여러 하위 상태를 가진 기능을 체계적으로 관리할 수 있습니다.

상태 변경 로직이 한 곳에 모여 있어서 이해하기 쉽고, 각 액션을 테스트하기도 간단합니다.

실전 팁

💡 reducer 함수는 순수 함수여야 합니다. 같은 입력에 항상 같은 출력을 반환하고, 사이드 이펙트가 없어야 합니다. API 호출이나 타이머 같은 작업은 컴포넌트에서 하세요.

💡 액션 타입을 상수로 정의하면 오타를 방지할 수 있습니다. const FIELD_CHANGE = 'FIELD_CHANGE' 같은 식으로 선언하세요.

💡 TypeScript를 사용한다면 액션 타입을 union type으로 정의하면 type safety를 보장할 수 있습니다.

💡 초기 상태 계산이 복잡하다면 useReducer의 세 번째 인자로 초기화 함수를 전달할 수 있습니다. useReducer(reducer, initialArg, init)

💡 dispatch 함수는 안정적이므로 useEffect의 의존성 배열에 포함하지 않아도 됩니다. 리렌더링되어도 같은 참조를 유지합니다.


8. Custom Hooks - 로직 재사용

시작하며

여러분이 여러 컴포넌트에서 비슷한 로직을 반복해서 작성하고 있다는 것을 발견한 적 있나요? 예를 들어 데이터 페칭 로직이 필요한 컴포넌트마다 useState, useEffect, 로딩 상태, 에러 처리를 매번 똑같이 작성한다면 코드 중복이 심각해집니다.

이런 반복은 단순히 귀찮은 것을 넘어서 유지보수의 악몽이 됩니다. 버그를 발견하면 모든 곳을 찾아다니며 수정해야 하고, 새로운 기능을 추가하려면 여러 파일을 동시에 변경해야 합니다.

실수하기 쉽고 일관성을 유지하기 어렵습니다. 바로 이럴 때 필요한 것이 Custom Hooks입니다.

Custom Hook은 여러분만의 Hook을 만들어서 상태 로직을 재사용 가능한 함수로 추출하는 것입니다. 컴포넌트 로직을 깔끔하게 분리하고, 팀 전체에서 공유할 수 있습니다.

개요

간단히 말해서, Custom Hook은 'use'로 시작하는 이름을 가진 JavaScript 함수로, 내부에서 다른 Hook들을 호출할 수 있고, 상태 로직을 여러 컴포넌트 간에 재사용할 수 있게 해줍니다. React의 가장 강력한 기능 중 하나는 Hook을 조합해서 새로운 Hook을 만들 수 있다는 것입니다.

클래스 컴포넌트 시대에는 Higher-Order Components(HOC)나 Render Props로 로직을 재사용했는데, 이들은 컴포넌트 트리를 복잡하게 만들었습니다. Custom Hook은 이런 문제 없이 순수하게 로직만 재사용합니다.

Custom Hook은 특별한 것이 아닙니다. 그냥 함수일 뿐입니다.

하지만 'use'로 시작하는 네이밍 컨벤션 덕분에 React는 이것이 Hook이라는 것을 알고, Hook의 규칙을 적용합니다. 내부에서 useState, useEffect 등을 자유롭게 사용할 수 있습니다.

Custom Hook의 핵심 특징은 네 가지입니다. 첫째, 상태 로직을 컴포넌트에서 분리해서 재사용할 수 있습니다.

둘째, 각 컴포넌트에서 Custom Hook을 호출하면 독립적인 상태를 가집니다. 셋째, 다른 Hook들을 조합해서 복잡한 로직을 캡슐화할 수 있습니다.

넷째, 테스트하기 쉽고, 팀 내에서 공유하기 좋습니다. 이러한 특징들이 코드 재사용성과 유지보수성을 극대화합니다.

코드 예제

import { useState, useEffect } from 'react';

// Custom Hook: 데이터 페칭 로직을 재사용 가능하게 만듦
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 이전 요청 취소를 위한 플래그
    let cancelled = false;

    setLoading(true);
    setError(null);

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

    // cleanup: 컴포넌트 언마운트 시 플래그 설정
    return () => {
      cancelled = true;
    };
  }, [url]); // url이 변경되면 다시 페칭

  return { data, loading, error };
}

// 사용 예시: 여러 컴포넌트에서 재사용
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

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

function PostList() {
  const { data: posts, loading } = useFetch('/api/posts');

  if (loading) return <div>로딩 중...</div>;
  return posts?.map(post => <div key={post.id}>{post.title}</div>);
}

설명

이것이 하는 일: Custom Hook은 여러 내장 Hook들을 조합해서 특정 기능을 캡슐화하고, 그 기능을 필요로 하는 모든 컴포넌트에서 재사용할 수 있는 인터페이스를 제공합니다. 첫 번째로, useFetch라는 Custom Hook을 정의합니다.

함수 이름이 'use'로 시작하는 것이 중요합니다. 이 네이밍 덕분에 React의 린터가 Hook의 규칙을 적용하고, 다른 개발자들도 이것이 Hook이라는 것을 즉시 알 수 있습니다.

내부에서 useState와 useEffect를 사용해서 데이터 페칭의 전체 생명주기를 관리합니다. 그 다음으로, url을 파라미터로 받아서 해당 엔드포인트에서 데이터를 가져옵니다.

useEffect의 의존성 배열에 url을 넣어서, url이 변경되면 자동으로 새로운 데이터를 페칭합니다. cancelled 플래그를 사용해서 컴포넌트가 언마운트된 후에 상태를 업데이트하려는 것을 방지합니다.

이것이 메모리 누수를 막는 중요한 패턴입니다. 세 번째로, data, loading, error를 객체로 반환합니다.

이렇게 하면 호출하는 쪽에서 필요한 값만 구조분해로 선택해서 사용할 수 있습니다. 배열을 반환할 수도 있지만([data, loading, error]), 객체가 더 명시적이고 순서를 신경 쓰지 않아도 됩니다.

마지막으로, UserProfile과 PostList 컴포넌트에서 useFetch를 호출합니다. 각 컴포넌트는 독립적인 상태를 가집니다.

useFetch를 두 번 호출해도 서로 영향을 주지 않습니다. 이것이 Custom Hook의 핵심입니다.

로직은 공유하지만 상태는 독립적입니다. 여러분이 이 코드를 사용하면 데이터 페칭, 폼 관리, 로컬 스토리지 동기화, 윈도우 사이즈 추적, 타이머 관리 등 모든 종류의 반복되는 로직을 추출할 수 있습니다.

코드베이스가 일관성 있게 유지되고, 버그 수정이나 기능 추가가 한 곳에서 이루어집니다.

실전 팁

💡 Custom Hook은 로직만 재사용하고 UI는 재사용하지 않습니다. UI를 재사용하려면 일반 컴포넌트를 만드세요.

💡 Custom Hook의 이름은 반드시 'use'로 시작해야 Hook의 규칙이 적용됩니다. useSomething 형태로 명명하세요.

💡 Custom Hook도 다른 Custom Hook을 호출할 수 있습니다. 복잡한 로직을 여러 작은 Hook으로 나누어 조합하세요.

💡 반환값은 배열보다 객체를 권장합니다. 호출하는 쪽에서 원하는 이름으로 구조분해할 수 있어서 유연합니다.

💡 Custom Hook을 별도 파일로 분리하면 여러 프로젝트에서 재사용할 수 있습니다. 팀에서 공통 Hook 라이브러리를 만드는 것을 고려하세요.


#React#Hooks#useState#useEffect#CustomHooks