내일배움캠프 React_7기 TIL - 24. Reack Hook

·2024년 11월 8일
0

hook

Hook은 React 16.8부터 새로 추가된 기능으로, 기존의 클래스 기반 코드 없이도 상태 값 관리와 여러 React 기능을 사용할 수 있게 해주는 함수이다.
Hook을 사용하면 컴포넌트에서 다양한 React 기능을 활용할 수 있으며, 내장된 Hook을 이용하거나 커스텀 Hook을 만들어 사용할 수 있다.

메모제이션

메모제이션이란, 컴포넌트가 불필요한 리렌더링을 막아 성능을 최적화하는 기법이다.
React.memo : 컴포넌트의 불필요한 리렌더링을 방지해주는 함수이다.
useCallback : 함수를 메모이제이션하여, 동일한 함수가 반복해서 생성되지 않도록 방지해주는 Hook이다.
useMemo : 복잡한 연산의 결과값을 메모이제이션하여, 재계산을 방지해주는 Hook이다.

React.memo

React.memo는 고차 컴포넌트로, 컴포넌트의 props가 변경되지 않으면 컴포넌트를 리렌더링하지 않도록 해준다.

고차 컴포넌트(Higher-Order Component, HOC)란?
-> 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 함수
const EnhancedComponent = higherOrderComponent(WrappedComponent);

React.memo를 언제 사용하는것이 좋을까?

  • props를 받아 렌더링만 하는 컴포넌트
  • 즉, 화면에 데이터만 표시하고 별다른 상태 변화가 없는, 반복적으로 렌더링되는 컴포넌트
import React, { useState } from 'react';

// React.memo로 최적화된 컴포넌트
const MyComponent = React.memo(({ name }) => {
  console.log("렌더링");
  return <div>{name}</div>;
});

function App() {
  const [name, setName] = useState('Alice');
  return (
    <div>
      <MyComponent name={name} />
      <button onClick={() => setName('Bob')}>이름 변경</button>
    </div>
  );
}

export default App;

React.memo는 props가 변경되지 않으면 해당 컴포넌트를 리렌더링하지 않도록 최적화한다.
이 예시에서는 nameAlice에서 Bob으로 변경될 때만 MyComponent가 리렌더링된다.
App컴포넌트에서 setName을 통해 name이 변경되면,MyComponent가 리렌더링되지만, 그 외의 상태 변경(예: App에서 name과 관련 없는 다른 상태 변경)은 MyComponent에 영향을 미치지 않으므로 MyComponent는 리렌더링되지 않는다.

useCallback

useCallback은 함수가 불필요하게 재생성(재정의)되는 것을 방지하는 훅이다.
주로 함수를 props로 전달할 때 사용한다.

useCallback을 언제 사용하는것이 좋을까?

  • 부모 컴포넌트에서 정의된 함수를 자식컴포넌트의 props로 전달될 때, 부모가 리렌더링되면 함수도 새로 생성된다. 그렇게되면 자식 컴포넌트도 불필요하게 리렌더링 될 수 있다.
  • 이때, useCallback을 사용하여 함수의 참조를 메모제이션 하면, 자식 컴포넌트가 동일한 함수 참조를 받게 되어 리렌더링을 방지할 수 있다.
  • 의존성 배열을 사용하여 함수가 특정 값들에 의존하는 경우, 해당 값들이 변경되지 않으면 동일한 함수를 재사용할 수 있다.
  • 단, 함수가 단순하거나 자주 변경되어야 할 필요가 있다면 굳이 사용하지 않는 것이 좋다.

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

  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

useCallback을 사용하여 handleClick의 참조값을 메모제이션 했기 때문에, ParentComponent가 리렌더링되어도 함수가 재생성되지 않는다.
+) 이 코드에서는 의존성 배열이 비어있으므로, handleClick 함수는 한번만 생성되고 그 이후로는 동일한 함수 참조를 계속 사용한다.

useMemo

useMemo는 계산량이 많거나 복잡한 연산의 결과를 캐싱해서 성능을 최적화(불필요한 재계산 방지) 할 때 사용한다.

useMemo를 언제 사용하는것이 좋을까?

  • 비용이 큰 계산이 필요한 경우 사용하는 것이 좋다. 동일한 입력값에 대해 매번 동일한 결과를 반환하도록 메모이제이션하여 성능을 최적화한다.
import React, { useState, useMemo } from 'react';

