Home [REACT] 최적화
Post
Cancel

[REACT] 최적화

최적화

1. 최적화

  1. 최적화(Optimization)
    • 웹 서비스의 성능을 개선하는 모든 행위
    • 아주 단순한 것부터 어려운 방법까지 다양힘
  2. 일반적인 웹 서비스 최적화 방법
    • 서버 응답속도 개선
    • 이미지, 폰트, 코드파일 등 정적 파일 로딩 개선
    • 불필요한 네트워크 요청 축소
  3. React App 내부의 최적화 방법
    • 컴포넌트 내부의 불필요한 연산 방지
    • 컴포넌트 내부의 불필요한 함수 재생성 방지
    • 컴포넌트의 불필요한 리렌더링 방지

2. useMemo - 불필요한 연산 방지

  1. useMemo란?
    • 메모이제이션 기법을 기반으로 불필요한 연산을 최적화하는 훅. (자매품: useCallback)
      • Memoization: 컴퓨터 프로그램이 동일한 계산을 반복적으로 할때, 이전에 계산한 값을 메모리에 저장하여 중복적인 계산을 제거하여 전체적인 실행속도를 빠르게 하는 기법.
    • 특정 조건이 만족했을때만 실행시킬 수 있다
  2. useMemo 실습
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// List.jsc
import './css/List.css';
import TodoItem from "./TodoItem";
import { useState } from "react"; 

const List = ({ todos, onUpdate, onDelete }) => {
  const [search, setSearch] = useState("");

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  }

  const getFilteredData = () => {
    if (search === "") {
        return todos;
    }
    return todos.filter((todo) => todo.content.toLowerCase().includes(search.toLowerCase()));
  }

  const filteredData = getFilteredData();

  // 현재 상태를 분석하여 수치로 제공하는 함수
  const getAnalyzedData = () => {
    const totalCount = todos.length;
    const doneCount = todos.filter(todo => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;

    return {
      totalCount
      , doneCount
      , notDoneCount
    }
  }

  const {totalCount, doneCount, notDoneCount} = getAnalyzedData();

  return (
    <div className='List'>
        <h4>Todo List 🌱</h4>
        <div>
            <div>total: {totalCount}</div>
            <div>done: {doneCount}</div>
            <div>notDone: {notDoneCount}</div>
        </div>
        <input value={search} onChange={onChangeSearch} placeholder='검색어를 입력하세요' />
        <div className='todos_wrapper'>
            {filteredData.map((todo) => {
                return <TodoItem key={todo.id} {...todo} onUpdate={onUpdate} onDelete={onDelete} />;
            })}
        </div>
    </div>
  );
};

export default List;
  • 먼저 현재 투두리스트의 수치를 계산하는 getAnalyzedData 함수를 만들었다
  • 해당 함수는 리스트가 길어질수록 filter 메서드가 state의 배열 내의 모든 요소를 순회하기때문에 많은 부하가 생길것이다
  • 현재는 해당 함수는 리스트 컴포넌트 내부에 있기때문에 리스트 컴포넌트가 리렌더링될때마다 계속 호출된다.
  • 해당 함수가 불필요하게 호출되는걸 방지해야한다
1
useMemo(() => {}, [])
  • 첫번째 인수 : 콜백함수
  • 두번째 인수 : 의존성 배열
    • useEffect와 같이 의존성배열에 들어간 변수의 값이 변경되면 콜백함수가 실행된다.
  • 추가로, useEffect와 달리 useMemo는 콜백함수가 반환한 값을 그대로 다시 반환한다.
    • 결과값을 받아 사용이 가능하다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import './css/List.css';
import TodoItem from "./TodoItem";
import { useState, useMemo } from "react"; 

