TIL64.React Hooks(2)

35735 단어 ReactTILReact

리액트 훅스 두번째 시간, useReducer, useMemo, useCallback, useRef에 대해 알아보자.

useReducer

useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트해주고 싶을 때 사용하는 hook이다. 리듀서는 현재상태, 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태로 반환하는 함수이다. 새로운 상태를 만들 때는 반드시 불변성을 지켜 주어야 한다.
관리해야 할 state나 넘겨줘야 할 것이 많아지면 관리하기 힘들기때문에 하나의 state, setstate로 관리해준다.

// useState로 관리하는 방법

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

const Info = () => {
  let [name, setName] = useState("");
  let [nickname, setNickname] = useState("");

  const onChangeName = (e) => {
    setName(e.target.value);
  };
  const onChangeNickname = (e) => {
    setNickname(e.target.value);
  };

  return (
    <div>
      <input value={name} onChange={onChangeName} />
      <input value={nickname} onChange={onChangeNickname} />
      <div>이름: {name}</div>
      <div>별명: {nickname}</div>
    </div>
  );
};

export default Info;

// useReducer로 관리하는 방법 (1)

import React, { useReducer } from "react";

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value,
  };
}

const Info = () => {
  const [state, dispatch] = useReducer(reducer, {name:"", nickname:""});
  const { name, nickname } = state;

  const onChange = (e) => {
    dispatch(e.target);
  };

  return (
    <div>
      <input name="name" value={name} onChange={onChange} />
      <input name="nickname" value={nickname} onChange={onChange} />
      <div>이름: {name}</div>
      <div>별명: {nickname}</div>
    </div>
  );
};

// useReducer로 관리하는 방법 (2)
const initialState = {
  name: "",
  nickname: "",
};

const Info = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
 ...
  };

useReducer의 첫번째 인자로 reducer함수, 두번째 인자로는 초기상태 값들을 넣어준다.(2)두번째 방법처럼 초기 state값들의 따로 빼서 변수로 관리하는 방법도 있다.

useMemo & useCallback

컴포넌트는 state, props가 변경될 때마다 리랜더링된다.
컴포넌트가 랜더링되면 내부의 변수, 또 다른 함수 등도 매번 다시 선언되어 사용된다. 랜더링될 때마다 바뀌지 않는 값까지 랜더링되다면, 굉장히 낭비일 것이다.
이때 useMemo와 useCallback를 사용하면 리액트의 랜더링 성능을 최적화시킬 수 있다.

useMemo

메모리제이션된 값을 반환한다. 하위 컴포넌트가 a,b 두 개의 props를 전달 받는 경우를 가정해보자. 전달받은 값으로 하위 컴포넌트에서 서로 다른 함수로 각각의 값을 가공한 새로운 값을 보여주는 로직이 있다. a만 변경되었을 때에도 이전과 같은 값인 b도 역시 다시 함수를 호출된다. useMemo를 쓰면, 다시 호출하여 재계산할 필요없이 이전에 계산된 b의 값을 그대로 쓰면 된다.

const memorizeValue = useMemo(() => computeExpensiveValue(a,b), [a,b]);
import React, { useState, useMemo } from "react";

const getAverage = (numbers) => {
  console.log("평균값 계산 중..");
  if (numbers.length === 0) return 0;

  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState("");
  const avg = useMemo(() => getAverage(list), [list]);

  const onChange = (e) => {
    setNumber(e.target.value);
  };

  const onInsert = () => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  };

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {avg}
      </div>
    </div>
  );
};

export default Average;

숫자를 등록할 때뿐만 아니라 인풋내용이 수정될 때도 'getAverage'함수가 호출된다. 인풋내용이 바뀔 때는 평균값을 다시 계산할 필요가 없기때문에 이렇게 랜더링할 때 마다 계산하는 것은 낭비이다. useMemo를 사용하면, 원하는 값이 바뀌지 않았다면 이전에 연산해둔 결과를 다시 사용한다.

useCallback

useMemo와 비슷한 hook이다. useMemo는 특정 값을 재사용하는 반면, useCallback은 특정함수를 새로 만들지 않고, 재사용하고 싶을 때 사용한다.

import React, { useState, useMemo, useCallback } from "react";

const getAverage = (numbers) => {
  console.log("평균값 계산 중..");
  if (numbers.length === 0) return 0;

  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState("");
  const avg = useMemo(() => getAverage(list), [list]);

  const handleOnChange = useCallback(
    (e) => {
      console.log("onChange");
      setNumber(e.target.value);
    },
    []
  );

  const onInsert = useCallback(() => {
    console.log("onInsert");
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  }, [list, number]);

함수 내부에서 상태 값에 의존하는 경우에는 그 값을두 번째 파라미터 안에 반드시 포함시켜줘야 한다.
onChange의 경우 기존의 값을 조회하지 않고, 바로 설정만 하기 때문에 빈배열이도 상관없지만, onInsert는 기존의 number와 list를 조회해서 nextlist를 생성하기 때문에 배열 안에 number와 list가 필요하다.

useMemo와 useCallback은 언제 사용해야할까?

주로 레퍼런스(메모리 값)가 동일한지 비교하거나, 컴퓨터를 활용하는 비싼 연산(피보나치 수열, 소수 구하기 등)이 필요할 때 사용한다고 한다.

자식 컴포넌트에 함수를 넘겨줄 때 useCallback을 사용하는 것을 추천한다.(jsx태그에 이벤트를 연결해주는 함수는 써주는게 좋다.)
부모 컴포넌트가 랜더링할 때마다 변함이 없는 함수여도 리랜더링되기 때문에 자식컴포넌트는 부모로부터 받은 프롭스함수가 매번 새로운 함수라고 착각하기 때문에 매번 리랜더링을 발생시킨다.

최적화를 위해 모든 함수에 사용한다면 코드가 복잡해지거나 유지보수가 어려워질 수 있기 때문에 무분별한 최적화는 독이 될 수 있다.
사용하기 전에 실질적으로 얻을 수 있는 성능이점이 어느정도인지 측정하고 판단해보는 습관이 필요할 것 같다.

useRef

특정 DOM에 접근해야할 상황(ex.포커스 설정, 특정 엘리먼트 크기 변경 등)에 useRef를 사용한다. 또는 컴포넌트 안에서 변수 조회 및 수정 할때도 사용할 수 있다.
useRef()변수를 작성하고 작업하고싶은 부분에 변수.current를 이용한다.

import React, { useEffect, useRef } from "react";
import track1 from "./track1.mp3";

const Audio = () => {
  const audioRef = useRef(null);
  useEffect(() => {
    //play하기
    audioRef.current.play();
  }, []);
  // 재생 멈추기
  const handleClick = () => {
    audioRef.current.pause();
  };
  return (
    <div>
      <audio src={track1} ref={audioRef} />
      <button onClick={handleClick}>stop Play</button>
    </div>
  );
};

export default Audio;

useState와 useRef

useState와 useRef 모두 상태관리를 위해 사용할 수 있다. 다만 useState의 경우, state변화 후에 리랜더링을 진행하는 반면 useRef는 리랜더링을 하지 않는다. 이러한 특성에 맞추어 랜더링이 필요한 state의 경우에는 useState를 사용하며 그렇지 않은 경우 useRef를 사용하는 것이 좋다.

좋은 웹페이지 즐겨찾기