본문 바로가기
fastcampus

[패스트캠퍼스 수강 후기] 프론트엔드 인강 100% 환급 챌린지 23회차 미션

by 새우하이 2020. 9. 29.

useCallback Hook

이전에 만들었던 함수를 새로 만들지 않고 재사용하는 방법을 알아보도록한다.
App.js 컴포넌트에 구현 했었던 onRemove, onToggle, onCreate 등을 보면 컴포넌트가 매번 리렌더링 될 때마다 새로운 함수를 만든다. 그런데 이렇게 함수를 새로 만드는것 자체가 메모리나 cpu를 많이 사용하지 않아서 부하를 일으키지는 않지만, 한번 만든 함수를 재사용할 수 있으면 재사용하는것이 좋다. 우리가 나중에 CreateUser나 UserList 컴포넌트들이 props가 바뀌지 않았다면 virtual DOM에 하는 리렌더링 조차 안하게끔 만들어 줄 수 있다. 그냥 이전에 만들어 놨던 결과물을 재사용 할 수 있게 만들 수 있다.

우리가 useMemo를 통해 countActiveUsers의 연산된 값을 재사용하는 작업을 했듯, useCallback도 마찬가지로 함수를 재사용 할 때 사용하고 deps도 넣어줘야한다.

// App.js
import React,{ useRef, useState, useMemo, useCallback } from 'react';

useCallback을 불러와주고 기존에 선언했던 함수들을 감싸주기만 하면된다.

// App.js
const onChange = useCallback(e =>{
    const {name, value} = e.target;
    setInputs({
      ...inputs,
      [name] : value
    });
  },[inputs]);
.
.

우선 onChange를 useCallback 으로 감싸주고 내부에서 의존하고 있는 값들을 살펴본다. ...inputs (useState 를통해서 관리하고 있는 상태) 를 사용하고 있다.
따라서 deps 배열에 inputs를 넣어준다.
그러면 onChange는 inputs가 바뀔 때만 새로 생성되고 그렇지 않으면 기존의 함수가 재사용된다.

onCreate도 마찬가지로 useCallback으로 감싸주고

// App.js
const onCreate = useCallback(() => {
  const user ={
    id : nextId.current,
    username,
    email,
  };

  setUsers(users.concat(user));
  setInputs({
    username:'',
    email:'',
  });

  console.log(nextId.current);
  nextId.current += 1;
},[username,email,users]);

onCreate에서 참조하고있는 username, email, users를 deps배열에 추가해준다.
만약 deps를 넣는것 잊는다면 함수 내부에서 해당 상태들을 참조하게 될 때 가장 최신상태를 참조하는것이 아니라 컴포넌트가 처음 만들어질때 (옛날 상태) 를 참조하게되는 의도치않은 현상이 나타날 수 있다.

// App.js
const onRemove = useCallback((id) =>{
  setUsers(users.filter(user => user.id !== id));
},[users]);
const onToggle = useCallback(id =>{
  setUsers(users.map(
    user => user.id === id
    ? { ...user, active: !user.active}
    :user
  ));
},[users]);

onRemove 와 onToggle도 마찬가지로 useCallback으로 감싸주고, deps 배열에 users를 추가해준다.
그리고 이렇게 한다고 해서 눈에 띄는 최적화는 없다. 나중에 컴포넌트리렌더링 성능 최적화 작업을 해줘야만 성능이 좋아진다.
하지만 그작업을 하기전에 어떤 컴포넌트가 현재 리렌더링 되는지 알기위해서
react devtools 를통해서 확인해보자.
chrome web store에서 React Developer Tools 를 다운로드 받으면된다.

크롬 개발자 도구탭에서 >>를 눌러보면 react나 component 등 리액트와 관련된 새로운 탭이 추가된 것을 확인할 수 있다. 이것을 눌러보면 현재 리액트 컴포넌트들이 어떻게 구성되어있는지 볼 수 있다. 특정 컴포넌트를 선택할 수도 있다.

컴포넌트 탭에서 톱니바퀴를 눌러보면 highlight update 블라블라 하는 체크박스를 체크해주고 개발자탭의 App 컴포넌트 부분을 클릭한 뒤 , 계정명 부분에 텍스트를 입력해보면 텍스트 입력과 동시에 렌더링 되는것을 볼 수 있는데, 이것이 지금 당장은 느려지거나 하진 않겠지만 렌더링 되어야할 것이 많을경우에는 속도에 영향을 미칠 수 있다.

React.memo

memo라는 함수를 사용해서 컴포넌트에서 리렌더링이 불필요할 때는 이전의 렌더링 결과를 재사용할 수 있게 하는 방법을 알아보자.

우선 CreateUser 컴포넌트를 열어준다.
그리고 export 부분에

// CreateUser.js
export default React.memo(CreateUser);

CreateUser를 React.memo로 감싸주기만 하면된다.
React.memo를 사용하면 props가 바뀌었을 때만 리렌더링 해준다.