const List = ({ todos, onUpdate, onDelete }) => {
  const [search, setSearch] = useState("");

  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  }

  const getFilteredData = () => {
    if (search === "") {
        return todos;
    }
    return todos.filter((todo) => todo.content.toLowerCase().includes(search.toLowerCase()));
  }

  const filteredData = getFilteredData();

  const {totalCount, doneCount, notDoneCount} = useMemo(() => {
    console.log("getAnalyzedData 호출");
    const totalCount = todos.length;
    const doneCount = todos.filter(todo => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;

    return {
      totalCount
      , doneCount
      , notDoneCount
    }
  }, [todos]);

  return (
    <div className='List'>
        <h4>Todo List 🌱</h4>
        <div>
            <div>total: {totalCount}</div>
            <div>done: {doneCount}</div>
            <div>notDone: {notDoneCount}</div>
        </div>
        <input value={search} onChange={onChangeSearch} placeholder='검색어를 입력하세요' />
        <div className='todos_wrapper'>
            {filteredData.map((todo) => {
                return <TodoItem key={todo.id} {...todo} onUpdate={onUpdate} onDelete={onDelete} />;
            })}
        </div>
    </div>
  );
};

export default List;

3. Reat.memo - 불필요한 리렌더링 방지

  1. React.memo
    • const MemoizedComponent = memo(Component)
    • 리액트의 내장메서드
    • 인수로 리액트의 컴포넌트를 받아 해당 컴포넌트의 최적화 기능을 추가하여 결과값으로 반환한다.
      • 반환된 컴포넌트는 Props를 기준으로 메모이제이션된다.
      • 즉, 반환된 컴포넌트는 부모컴포넌트가 리렌더링되더라도 자신이 받는 props가 바뀌지 않으면 리렌더링이 발생하지 않도록 최적화가 이루어지는것
  2. 불필요하게 리렌더링되는 컴포넌트 확인하기
    • 개발자도구 -> 컴포넌트 -> 톱니바퀴 -> Highlight updates when components render 옵션 체크
  3. 예외경우
    • Props로 객체타입을 받을 경우, memo 메서드는 얕은 비교를 하기때문에 주소값을 비교하게 됨.
    • 부모 컴포넌트가 리렌더링될때마다 내부 함수 및 객체 변수들도 모두 같은것처럼 보이지만 새로 생성되고 있기 때문에 주소값이 변경되어 memo(Component) 만으론 의도대로 동작하지 않음
    • 이를 방지하기 위해서는 두가지 방법이 있음
      • 내부 함수 및 객체타입이 재생성되지 않도록 방지 - useCallback
      • memo의 두번째인수로 콜백함수를 넣어 커스텀 진행
  • 컴포넌트에 메모이제이션 등의 기능이 추가된 새로운 컴포넌트를 반환해주는 메서드를 리액트에서는 고차 함수 컴포넌트 (Higher Order Component. HOC) 라고 부름.

4. useCallback - 불필요한 함수 재생성 방지

  1. useCallback
1
const func = useCallback(fn, deps)
  • fn: 반환함수
  • deps: 의존성배열
  • deps 가 변화하면 새로운 fn을 반환한다

5. 최적화?

  1. 최적화 타이밍
    • 최적화가 너무 이르거나 너무 많아도 문제가 발생할 수 있음
      • 이미 최적화가 되어있는 기능을 수정하게되면 최적화가 풀리게되거나 고장나게 되어버리는 경우가 생김
      • 기능구현이 끝나고 프로젝트 마지막에 최적화 하는걸 권장함
  2. 최적화 대상
    • 모든것을에 최적화하는것은 안됨.
    • 꼭 최적화가 필요할 것 같은 함수, 컴포넌트에만 최적화를 적용하는 것이 좋음
      • 예시
    • React.memo에도 연산이 필요함. props 비교, 메모리에 컴포넌트 값 보관 등,..
    • 근데 헤더 컴포넌트와 같은 사소한 컴포넌트에 사용하게 되면 차라리 리렌더링이 더 빠를 수 있다.
    • TodoItem 컴포넌트와 같이 유저의 행동에 따라 개수가 많아질 수 있거나, 함수를 많이 갖고있어 코드가 무거운 컴포넌트에 한해서 최적화를 수행하는 것을 권장함
This post is licensed under CC BY 4.0 by the author.