Jint 2024. 1. 25. 23:05

컴포넌트 & 함수 재사용하기

 

1. 컴포넌트 & 함수 재사용
먼저 최적화할 컴포넌트 대상 찾아야 할 수 있어야 한다. React Developer Tools 의 Components - General : Highlight updates when components render 기능을 활용한다.
일기 삭제시 일기 입력폼 컴포넌트는 리렌더링 될 필요가 없다. DiaryEditor 컴포넌트를 최적화한다.

- DiaryEditor.js

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

const DiaryEditor = ({ onCreate }) => {

    // 렌더링 확인
    useEffect(() => {
        console.log('DiaryEditor 렌더');
    });

    // DOM Element 접근
    const authorInput = useRef();
    const contentInput = useRef();

    // state 기본값 설정
    const [state, setState] = useState({
        author: ""
      , content: ""
      , emotion: 1
    })

    // 이벤트 함수
    const handleChangeState = (e) => {
        setState({
            ...state
          , [e.target.name]: e.target.value
        })
    }

    // 저장 함수
    const handleSubmit = () => {
        if (state.author.length < 1) {
            // alert('작성자는 최소 1글자 이상 입력해주세요.');
            // focus
            authorInput.current.focus();
            return;
        }
        if (state.content.length < 5) {
            // alert('일기 본문은 최소 5글자 이상 입력해주세요.');
            // focus
            contentInput.current.focus();
            return;
        }
        onCreate(state.author, state.content, state.emotion);
        alert('저장 성공!');
        // 저장 후 일기장 입력폼 초기화
        setState({
            author: ''
          , content: ''
          , emotion: 1
        });
    }

    return (
        <div className="DiaryEditor">
            <h2>오늘의 일기</h2>
            <div>
                <input
                    ref={authorInput}
                    name="author"
                    value={state.author}
                    onChange={handleChangeState}
                />
            </div>
            <div>
                <textarea
                    ref={contentInput}
                    name="content"
                    value={state.content}
                    onChange={handleChangeState}
                />
            </div>
            <div>
                <select
                    name="emotion"
                    value={state.emotion}
                    onChange={handleChangeState}
                >
                    <option value={1}>1</option>
                    <option value={2}>2</option>
                    <option value={3}>3</option>
                    <option value={4}>4</option>
                    <option value={5}>5</option>
                </select>
            </div>
            <div>
                <button onClick={handleSubmit}>일기 저장하기</button>
            </div>
        </div>
    )
  
}

export default React.memo(DiaryEditor);

 

2번 렌더링 된 DiaryEditor 컴포넌트


2번 렌더링된 이유는 App.js 에서 확인 가능하다. data state 초기값이 빈 배열 '[]' 이어서 App 컴포넌트가 1번 렌더링이 일어나 DiaryEditor 에도 렌더링이 일어난다. 이후 컴포넌트가 Mount 된 시점에 호출한 getData() 함수에서 setData() 함수를 호출하여 data state 가 한 번 더 바뀌게 된다. 이 때 DiaryEditor 에도 렌더링이 일어난다.
DiaryEditor 컴포넌트가 전달받는 onCreate 함수도 App 컴포넌트가 렌더링되며 다시 생성된다. 비원시타입 비교는 React.memo 에서 얕은 비교로 일어나기 때문에, DiaryEditor 컴포넌트가 props 로 가진 onCreate 함수가 App 컴포넌트가 렌더링 될 때마다 다시 만들어질 때마다 다를 것이기 때문에 렌더링이 일어나게 된다.
결론적으로 onCreate 함수가 재생성되지 않아야만 DiaryEditor 컴포넌트를 React.memo() 와 함께 최적화할 수 있다. App 컴포넌트에서 onCreate 함수가 재생성되지 않도록 수정한다. useMemo() 를 사용하면 될 것 같으나, 함수가 아닌 값을 반환하기 떄문에 사용할 수 없다.
useCallback 을 사용해본다.

