React Hook 종류

이대현·2024년 6월 18일
0

react

목록 보기
4/9

useRef vs useState

React의 useRefuseState는 둘 다 컴포넌트 상태를 관리하기 위한 훅이지만, 사용하는 목적과 리액트 라이프사이클에서의 동작 방식이 다릅니다.

useRef

  • 목적: useRef는 DOM 요소나 컴포넌트 인스턴스에 대한 참조를 유지하거나, 값이 컴포넌트의 렌더링과 상관없이 유지되어야 할 때 사용합니다.
  • 리액트 라이프사이클에서의 동작: useRef로 생성된 객체는 컴포넌트가 다시 렌더링되어도 동일한 객체를 유지합니다. current 속성의 값이 바뀌어도, 컴포넌트를 다시 렌더링하지 않습니다.
  • 사용 시기:
    • DOM 접근이 필요한 경우
    • 값이 렌더링과 상관없이 유지되어야 하는 경우 (예: 타이머 ID, 이전 렌더링 값)
import { useRef, useEffect } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;
}

useState

  • 목적: useState는 컴포넌트의 상태를 관리하기 위해 사용됩니다. 상태가 변하면 해당 상태를 사용하는 컴포넌트가 다시 렌더링됩니다.
  • 리액트 라이프사이클에서의 동작: useState로 설정된 상태 값이 변경되면, 리액트는 해당 상태를 사용하고 있는 컴포넌트를 다시 렌더링합니다.
  • 사용 시기:
    • 사용자의 입력이나 네트워크 요청 결과 등 컴포넌트에서 표시해야 하는 값이 변경될 때
import { useState } from 'react';

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

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

주요 차이점

  1. 리렌더링:
    • useState는 상태가 변경되면 컴포넌트를 다시 렌더링합니다.
    • useRefcurrent 속성이 변경되어도 컴포넌트를 다시 렌더링하지 않습니다.
  2. 목적:
    • useState는 컴포넌트의 상태를 관리합니다.
    • useRef는 DOM 요소에 대한 참조 또는 렌더링과 상관없이 값을 유지하는 데 사용됩니다.

이렇게 각 훅의 특성을 이해하고 상황에 맞게 사용하는 것이 중요합니다.

useEffect

useEffect는 React Hook 중 하나로, 함수형 컴포넌트에서 부수 효과(side effect)를 수행하기 위해 사용됩니다. 예를 들어, 데이터 fetching, 구독 설정 및 해제, DOM 조작 등이 포함됩니다. useEffect의 파라미터는 두 가지 있습니다:

useEffect(effectFunction, dependencies);

각 파라미터에 대해 자세히 설명하겠습니다.

  1. effectFunction (필수)

    • 이는 부수 효과를 수행할 함수입니다. 이 함수는 컴포넌트가 렌더링된 후에 호출됩니다.
    • effectFunction 내부에서는 데이터를 fetch하거나, 이벤트 리스너를 추가하는 등의 작업을 할 수 있습니다.
    • 이 함수는 선택적으로 정리(clean-up) 작업을 반환할 수 있습니다. 정리 함수는 컴포넌트가 재렌더링되기 전에나 컴포넌트가 언마운트되기 전에 호출됩니다.

    예시:

    useEffect(() => {
        // effect code, e.g., fetching data
        return () => {
            // clean-up code, e.g., removing event listeners
        };
    }, []);
    
  2. dependencies (선택)

    • 이는 의존성 배열로, effectFunction 내에서 사용되는 값들을 포함하는 배열입니다.
    • 이 배열에 포함된 값이 변경될 때마다 effectFunction이 다시 호출됩니다.
    • 의존성 배열이 빈 배열([])일 경우, effectFunction은 컴포넌트가 처음 렌더링될 때 한 번만 실행됩니다.
    • 의존성 배열을 생략한 경우, useEffect는 컴포넌트가 렌더링될 때마다, 즉 매번 컴포넌트가 업데이트될 때마다 실행됩니다. 이 경우는 의존성 배열에 특정 값을 넣는 것이 의미가 없거나, 모든 렌더링마다 부수 효과가 필요할 때 사용됩니다.
      매번 렌더링 후에 useEffect가 실행되어 항상 최신 상태를 반영할 수 있습니다. 그러나 필요 없이 빈번하게 실행되지 않도록 주의해야 합니다.

    예시:

    useEffect(() => {
        // effect code
    }, [dependency1, dependency2]);
    