UserList 도 마찬가지이다.

// UserList.js
export default React.memo(UserList);

UserList에 있는 User컴포넌트의 경우에는

// UserList.js
const User = React.memo(function User({user, onRemove, onToggle}) {
    const {username, email, id, active} = user;

    return(
        <div>
        <b style={{
            color: active ? 'green': 'black',
            cursor:'pointer'
        }} onClick={() => onToggle(id)}>{username}</b>&nbsp;<span>({email})</span>
        <button onClick={() => onRemove(id)}>삭제</button> 
        </div>
    )
});

User function을 User 변수에 담으면서 이 function을 React.memo로 감싸준다.

// UserList.js 전체 파일
import React,{ useEffect } from 'react';

const User = React.memo(function User({user, onRemove, onToggle}) {
    const {username, email, id, active} = user;

    return(
        <div>
        <b style={{
            color: active ? 'green': 'black',
            cursor:'pointer'
        }} onClick={() => onToggle(id)}>{username}</b>&nbsp;<span>({email})</span>
        <button onClick={() => onRemove(id)}>삭제</button> 
        </div>
    );
});
function UserList({users, onRemove, onToggle}){


    return(
        <div>
            {
            users.map(
                user => (<User user={user} key={user.id} onRemove={onRemove} onToggle={onToggle}/>) 
            )
            }
        </div>
    )
};

export default React.memo(UserList);

이렇게 하면 어느정도 최적화가 된다.
이제 실행환경에서 계정명이나 이메일 같은 input에 새로운 문자를 기입하면
전체 컴포넌트가 리렌더링 되는것이 아니라 해당 컴포넌트만 리렌더링 되는것을 확인 할 수 있고. 활성 사용자수 active 부분도 마찬가지로 동작한다.
그런데 onToggle 이나 onRemove 를 보면
users가 deps에 있다. 따라서 users가 바뀌면 onRemove나 onToggle이 리렌더링 되는것이고 User입장에서도 onRemove와 onToggle이 바뀌었으니 리렌더링 해야한다.
이것을 해결하려면 onToggle, onRemove 등 함수에서 기존 users를 참조하면 안된다.그 대신 다른 방법으로 useState의 함수형 업데이트 이다.

// App.js
const onCreate = useCallback(() => {
  const user ={
    id : nextId.current,
    username,
    email,
  };

  setUsers(users => users.concat(user));
  setInputs({
    username:'',
    email:'',
  });

  console.log(nextId.current);
  nextId.current += 1;
},[username,email]);

App 컴포넌트의 deps에서 users를 지우고
setUsers에 users => 로 setUsers의 callback함수의 파라미터에서 최신 users를 줘야하기 때문에 굳이 deps에 users를 넣지 않아도된다.
username과 email이 바뀔 때에만 새로 생성된다.

// App.js
const onRemove = useCallback((id) =>{
  setUsers(users => users.filter(user => user.id !== id));
},[]);
const onToggle = useCallback(id =>{
  setUsers(users => users.map(
    user => user.id === id
    ? { ...user, active: !user.active}
    :user
  ));
},[]);

onRemove와 onToggle 에도 마찬가지로 적용해주면
두 함수는 컴포넌트가 처음 만들어질 때만 만들어지고 그 이후로는 재사용되는것이다.

추가적으로 React.memo를 사용할 때 두번째 파라미터로 propsAreEqual 이라는 함수를 넣어줄 수 있는데 설명을 보면 preveProps와 nextProps를 비교해서 true를 반환하면 리렌더링을 방지하고 false 때 리렌더링 하게된다.
현재 UserList에서는 onRemove와 onToggle이 useCallback때문에 안바뀔 것이라는 걸 잘 알기 때문에

// UserList.js
export default React.memo(UserList, (prevProps, nextProps)=> nextProps.users === prevProps.users);

prevProps와 nextProps를 가져와서 두개 가같다면 리렌더링 하지 않겠다고 선언해 볼 수 있겠다.
이렇게 propsAreEqual 함수를 사용할 때에는 나머지 props가 정말 고정적이어서 비교할 필요가 없는지를 꼭 확인해줘야 한다.

정리 : 연산된 값을 재사용하기 위해서 userMemo를 사용하고 특정 함수를 재사용하기 위해 useCallback을 사용하고 컴포넌트 렌더링된 결과를 재사용하기 위해서는 React.memo를 사용한다.

 

해당 내용은 아래 링크에서 수강할 수 있다.

프론트엔드 개발 올인원 패키지 with React Online. 👉https://bit.ly/2ETLEzm

 

프론트엔드 개발 올인원 패키지 with React Online. | 패스트캠퍼스

성인 교육 서비스 기업, 패스트캠퍼스는 개인과 조직의 실질적인 '업(業)'의 성장을 돕고자 모든 종류의 교육 콘텐츠 서비스를 제공하는 대한민국 No. 1 교육 서비스 회사입니다.

www.fastcampus.co.kr



댓글