상세 컨텐츠

본문 제목

[React.js 1팀] 8장. 최적화

24-25/React.js 1

by mingging17 2025. 1. 10. 10:01

본문

728x90

 

 

 

 

 

 

 

 

 

 

1. 최적화와 메모이제이션

최적화: 웹서비스의 성능을 개선하는 기술 + 렌더링의 성능을 높이는 방법

-> 최적화가 잘된 웹서비스는 사용자를 불필요하게 기다리지 않게 하여 서비스의 긍정적인 경험을 만든다.

최적화 방법으로는 코드, 폰트, 이미지 파일의 크기를 줄이는 등의 여러 방법이 있다.

 

<리액트 최적화 방법 1: 연산 낭비를 줄이는 메모이제이션>

리액트는 '메모이제이션'으로 연산 최적화 한다.

메모이제이션: 특정 입력에 대한 결과를 계산해 메모리 어딘가에 저장했다가, 동일한 요청이 들어오면 저장한 결괏값을 제공해 빠르게 응답하는 기술이다.  (불필요한 연산을 줄여 실행속도가 빨라진다: 동적 계획법)

 

ex) 식당에서 메뉴를 외웠다가 누가 물어보면 답한다. 

 

 

 

2. 함수의 불필요한 재호출 방지하기

1. 할 일 분석 기능 추가하기

<완료, 미완료, 아이템 개수 출력>

// src/component/TodoList.js
import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";

const TodoList = ({ todo, onUpdate, onDelete }) => {
  const [search, setSearch] = useState(""); 
  const onChangeSearch = (e) => { 
    setSearch(e.target.value);
  };
  const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) =>
          it.content.toLowerCase().includes(search.toLowerCase())
        );
  };
  const analyzeTodo = () => {
    const totalCount = todo.length;
    const doneCount = todo.filter((it) => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return{
      totalCount,
      doneCount,
      notDoneCount,
    };
  };
  const {totalCount, doneCount, notDoneCount} = analyzeTodo();


  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <div>
        <div>총개수: {totalCount}</div>
        <div>완료된 할 일: {doneCount}</div>
        <div>아직 완료하지 못한 할 일: {notDoneCount}</div>
      </div>
      <input
        value={search} 
        onChange={onChangeSearch} 
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem
            key={it.id}
            {...it}
            onUpdate={onUpdate}
            onDelete={onDelete} 
          />
        ))}
      </div>
    </div>
  );
};
export default TodoList;

 

 

2. 문제점 파악하기

analyzeTodo는 todo에 저장한 아이템 개수에 비례하여 수행할 연산량이 증가한다. 따라서 todo에 아이템이 많아지면 성능상의 문제가 생길 수 있다.

 

<불필요한 호출 확인하기> 

// src/component/TodoList.js
(...)
  const analyzeTodo = () => {
    console.log("analyzeTodo 함수 호출");
    const totalCount = todo.length;
    const doneCount = todo.filter((it) => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return{
      totalCount,
      doneCount,
      notDoneCount,
    };
  };
  const {totalCount, doneCount, notDoneCount} = analyzeTodo();


  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <div>
        <div>총개수: {totalCount}</div>
        <div>완료된 할 일: {doneCount}</div>
        <div>아직 완료하지 못한 할 일: {notDoneCount}</div>
      </div>
      <input
        value={search} 
        onChange={onChangeSearch} 
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem
            key={it.id}
            {...it}
            onUpdate={onUpdate}
            onDelete={onDelete} 
          />
        ))}
      </div>
    </div>
  );
};
export default TodoList;

1: 컴포넌트 처음 마운트 5: 검색폼에 글자 입력할 때마다 리렌더

 

 

 

3. useMemo를 이용해 [할 일 관리] 앱 최적화하기

<useMemo 사용법>

useMemo를 사용하면 특정 함수를 호출했을 때 그 함수의 반환값을 기억한다. 같은 함수를 다시 호출하면 기억해 두었던 값을 반환합니다. 따라서 반환값을 다시 구할 필요 없다. (성능 최적화: 메모이제이션)

  • useMemo(콜백함수, 의존성 배열)

<함수 analyzeTodo의 재호출 방지하기>

// src/component/TodoList.js
import { useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";

const TodoList = ({ todo, onUpdate, onDelete }) => {
  const [search, setSearch] = useState(""); 
  const onChangeSearch = (e) => { 
    setSearch(e.target.value);
  };
  const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) =>
          it.content.toLowerCase().includes(search.toLowerCase())
        );
  };
  const analyzeTodo = useMemo(() => {
    console.log("analyzeTodo 함수 호출");
    const totalCount = todo.length;
    const doneCount = todo.filter((it) => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return{
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todo]);
  const {totalCount, doneCount, notDoneCount} = analyzeTodo;


  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <div>
        <div>총개수: {totalCount}</div>
        <div>완료된 할 일: {doneCount}</div>
        <div>아직 완료하지 못한 할 일: {notDoneCount}</div>
      </div>
      <input
        value={search} 
        onChange={onChangeSearch} 
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem
            key={it.id}
            {...it}
            onUpdate={onUpdate}
            onDelete={onDelete} 
          />
        ))}
      </div>
    </div>
  );
};
export default TodoList;

 

 

 