예제 코드

예제 1: 빈 의존성 배열 (한 번만 실행)

useEffect(() => {
    console.log('Component mounted');
}, []);

예제 2: 의존성 배열 포함 (의존성이 변경될 때마다 실행)

useEffect(() => {
    console.log('Dependency changed:', someDependency);
}, [someDependency]);

예제 3: 클린업 함수 포함

useEffect(() => {
    const handleResize = () => {
        console.log('Window resized');
    };
    window.addEventListener('resize', handleResize);

    // Cleanup function
    return () => {
        window.removeEventListener('resize', handleResize);
    };
}, []);

위 예시들을 참고하여 다양한 방식으로 useEffect를 사용할 수 있습니다. 필요에 따라 적절한 의존성 배열을 설정하는 것이 중요합니다.

useImperativeHandle

useImperativeHandle 훅은 부모 컴포넌트가 자식 컴포넌트 인스턴스에 접근할 수 있는 방법을 제공하여, 커스텀 메서드를 사용할 수 있도록 합니다. 주로 포워딩된 참조(Ref)를 사용하는 경우에 유용합니다. React.forwardRef와 함께 사용하여 구현합니다.

아래는 useImperativeHandle의 사용 예시입니다.

1. 부모 컴포넌트와 자식 컴포넌트 준비

때로는 부모 컴포넌트가 자식 컴포넌트의 특정 기능에 대해 직접 접근해서 호출해야 할 때가 있습니다. 이런 경우 useImperativeHandle을 사용하면 됩니다.

2. 코드 예시

자식 컴포넌트: CustomInput

import React, { useImperativeHandle, forwardRef, useRef } from 'react';

// forwardRef를 사용하여 Ref 전달을 가능하게 함
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  // useImperativeHandle을 사용하여 부모가 사용할 수 있는 메서드를 정의
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    }
  }));

  return <input ref={inputRef} type="text" />;
});

export default CustomInput;

부모 컴포넌트: ParentComponent

import React, { useRef } from 'react';
import CustomInput from './CustomInput';

const ParentComponent = () => {
  const inputRef = useRef();

  const handleFocus = () => {
    inputRef.current.focus();
  };

  const handleClear = () => {
    inputRef.current.clear();
  };

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={handleFocus}>Focus Input</button>
      <button onClick={handleClear}>Clear Input</button>
    </div>
  );
};

export default ParentComponent;

3. 설명

  1. CustomInput 컴포넌트:

    • forwardRef를 사용하여 부모 컴포넌트로부터 전달된 ref를 받을 수 있게 합니다.
    • useImperativeHandle 훅을 사용하여 부모가 사용할 수 있는 커스텀 메서드를 정의합니다. 이 메서드들은 ref 객체의 현재 값을 설정하는 함수들입니다.
    • inputRef는 실제 DOM 요소에 대한 참조를 갖습니다.
  2. ParentComponent 컴포넌트:

    • useRef를 사용하여 inputRef 참조를 생성합니다.
    • 버튼 클릭 이벤트 핸들러에서 inputRef.current를 통해 CustomInput 컴포넌트에서 정의한 커스텀 메서드 focusclear를 호출합니다.

위와 같은 방법으로 useImperativeHandle을 사용하여 부모 컴포넌트가 자식 컴포넌트의 특정 기능에 접근할 수 있게 할 수 있습니다. 이는 특히 여러 자식 컴포넌트를 다루거나 특정 조건에서만 자식 컴포넌트의 메서드를 호출해야 하는 경우에 유용합니다.

useReducer

useReducer 훅은 리액트에서 상태 관리를 더 구조화하고 복잡한 상태 로직을 다룰 때 유용합니다. 주로 상태가 여러 가지로 나뉘어지고 변화가 복잡한 경우에 사용하는데, 이는 redux와 비슷한 패턴을 따릅니다.

아래는 useReducer를 사용한 상태 관리의 예시입니다.

1. 기본 구조

useReducer는 기본적으로 두 가지 인자를 가지며, 상태와 이를 업데이트하는 dispatch 함수를 반환합니다.

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer는 상태를 업데이트하는 함수입니다.
  • initialState는 초기 상태 값입니다.