function App() {
  const [count, setCount] = useState(0);
  
  // 복잡한 계산 (예시: 제곱 계산)
  const squared = useMemo(() => {
    console.log("계산 중...");
    return count * count;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Squared: {squared}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

export default App;

useMemo를 사용하여 squared값을 계산한다. count 값이 변경될 때만 count * count 계산이 다시 실행된다.

  • 참조 값이 동일한 객체/배열을 재사용할 때도 사용이 가능하다.
import React, { useState, useMemo } from 'react';

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

  // 객체 생성 (useMemo를 사용하여 객체를 메모이제이션)
  const objectValue = useMemo(() => {
    return { count }; // { count: count }와 동일(객체리터럴)
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Object: {JSON.stringify(objectValue)}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

export default App;

objectValue{ count: 0 }과 같이 { count: 숫자 } 형태의 객체이다. useMemo는 이 객체를 메모이제이션하여 count 값이 변경되지 않는 한 이전 객체를 재사용한다.

메모제이션 사용 시 주의할 점

메모이제이션은 유용하지만, 메모이제이션도 메모리를 사용하기 때문에 필요 이상으로 남용하면 성능을 오히려 떨어뜨릴 수 있다.

useRef

useRef는 컴포넌트의 상태가 변경되더라도 리렌더링을 유발하지 않고 값을 저장하는 데 유용한 훅이다.

useRef로 DOM 요소에 접근

import React, { useRef } from 'react';

function FocusInput() {
  const inputRef = useRef(null); // input 요소를 참조하는 useRef 생성

  const handleFocus = () => {
    // input 요소에 포커스 설정
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="버튼을 클릭하면 포커스가 이동합니다." />
      <button onClick={handleFocus}>포커스 이동</button>
    </div>
  );
}

export default FocusInput;

여기서 currentuseRef로 만든 객체의 속성 중 하나다.

왜 inputRef.focus() 가 아니라 inputRef.current.focus()일까?
-> useRef 객체를 반환하기 때문이다.
inputRef는 아래와 같은 형태로 초기화되는 것이다.

{
  current: null  // 초기값은 null
}

즉, inputRef는 dom요소를 직접 가리키고 있는 것이 아니고 inputRef 객체의 current 속성 안에 해당 dom요소가 할당되는것이다.

처음에는 null로 시작하고 있고, DOM요소가 렌더링되면 그 요소가 current에 할당되고 있다. 버튼을 클릭하면 handleFocus 함수에서 inputRef.current.focus()를 호출하여 input 요소에 포커스를 설정한다.
이렇게 useRef를 사용하면, DOM 요소에 직접 접근하여 상태를 변경할 수 있고, ref로 참조된 DOM 요소는 컴포넌트가 리렌더링되어도 리렌더링되지 않는다.

리렌더링 없이 값 유지하기

useRef는 리렌더링을 발생시키지 않고 데이터를 저장할 수 있기 때문에, 렌더링에 영향을 미치지 않아야 하는 데이터를 저장하는 데 유용하다.

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

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

  useEffect(() => {
    // 컴포넌트가 업데이트될 때마다 이전 카운터 값을 저장
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>현재 값: {count}</p>
      <p>이전 값: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

export default Counter;

prevCountRef는 useRef로 초기화되며, 초기값은 count 값이다.
useEffect는 count 값이 변경될 때마다 실행되고, 현재 count값을 저장하게 된다.
useEffect는 렌더링 후에 실행되므로, 항상 prevCountRef.current에는 이전 렌더링에서의 count 값이 담기게 된다.

<p>이전 값: {prevCountRef.current}</p>는 계속 이전 값을 담게 될까?
이유는, useEffcet의 실행 시점 때문. useEffect는 JSX가 렌더링이 완료되었을 시점에서 실행된다. 즉,<p>이전 값: {prevCountRef.current}</p>가 먼저 그려진 후에 useEffect가 실행되기에 아직 prevCountRef.current로 보여지는 값은 리렌더링 이전의 값일 것이다.

이외에도, setInterval과 setTimeout 제어, 컴포넌트의 마운트 여부 추적, 애니메이션 효과 제어 등의 사용법이 있으나 현재 여기까지 이해하고 정리하는데도 어려웠기 때문에... 실제로 사용 시 다시 정리하고자 한다. 😔😔

React hook의 사용 규칙

훅은 렌더링 이후에 호출 (혹은 위치)되면 안 된다

훅은 렌더링이 시작되기 전에 선언되어야 한다. 렌더링 이후에 훅이 호출되면 리액트가 컴포넌트의 상태와 생명주기를 올바르게 관리 할 수 없다. 이로 인해 오류가 나거나 불필요한 리렌더링이 발생할 수 있으니 주의.

훅은 조건문, 반복문이나 함수 내부에서 호출하면 안 된다

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

function ExampleComponent({ show }) {
  // 조건에 따라 훅을 나중에 선언하는 잘못된 패턴
  if (show) {
    const [count, setCount] = useState(0); // 이 훅은 조건에 따라 실행됨
  }

  useEffect(() => {
    console.log("컴포넌트가 업데이트되었습니다.");
  });

  return <div>컴포넌트</div>;
}

export default ExampleComponent;

이 코드에서는 show가 true일 때만 useState가 호출된다. 그러나 리액트는 컴포넌트가 처음 렌더링될 때 모든 훅이 같은 순서로 호출될 것을 기대한다. 훅의 위치가 바뀌면 리액트는 상태를 제대로 관리하지 못해 오류가 발생하거나 예기치 않은 동작이 일어날 수 있다.

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

function ExampleComponent({ show }) {
  // 최상위에서 훅을 선언
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("컴포넌트가 업데이트되었습니다.");
  });

  return (
    <div>
      컴포넌트
      {show && <p>Count: {count}</p>}
    </div>
  );
}

export default ExampleComponent;

따라서 조건문이나 반복문을 사용해 훅의 위치가 바뀌지 않도록 주의해야 한다!

훅은 함수형 컴포넌트나 커스텀 훅에서만 호출해야 한다

function someUtilityFunction() {
  const [value, setValue] = useState(0); // 규칙 위반
  return value;
}

일반 함수 내부에서 useState를 호출하는 것은 규칙 위반이니 주의.

불필요한 훅 사용을 피해야 한다

React에서는 훅을 남용할 경우 성능이 저하되거나 예상치 못한 문제가 발생할 수 있다. 예를 들어, 상태가 아닌 값을 저장하기 위해 useState를 사용하는 것은 불필요하다. 리렌더링이 필요하지 않은 데이터는 useRef를 사용하는 것이 더 적절하고, 때로는 let 변수로도 충분하다.

useEffect와 useState 하나의 컴포넌트 내에서 너무 많이 쓰인다면, 컴포넌트 분리하여 로직을 단순화 시키는 것을 고려해야한다.

profile
내배캠 React_7기 이수중

0개의 댓글

관련 채용 정보