참고링크 : https://ko.legacy.reactjs.org/docs/hooks-reference.html#usecallback

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org


useCallback 은 메모이제이션된 콜백을 반환한다. 즉, 값을 반환하는 것이 아닌 첫 번째 매개변수의 Callback 함수 자체를 반환한다. 주의할 점은 메모이제이션된 Callback 함수를 반환한다는 점이다. 두 번째 매개변수인 Dependency Array 안에 들어있는 값이 변화하지 않으면, 첫 번째 인자로 전달된 Callback 함수를 계속 재사용할 수 있도록 도와주는 React Hook 이다.
App 컴포넌트의 onCreate 함수에 useCallback 을 적용하고, Dependency Array 를 빈 배열로 설정한다.

- App.js

...
// Mount 시점에 한 번만 만들고 재사용 되도록 함
const onCreate = useCallback((author, content, emotion) => {
  const created_date = new Date().getTime();
  const newItem = {
    author,
    content,
    emotion,
    created_date,
    id: dataId.current
  }
  dataId.current += 1;
  setData([newItem, ...data]); // 위에서 부터 출력하기 위해 newItem, ...data 순서로 작성
}, []);
...

 

onCreate 함수에 useCallback 적용 후 일기 작성 결과


일기 삭제시 DiaryEditor 가 리렌더 되지 않는다. 그러나 새로운 일기를 작성하자, 기존 일기가 모두 삭제되고 추가한 일기만 남았다. 왜냐 하면, Dependency Array 에 아무 값도 넣지 않았기 때문이다. onCreate 함수는 컴포넌트 Mount 시점에 한 번만 생성되는데, 그 당시 data state 값이 빈 배열이기 때문이다. 즉, onCreate 함수가 가장 마지막으로 생성되었을 때 data state 가 빈 배열이였기 때문이다.
함수는 컴포넌트가 재생성될 때 다시 생성되는 이유는, 현재의 state 값을 참조할 수 있어야하기 때문이다. onCreate 함수는 useCallback 에 갇혀서 Dependency Array 를 빈 배열로 전달했기 때문에 onCreate 가 알고있는 data 의 값은 그대로 빈 배열이다. 빈 배열에 newItem 을 추가했기 때문에 추가한 1개의 일기만 남게 되었다.
정상적으로 작동시키려면, Dependency Array 에 data state 를 넣어줘야 한다. 그러면 원하는 동작이 불가능하여 딜레마에 빠지지만, 이 때 함수형 Update 를 활용하면 된다. setData() 함수의 매개변수로 함수를 전달하는 것이다. 이 때, Dependency Array 를 비워도 항상 최신의 state 를 setData() 함수의 매개변수로 전달하는 함수의 매개변수 data 를 통해 참고할 수 있다.

- App.js

...
// Mount 시점에 한 번만 만들고 재사용 되도록 함
const onCreate = useCallback((author, content, emotion) => {
  const created_date = new Date().getTime();
  const newItem = {
    author,
    content,
    emotion,
    created_date,
    id: dataId.current
  }
  dataId.current += 1;
  // setData([newItem, ...data]); // 위에서 부터 출력하기 위해 newItem, ...data 순서로 작성
  setData((data) => [newItem, ...data]);
}, []);
...

 

useCallback 적용한 onCreate 함수 정상 실행 결과


새로고침 하면 개발자 도구 콘솔에도 DiaryEditor 컴포넌트가 1번만 렌더링 되었다. DiaryEditor 컴포넌트가 정상적으로 메모이제이션, 즉 최적화 되었다.


참고강의 : https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8#

 

한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 강의 - 인프런

개념부터 독특한 프로젝트까지 함께 다뤄보며 자바스크립트와 리액트를 이 강의로 한 번에 끝내요. 학습은 짧게, 응용은 길게 17시간 분량의 All-in-one 강의!, 리액트, 한 강의로 끝장낼 수 있어요.

www.inflearn.com