2. 예시: 카운터 앱

1. 리듀서 함수(reducer)

리듀서 함수는 현재 상태와 액션을 받아서 새로운 상태를 반환합니다.

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error();
  }
}

2. 컴포넌트

useReducer 훅을 사용하여 상태를 관리하는 컴포넌트를 만듭니다.

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

export default Counter;

3. 설명

  1. 리듀서 함수 정의:

    • reducer 함수는 두 가지 인자를 받습니다:
      • state: 현재 상태
      • action: 상태를 변화시킬 명령을 내리는 객체
    • 이 함수는 switch 문을 통해 다양한 액션 타입에 따라 상태를 업데이트합니다.
  2. 초기 상태 정의:

    • initialState는 초기 값으로 { count: 0 }을 가집니다.
  3. Counter 컴포넌트:

    • useReducer 훅을 사용하여 statedispatch 함수를 가져옵니다.
    • dispatch 함수는 액션 객체를 받아 reducer 함수에 전달하여 상태를 업데이트합니다.
    • 각 버튼 클릭 시 해당 액션 타입을 dispatch에 전달합니다.

이렇게 하면 상태 변화 로직이 각각의 컴포넌트에서 분리되어 보다 명확하고 관리하기 쉬워집니다. useReducer는 상태 로직이 복잡하고 다양한 상태를 처리해야 하는 경우에 특히 유용합니다.

useLayoutEffect

useLayoutEffectuseEffect 와 유사하지만, DOM 변화 직후 그리고 브라우저가 그 내용을 스크린에 그리기 전에 실행됩니다. 이 훅은 렌더링 성능에 민감한 작업을 수행해야 할 때 주로 사용됩니다. 예를 들면, 레이아웃을 측정하거나 동기적으로 DOM을 업데이트하는 경우에 적합합니다.

1. 사용 시기

  • 인터페이스를 그릴 때 발생할 수 있는 깜빡임(Flashing)을 방지하고 싶은 경우
  • DOM 노드의 레이아웃(높이, 너비 등)을 측정한 후 그 값을 기반으로 다른 작업을 해야 하는 경우

2. 차이점

  • useEffect: 비동기적으로 실행됨 (브라우저 페인트 후 실행)
  • useLayoutEffect: 동기적으로 실행됨 (브라우저 페인트 전에 실행)

3. 예시: 높이 측정 및 동기화

아래 예시는 모든 div 요소의 높이를 측정하고 그 높이에 기반하여 UI를 업데이트하는 예시입니다.

1. 컴포넌트 정의

import React, { useLayoutEffect, useRef, useState } from 'react';

function MeasureExample() {
  const divRef = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    if (divRef.current) {
      setHeight(divRef.current.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ padding: '20px', border: '1px solid black' }}>
        Hello, I am the content inside the div. Resize me to see effect!
      </div>
      <p>The above div's height is: {height}px</p>
    </div>
  );
}

export default MeasureExample;

4. 설명

  1. useRef로 DOM 접근:

    • divRef를 생성하여 div 요소에 대한 참조를 저장합니다.
    • ref 속성을 div에 설정하여 해당 DOM 노드에 직접 접근할 수 있게 합니다.
  2. useState로 상태 관리:

    • height라는 상태를 만들어 div 요소의 높이를 저장합니다.
  3. useLayoutEffect로 높이 측정:

    • 컴포넌트가 처음 렌더링될 때 useLayoutEffect가 실행됩니다.
    • divRef.current.getBoundingClientRect().height를 호출하여 div의 높이를 측정하고, 이 값을 height 상태에 저장합니다.
    • height 상태가 업데이트되면 컴포넌트는 다시 렌더링되고, 높이가 화면에 표시됩니다.

5. 적용 사례

  • 레이아웃 측정: DOM 요소의 크기나 위치를 측정하고 이를 기반으로 레이아웃을 동기화할 때.
  • 애니메이션: 애니메이션을 적용하기 전에 DOM의 초기 상태를 설정해야 할 때.
  • 스크롤 제어: 컴포넌트가 렌더링되기 전에 스크롤 위치를 설정해야 할 때.