3. 불필요한 컴포넌트 리렌더 방지하기

< 리액트 최적화 방법2: React.memo>

react.memo를 이용해 메모이제이션 기법으로 불필요한 리렌터 방지

 

1. 고차 컴포넌트와 횡단 관심사

<고차 컴포넌트>

  • 컴포넌트의 기능을 다시 사용하기 위한 리액트의 고급 기술
  • 인수로 전달된 컴포넌트를 새로운 컴포넌트로 기능을 추가해 반환하는 함수 (강화된 컴포넌트)

 

<횡단 관심사>

  • 고차원 컴포넌트 이용하여 쉽게 해결한다. 
  • 크로스 커팅 관심사
  • 프로그래밍에서 비지니스 로직과 구분되는 공통 기능을 지칭할 때 사용
  • 비즈니스 로직: 컴포넌트가 존재하는 핵심 기능 표현할 때 사용
const CompA = () => {
	console.log("컴포넌트가 호출되었습니다.");
    return <div>CompA</div>;
};
const CompB = () => {
	console.log("컴포넌트가 호출되었습니다.");
    return <div>CompB</div>;
};
  • CompA, CompB는 공통으로 사용되는 횡단 관심사입니다. 
  • 프로그래밍에서 횡단 관심사는 로깅, 데이터베이스 접속, 인사 등에서 호출해 사용하는 코드입니다. 

비즈니스 로직을 세로로 배치한다고 했을 때, 여러 컴포넌트에서 공통적으로 사용하는 기능은 가로로 배치한다.

 

 

 

 

 

 

횡단관심사 코드는 중복코드를 만드는 요인이 된다. 

 

해결

function whileLifecycleLogging(WrappedComponent) {
  return (props) => {
    useEffect(() => {
      console.log("Mount!");
      return () => console.log("Unmount!");
    }, []);
    useEffect(() => {
      console.log("Update!");
    });
    return <wrappedComponent {...props} />;
  };
}

래핑된 컴포넌트: 고차 컴포넌트에 인수로 전달되는 컴포넌트

강화된 컴포넌트: 고차 컴포넌트가 반환하는 컴포넌트

 

 

 

2. React.memo를 이용해 [할 일 관리] 앱 최적화하기

react.memo는 코든 상황에서 리렌더 되지 않도록 강화함으로써 서비스를 최적화하는 도구

 

 

<React.memo 기본 사용법>

react.memo가 반환하는 컴포넌트는 Props가 변경되지 않는 한 리렌더 되지 않는다.

  • React.memo(메모이제이 시원하려는 컴포넌트)
  • 함수컨포넌트 선언과 동시에 메모이제이션
const CompA = React.memo(() => {
  console.log("컴포넌트가 호출되었습니다.");
  return <div>CompA</div>;
});

// Props의 변경 여부를 기준으로 리렌더
const Comp = ({a, b, c}) => {
  console.log("컴포넌트가 호출되었습니다.");
  return <div>CompA</div>;
};

function areEqual(prevProps, nextProps) {
  if (prevProps.a === nextProps.a) {
    return true;
  } else {
    return false;
  }
}
const MemoizesComp = React.memo(Comp, areEqual);

 

 

< Header 컴포넌트의 리렌더 방지하기>

Header 컴포넌트는 App(부모 컴포넌트)에서 아무런 Props도 받지 않는다. 

따라서 어떤 상황에서도 리렌더할 필요가 없다. 

// src/component/Header.js
import "./Header.css";
import React from "react";

const Header = () => {
  console.log("Header 업데이트");
  return (
    <div className="Header">
      <h3>오늘은 📅</h3>
      <h1>{new Date().toDateString()}</h1>
    </div>
  );
};
export default React.memo(Header);

 

한 번만 호출된 것을 볼 수 있습니다.

 

 

 

<TodoItemp 컴포넌트 리렌더 방지하기>

마운트 한번 그리고 나머지 0,1,2번 아이템 리렌더

 

아이템이 추가하는 상황, 아이템 제거, 체크박스 클릭, 검색폼에서의 검색에서도 TodoItem리렌더 됩니다. 

 

// src/component/TodoItem.js
import "./TodoItem.css";
import React from "react";

