[TIL] 23.05.09

Minkyu Shin·2023년 5월 9일
0

TIL

목록 보기
25/44
post-thumbnail

오늘의 나는 무엇을 배웠을까?

useCallback

useCallback 은 특정 함수를 새로 만들지 않고, 재사용하고 싶을 때 사용하는 리액트 Hook이다. 동작은 마치 useMemo 와 같이 하지만, 결과값을 재사용할지 함수를 재사용할지에 따라 각각 골라 사용하면 된다.

컴포넌트 내부에 함수를 선언하게 되면 리렌더링 시마다 함수가 새로 만들어진다. 함수를 렌더링 시마다 선언하여 사용하는 것이 리소스를 많이 사용하지 않기 때문에 큰 문제는 없다고 한다. 하지만 컴포넌트에서 props 가 변경되지 않으면 Virtual Dom에 새로 렌더링조차 하지 않고 컴포넌트의 결과물을 재사용하는 최적화 작업을 하기 위해서는 함수의 재사용이 반드시 필요하다.

컴포넌트 내에서 선언된 다음과 같은 함수가 있다고 생각해보자.

const onCreate = () => {
  const user = {
    id: nextId.current,
    username,
    email
  };
  setUsers([...users, user]);

  setInputs({
    username: '',
    email: ''
  });
  nextId.current += 1;
};

const onRemove = id => {
  setUsers(users.filter(user => user.id !== id));
};
const onToggle = id => {
  setUsers(
    users.map(user =>
      user.id === id ? { ...user, active: !user.active } : user
    )
  );
};

이 함수들은 컴포넌트가 리렌더링 될 때마다 새로 생기게 된다. 이를 방지하기 위해 useCallback Hook을 사용해주면 된다.

useCallback 을 활용하여 함수를 선언하게 되면 코드는 아래와 같이 바뀐다.

import { useCallback, useMemo, useRef, useState } from "react";
import UserList from "./UserList";
import CreateUser from "./CreateUser";

const countActiveUsers = (users) => {
  console.log("활성 사용자를 세는 중...");
  return users.filter((user) => user.active).length;
};

function App() {
  const [inputs, setInputs] = useState({
    username: "",
    email: "",
  });
  const { username, email } = inputs;

  const onChange = useCallback(
    (e) => {
      const { name, value } = e.target;
      setInputs({
        ...inputs,
        [name]: value,
      });
    },
    [inputs] // 함수 안에서 사용하는 state deps 배열에 추가
  );

  const [users, setUsers] = useState([
    {
      id: 1,
      username: "velopert",
      email: "public.velopert@gmail.com",
      active: true,
    },
    {
      id: 2,
      username: "tester",
      email: "tester@example.com",
      active: false,
    },
    {
      id: 3,
      username: "liz",
      email: "liz@example.com",
      active: false,
    },
  ]);

  const nextId = useRef(4);

  const onCreate = useCallback(() => {
    const user = {
      id: nextId.current,
      username,
      email,
    };
    setUsers([...users, user]);

    setInputs({
      username: "",
      email: "",
    });
    nextId.current += 1;
  }, [users, username, email] // 함수 안에서 사용하는 state deps 배열에 추가
  );

  const onRemove = useCallback(
    (id) => {
      setUsers(users.filter((user) => user.id !== id));
    },
    [users] // 함수 안에서 사용하는 state deps 배열에 추가
  );

  const onToggle = useCallback(
    (id) => {
      setUsers(
        users.map((user) => {
          return user.id === id ? { ...user, active: !user.active } : user;
        })
      );
    },
    [users] // 함수 안에서 사용하는 state deps 배열에 추가
  );

  const count = useMemo(() => countActiveUsers(users), [users]);

  return (
    <>
      <CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} />
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} active />
      <div>활성사용자 수 : {count}</div>
    </>
  );
}

export default App;

useCallback 을 사용할 때 함수 내부에서 사용하는 state나 props가 있다면 반드시 deps에 추가해 줘야 한다. 만약, 이를 추가해주지 않으면 함수 내부에서 state 또는 props를 참조할 때 그것들이 최신값을 가리킨다는 보장을 하지 못하기 때문이다. 이를 풀어서 말한다면 state나 props가 변경되어도 함수가 다시 생성되지 않기 때문에, 이전 생성 당시의 값들을 참조한다고 할 수 있다.