이 예시를 통해 useLayoutEffect가 브라우저가 내용을 그리기 전에 실행되어 DOM 측정 및 동기화에 유용함을 알 수 있습니다. 이를 활용하면 사용자 경험을 향상시키고 컨텐츠 깜빡임을 방지할 수 있습니다.

useCallback

useCallback 훅은 메모이제이션된 콜백 함수를 생성하는 데 사용됩니다. 이는 리액트가 매 렌더링 시마다 새로운 함수 객체를 생성하는 것을 방지하여 성능 최적화를 도와줍니다. 특히, 하위 컴포넌트에 콜백 함수를 props로 전달할 때 유용합니다.

메모이제이션이란 값이 변경되지 않으면 이전에 계산된 결과를 재사용하여 불필요한 연산을 줄이는 방법입니다.

1. 기본 사용법

useCallback 훅은 두 개의 인자를 가집니다.

  • 첫 번째 인자: 메모이제이션할 함수
  • 두 번째 인자: 의존성 배열 (이 배열에 있는 값이 변경될 때만 함수가 다시 생성됨)
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

2. 예시: 부모와 자식 컴포넌트

리스트 항목을 렌더링하고, 항목 클릭 이벤트를 처리하는 예제를 통해 useCallback의 사용법을 설명하겠습니다.

1. 예시 코드

부모 컴포넌트
import React, { useCallback, useState } from 'react';
import List from './List';

function App() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([1, 2, 3, 4, 5]);

  const handleItemClick = useCallback((item) => {
    console.log(`Item clicked: ${item}`);
  }, [items]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <List items={items} onItemClick={handleItemClick} />
    </div>
  );
}

export default App;
자식 컴포넌트
import React from 'react';

function List({ items, onItemClick }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item} onClick={() => onItemClick(item)}>
          {item}
        </li>
      ))}
    </ul>
  );
}

export default List;

3. 설명

  1. 상태 설정:

    • countitems 상태를 useState 훅을 사용하여 설정합니다.
    • handleItemClick 콜백 함수를 정의합니다. 이 함수는 목록 항목이 클릭될 때 호출됩니다.
  2. useCallback 사용:

    • handleItemClickuseCallback 훅을 사용하여 메모이제이션됩니다. 이는 items 배열이 변경되지 않는 한 매 렌더링마다 동일한 함수가 유지됨을 의미합니다.
    • 의존성 배열 [items]items 배열이 변경될 때만 handleItemClick 함수가 다시 생성되도록 합니다.
  3. 자식 컴포넌트 및 prop 전달:

    • 자식 컴포넌트 ListitemshandleItemClick을 props로 전달합니다.
    • List 컴포넌트는 리스트의 각 항목을 렌더링하며, 항목이 클릭되면 handleItemClick 콜백을 호출합니다.

4. 성능 최적화

이 예제에서 useCallback을 사용하지 않는다면, App 컴포넌트가 렌더될 때마다 새로운 handleItemClick 함수가 생성될 것입니다. 이는 List 컴포넌트가 불필요하게 다시 렌더링되는 결과를 초래할 수 있습니다. useCallback을 사용함으로써 이러한 불필요한 렌더링을 방지하여 성능을 최적화할 수 있습니다.

5. 요약

  • useCallback 훅은 메모이제이션된 콜백 함수를 생성하여 함수 객체가 불필요하게 다시 생성되는 것을 방지합니다.
  • 이를 통해 성능 최적화가 가능하며, 특히 함수가 자식 컴포넌트에 전달되는 경우 불필요한 재렌더링을 방지할 수 있습니다.
  • 의존성 배열을 사용하여 어떤 조건에서 콜백 함수가 다시 생성되어야 하는지를 지정할 수 있습니다.

이 예제를 통해 리액트에서 useCallback이 어떻게 성능 최적화에 기여할 수 있는지, 특히 콜백 함수가 종속된 상태나 props가 변경될 때만 다시 생성되도록 하는 방법을 이해할 수 있습니다.

useMemo

useMemo 훅은 연산 비용이 높은 작업의 결과를 메모이제이션하여 성능 최적화를 돕는 역할을 합니다. 이를 통해 컴포넌트가 불필요하게 재렌더링되는 것을 방지할 수 있습니다. useMemo는 특정 값이 변경되었을 때만 메모이제이션된 값을 다시 계산합니다.

1. 기본 사용법