const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => { 
  console.log(`${id} TodoItem 업데이트`);
  const onChangeCheckbox = () => {
    onUpdate(id);
  };
  const onClickDelete = () => { 
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <div className="checkbox_col">
        <input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
      </div>
      <div className="title_col">{content}</div>
      <div className="date_col">
        {new Date(createdDate).toLocaleDateString()}
      </div>
      <div className="btn_col">
        <button onClick={onClickDelete}>삭제</button> 
      </div>
    </div>
  );
};
export default React.memo(TodoItem);

 

 

 

 

4. 불필요한 함수 재생성 방지하기

useCallBack: 컴포넌트가 리렌더 될 때 내부에 작성된 함수를 다시 생성하지 않도록 메모이제이션하는 리액트 훅

 

1. useCallBack을 이용해 [할 일 관리] 앱 최적화하기

App이 리렌더 될 때, useCallback으로 함수 onUpdate, onDelete를 재생성하지 않도록 만들어 TodoItem 컴포넌트의 렌더링 최적화. 

 

< useCallBack의 기본 사용법>

  • useCallback(콜백함수, 의존성 배열)
  • useCallback(콜백함수, []): 콜백함수를 어떠한 경우에도 다시 생성되지 않게 하려면 빈배열로 전달

< useCallBack과 함수형 업데이트>

인수로 전달되는 콜백함수에서 State변수에 접근하는 경우, 문제가 발생할 수 있습니다. 

const onCreate = useCallback(() => {
  setState([newItem, ...state]);
}, [])
// 빈배열이므로 컴포넌트 리렌더가 되지 않는다. 
// 즉, State는 초기값이 반환됩니다. 마운트할 때의 State값만 사용할 수 있습니다.
// State변화 추적 x -> 의도치 않은 동작 야기

const onCreate = useCallback(() => {
  setState([newItem, ...state]);
}, [State])
// useCallback을 적용한 의미가 없다.

const onCreate = useCallback(() => {
  setState((State) => [newItem, ...state]);
}, [])
// 함수형 업데이트 사용가능
// 항상 최신 State값을 매개변수로 저장합니다. (콜백함수가 반환한 값은 새로운 State값이 되어 업데이트 됩니다.)

useCallback을 사용하면서 setState로 최신 State값을 추적하려면 함수형 업데이트 기능을 이용해야 합니다. 

 

 

< useCallBack을 이용해 TodoItem 컴포넌트의 리렌더 방지하기>

// src/App.js
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useCallback, useReducer, useRef } from "react";


const mockTodo = [ 
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    createdDate: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래 널기",
    createdDate: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    createdDate: new Date().getTime(),
  },
];

function reducer(state, action) {
  switch (action.type){
    case "CREATE":{
      return [action.newItem, ...state]
    }
    case "UPDATE": {
      return state.map((it) =>
        it.id === action.targetId
          ?{
            ...it,
            isDone: !it.isDone,
          }
        : it
      );
    }
    case "DELETE":{
      return state.filter((it) => it.id !== action.targetId);
    }
    default:
      return state;
  }
}

function App() {

  const [todo, dispatch] = useReducer(reducer, mockTodo);
  const idRef = useRef(3);

  const onCreate = (content) => {
    dispatch({
      type: "CREATE",
      newItem: {
        id: idRef.current,
        content,
        isDone: false,
        createdDate: new Date().getTime(),
      },
    })
    idRef.current += 1; 
  };
  const onUpdate = useCallback((targetId) => { 
    dispatch({
      type: "UPDATE",
      targetId,
    });
},[]);
  const onDelete = useCallback((targetId) => { 
    dispatch({
      type: "DELETE",
      targetId,
    });
  },[]);

  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} /> 
    </div>
  );
}
export default App;
  • useReducer가 반환하는 함수 dispatch는 함수 reducer를 호출하는데, 이 reducer는 항상 최신 State를 인수로 받습니다. 따라서 reducer을 이용할 때는 함수형 업데이트를 사용하지 않아도 됩니다. 
  • TodoItem컴포넌트에 함수로 전달되는 Props인 onUpdate와 onDelete만 다시 생성하지 않도록 useCallback으로 최적화합니다. 

 

 

 

2. 최적화할 때 유의할 점

  • 최적화는 항상 마지막에 하세요: 개발이 끝나고 가장 마지막으로 진행하는 작업
  • 모든 것을 최적화할 필요는 없습니다: 사소한 연산이나 리렌더까지 최적화할 필요 없다. 
  • 컴포넌트 구조를 잘 설계했는데 다시 한번 돌아보세요: 하나의 컴포넌트의 여러 State 생성은 비효율적이다. 컴포넌트를 역할이나 기능으로 잘 분리했는지 먼저 확인하자.
  • 최적화는 여기서 끝나지 않습니다: 매우 다양한 최적화 방법들이 있다 (포트 최적화, 번들 사이즈 최적화)

 

 

 

 

 

 

 

 

 


출처 :  이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).

Corner React.js 1

Editor: MARIN

728x90

관련글 더보기