useCallbackuseMemo 를 기반으로 함수에 특화되어 개량된 것으로 볼 수 있다. 따라서, 둘의 기능 차이만 알고 묶어 알고 있어도 좋을 것 같다는 생각을 했다.

useCallback 만을 사용하면 눈에 띄는 최적화가 없다고 한다. 하지만 컴포넌트 렌더링 최적화와 함께 사용된다면 성능 최적화가 가능하다.

React.memo

컴포넌트 렌더링 최적화를 위해 사용하는 함수이다. 컴포넌트에서 사용하는 props가 변경되지 않으면 리렌더링을 발생시키지 않아 컴포넌트 리렌더링 성능 최적화를 해줄 수 있다.

사용방법은 단순히 컴포넌트를 함수로 감싸주면 된다.

import React from "react";

const CreateUser = ({ username, email, onChange, onCreate }) => {
  return (
    <div>
      <input name="username" placeholder="계정명" onChange={onChange} value={username} />
      <input name="email" placeholder="이메일" onChange={onChange} value={email} />
      <button onClick={onCreate}>등록</button>
    </div>
  );
};

export default React.memo(CreateUser); // React.memo 함수로 감싸준다.

추가적으로, React.memo 의 두번째 매개변수에 propsAreEqual 함수를 사용하여 특정 값들만 비교할 수도 있다.

export default React.memo(
  CreateUser,
  (prevProps, nextProps) => prevProps.username === nextProps.username
);

state 함수형 업데이트 하기

useCallback 을 사용하여 함수를 만들게 되면 states가 항상 최신값을 참조하도록 하기 위해 deps에 함수 내부에서 활용하는 state를 작성해 줘야 한다. 하지만 이것이 불필요한 함수 생성을 발생시킬 수 있는데, 가령 state의 변화가 발생하면 컴포넌트가 리렌더링 되며 함수 또한 다시 생성되게 된다. 이를 최적화 하기 위한 방법이 있는데 그것이 바로 state를 함수형으로 업데이트 하는 것이다.

함수형 업데이트를 하게 되면 setter 함수 내 콜백함수의 매개변수에서 항상 최신의 state를 참조할 수 있기 때문에 deps에서 state를 빼주어도 된다. 이제 함수 생성을 최적화 할 수 있게 되었다. 어떻게 변경할 수 있을지 코드를 통해 예시를 보자.

function App() {
  const [inputs, setInputs] = useState({
    username: "",
    email: "",
  });
  const { username, email } = inputs;

  const onChange = useCallback(
    (e) => {
      const { name, value } = e.target;
      setInputs({
        ...inputs,
        [name]: value,
      });
    },
    [inputs]
  );
  // ~~ 이하 생략

위와 같은 코드가 있다면, onChange 함수는 inputs state가 변경될 때마다 다시 생성되게 된다. 이를 함수형 업데이트를 통해 변경해 준다면 아래와 같은 코드로 변경될 것이다.

function App() {
  const [inputs, setInputs] = useState({
    username: "",
    email: "",
  });
  const { username, email } = inputs;

  const onChange = useCallback((e) => {
    const { name, value } = e.target;
    setInputs((inputs) => ({
      ...inputs,
      [name]: value,
    }));
  }, []);
  // ~~ 이하 생략

useCallback 을 활용한 함수들을 이렇게 변경해 준다면 특정 항목을 수정하면 해당 항목만 리렌더링이 일어나게 된다.

최적화 하기

어제부터 오늘까지 알아본 useMemo , useCallback , React.memo 함수들은 모두 최적화를 하기 위해 사용하는 함수였다. 이들을 컴포넌트의 성능을 실제로 개선할 수 있을 때만 사용해야 한다고 한다. 또 React.memo 의 경우 실제로 컴포넌트 렌더링 최적화를 하지 않을 경우에는 굳이 사용할 필요가 없다. 불필요하게 props의 비교를 발생시키기 때문이다.

내일의 나는 무엇을 해야할까?

  • velopert 모던 리액트 21~27강
  • 쿠키, 세션, 로컬 스토리지 학습
  • 팀 데일리 출제 및 답변
profile
개발자를 지망하는 경영학도

0개의 댓글