useMemo는 두 개의 인자를 받습니다:

  • 첫 번째 인자: 메모이제이션할 값이나 함수.
  • 두 번째 인자: 의존성 배열. 이 배열에 명시된 값이 변경될 때만 첫 번째 인자의 값이나 함수가 다시 계산됩니다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • computeExpensiveValue(a, b) 함수는 a 또는 b가 변경될 때만 다시 실행됩니다.

2. 예시: 리스트 필터링

아래 예시는 입력 값에 따라 필터링된 리스트를 계산하고, 이 작업이 불필요하게 다시 실행되지 않도록 하는 예시입니다.

1. 예시 코드

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

function List({ items }) {
  const [filter, setFilter] = useState('');

  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => item.toLowerCase().includes(filter.toLowerCase()));
  }, [items, filter]);

  return (
    <div>
      <input
        type="text"
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter items"
      />
      <ul>
        {filteredItems.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default List;

3. 설명

  1. 상태 관리:

    • filter 상태는 필터 텍스트 입력값을 저장합니다.
  2. useMemo 사용:

    • filteredItemsuseMemo 훅을 사용하여 계산된 리스트입니다.
    • itemsfilter가 변경될 때만 필터링 연산이 수행됩니다.
    • useMemo는 의존성 배열 [items, filter]을 보고 이 값들이 변경되었을 때만 필터링 함수를 다시 실행합니다.
  3. 필터링 로직:

    • 필터링 로직은 items 배열을 filter 문자열에 맞게 필터링합니다.
    • 이 작업은 비용이 많이 드는 작업일 수 있는데, useMemo를 사용함으로써 itemsfilter가 변경되지 않으면 필터링 작업을 다시 수행하지 않습니다.

4. 성능 최적화

useMemo를 사용하지 않으면, List 컴포넌트가 렌더링될 때마다 items.filter 연산이 다시 실행됩니다. 이는 특히 items 배열이 크거나 필터링 연산이 복잡할 경우 성능에 큰 영향을 줄 수 있습니다. useMemo를 사용하면 필요할 때만 필터링 연산을 수행하여 성능을 최적화할 수 있습니다.

5. 추가 예시: 계산된 값 메모이제이션

아래는 숫자 리스트의 합계를 계산하는 예시입니다. 이 역시 사용자 입력에 따라 값이 변경될 때만 다시 계산하도록 useMemo를 사용합니다.

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

function SumList() {
  const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
  const [newNumber, setNewNumber] = useState('');

  const addNumber = () => {
    setNumbers([...numbers, parseInt(newNumber)]);
    setNewNumber('');
  };

  const totalSum = useMemo(() => {
    console.log('Calculating sum...');
    return numbers.reduce((sum, number) => sum + number, 0);
  }, [numbers]);

  return (
    <div>
      <input
        type="number"
        value={newNumber}
        onChange={(e) => setNewNumber(e.target.value)}
        placeholder="Add a number"
      />
      <button onClick={addNumber}>Add</button>
      <p>Sum of numbers: {totalSum}</p>
    </div>
  );
}

export default SumList;

6. 설명

  1. 상태 관리:

    • numbers 상태는 숫자들의 배열을 저장합니다.
    • newNumber 상태는 입력된 숫자 값을 저장합니다.
  2. useMemo 사용:

    • totalSumuseMemo 훅을 사용하여 숫자 배열의 합계를 계산합니다.
    • numbers 배열이 변경될 때만 합계를 다시 계산합니다.
    • 이를 통해 컴포넌트의 다른 상태나 props가 변경되어도 불필요한 재계산을 방지할 수 있습니다.

요약

  • useMemo 훅은 연산 비용이 높은 작업의 결과를 메모이제이션하여 성능을 최적화합니다.
  • 의존성 배열을 사용하여 특정 조건에서만 값을 다시 계산하도록 설정할 수 있습니다.
  • useMemo를 사용함으로써 불필요한 연산을 피하고, 성능을 향상시킬 수 있습니다.

이 예제를 통해 useMemo가 어떻게 사용되고, 연산 비용이 높은 작업을 최적화하는 데 얼마나 유용한지 이해할 수 있습니다. 이를 적절히 활용하면 리액트 컴포넌트의 성능을 크게 향상시킬 수 있습니다.

profile
Frontend Developer

0개의 댓글