최적화를 해야하는 이유와 최적화 hooks

const job = '프론트엔드';·2023년 10월 6일
0
post-thumbnail

최적화?

웹 서비스의 성능을 개선하는 것

  • 유튜브를 켰다. 그런데, 로딩하는데만 5초, 10초 이상이 걸린다고 생각하면? '유튜브 고장났네...'싶고
  • 유튜브를 점점 떠나가겠지?
  • 엔지니어적으로나 서비스적으로나 최적화는 필수임

웹 서비스 성능을 개선하는 방법

  1. 코드 사이즈 줄이기
  2. 폰트 사이즈 줄이기
  3. 이미지 사이즈 줄이기
  4. 페이지 로딩 속도 빠르게 하기
    5. 불 필요한 연산 다시 수행하지 않게 하기

다시 연산하는 것을 막자(useMemo)

컴포넌트 내부에서 다시 불필요한 연산을 수행하지 않도록 하는 hook

  • 필요한 것 : 이전 todolist

계산이 필요한 새로운 함수를 만들기

  const getAnalyzedTodoData = () => {
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return {
      totalCount,
      doneCount,
      notDoneCount,
    };
  };
  • todos배열의 전체 길이가 totalCount(전체 갯수)
  • todos배열에서 filter로 todo의 상태가 isdone인 것의 길이를 구하면 doneCount(완료된 갯수)
  • totalCount에서 doneCount를 빼면, notDoneCount(미완료 갯수)
  const { totalCount, doneCount, notDoneCount } = getAnalyzedTodoData();
  • 객체 형태로 반환했기 때문에, 객체 형태로 받아옴
      <div>
        <div>전체 투두: {totalCount}</div>
        <div>완료 투두: {doneCount}</div>
        <div>미완 투두: {notDoneCount}</div>
      </div>

그렇다면 불필요한 호출이 있는지 확인

  • todolist가 마운트 될 때 정상적으로 호출
  • 새로운 todo를 추가할 때 정상적으로 호출
  • 그러니깐, todos 배열의 데이터가 변경될 때는 정상적으로 호출됨
  • 그런데 search bar에 입력을 해도 계속 불필요하게 호출됨

왜 호출되는 거지?

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

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

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

  const getAnalyzedTodoData = () => {
    console.log("Todo 분석 함수 호출");
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return {
      totalCount,
      doneCount,
      notDoneCount,
    };
  };

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

  return (
    <div className="TodoList">
      <h4>오늘의 할 일</h4>
      <div>
        <div>전체 투두: {totalCount}</div>
        <div>완료 투두: {doneCount}</div>
        <div>미완 투두: {notDoneCount}</div>
      </div>
      <input
        value={search}
        onChange={onChangeSearch}
        placeholder="검색어를 입력하세요"
      />
      <div className="todos_wrapper">
        {filterTodos().map((todo) => (
          <TodoItem
            key={todo.id}
            {...todo}
            onUpdate={onUpdate}
            onDelete={onDelete}
          />
        ))}
      </div>
    </div>
  );
};
  • 서치바에 새로운 text를 입력하게 되면, onChangeSearch가 실행 됨
  • 이벤트 핸들러 안에 있는 setSearch가 호출되고
  • TodoList컴포넌트에 있는 search의 상태가 업데이트 되기 때문
  • 즉, TodoList가 리렌더 되면서 함수가 다시 계속해서 호출되면서 업데이트 되는 상태가 되기 때문!

따라서, getAnalyzedTodoData() 함수는 todos의 데이터가 변경되지 않는 경우 다시 호출될 이유가 없음 !

const { 반환하는 값 } = useMemo(() => {

  조건에 충족될 경우에만 실행되는 함수

}, [조건])
  const { totalCount, doneCount, notDoneCount } = useMemo(() => {
    console.log("Todo 분석 함수 호출");
    const totalCount = todos.length;
    const doneCount = todos.filter((todo) => todo.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return {
      totalCount,
      doneCount,
      notDoneCount,
    };
  }, [todos]);

컴포넌트 리렌더링 방지!(React.memo)

react developer tool을 이용해서 불필요한 렌더가 있는지 확인

  • 새로운 todo를 추가했더니 Header 컴포넌트도 렌더가 됨
  • 그리고 원래 todos에 있던 항목들도 렌더가 됨
function App() {
  const [todos, dispatch] = useReducer(reducer, mockDate);
  
  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

  • App컴포넌트(부모) - Header컴포넌트, TodoList컴포넌트는 각각 자식
  • 새로운 todo를 추가하면, App컴포넌트의 todos 상태가 업데이트 되고, 부모컴포넌트인 App이 리렌더 되니까 Header컴포넌트도 리렌더 됨

Header 컴포넌트의 불필요한 상황에서 렌더를 막아보자 !

import "./Header.css";
import { memo } from "react";

const Header = () => {
  const options = {
    weekday: "long",
    year: "numeric",
    month: "long",
    day: "numeric",
  };
  const date = new Date().toLocaleDateString(undefined, options);

  return (
    <div className="Header">
      <h1>{date}</h1>
    </div>
  );
};

export default memo(Header);
  • export default memo(Header) 최적화된 컴포넌트를 반환

TodoItem 컴포넌트에도 React.memo를 적용해보자!

  • 적용후에도 다른 todos 항목들이 계속 깜빡임 적용되지 않은 상태임을 확인했음
  • 이것은 App컴포넌트에서 todos가 바뀌면서 리렌더 되는데 이때 onUpdate, onDelete 함수가 다시 호출되면서 렌더가 발생됨
  • 그런데, todoItem 컴포넌트는 prop을 통해 onUpdate, onDelete를 받고 있음
const TodoItem = ({ id, isDone, createdDate, content, onUpdate, onDelete }) => {
  
}
  • 즉, 원시자료형인 id, isDone, createDate, content는 다른 todos에 변화가 있더라도 변하지 않음
const mockDate = [
  {
    id: 0,
    isDone: false,
    content: "공부하기",
    createdDate: new Date().getTime(),
  },
 ]

cf. 참조자료형 중 함수는 변수를 저장할 때 그 값 그대로 저장하는 것이 아니라 참조값이 저장됨, 그러니까 새로 렌더링 될 때마다 함수에 저장된 참조값은 계속 변하는 형태

그렇다면, 이 경우 React.memo를 통해 근본적인 해결이 불가능하기 때문에 onUpdate, onDelete 함수의 재생성을 막아야 한다 !

onUpdate, onDelete 함수의 재생성을 막자(useCallback)

  const onUpdate = useCallback((targetId) => {
    dispatch({
      type: "UPDATE",
      data: targetId,
    });
  }, []);

  const onDelete = useCallback((targetId) => {
    dispatch({
      type: "DELETE",
      data: targetId,
    });
  }, []);
  • 이 경우, 의존성 배열의 내용이 변화할 때 첫번째 인수가 실행되도록 하게 하는데
  • 빈배열의 경우 최초 1회만 렌더하고, 그 이후에는 어떠한 경우에도 재생성을 막아줌

또 다른 문제점 TodoList 컴포넌트

  • TodoList는 사용하지 않으면서 onUpdate, onDelete를 App컴포넌트에게 받아 TodoItem으로 전달하는 역할만 하고 있음
  • 뎁스가 깊어지면, 골치아파짐
  • props drilling

Context를 이용해보자.

자식컴포넌트들에게 데이터를 직송으로 보내줄 수 있는 객체

import { createContext } from "react";

export const TodoContext = createContext();

  • TodoContext라는 새로운 컴포넌트를 만들어서 내보내줌
 <div className="App">
      <Header />
      <TodoContext.Provider value={(todos, onCreate, onUpdate, onDelete)}>
        <TodoEditor onCreate={onCreate} />
        <TodoList todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
      </TodoContext.Provider>
    </div>
  • 그리고 prop으로 내려받을 컴포넌트를 TodoContext.Provider로 감싸줌

  • 전달한 데이터를 value에 담아서 전달

  • 즉, 자식컴포넌트들은 Provider가 제공하는 데이터를 다이렉트로 받아서 사용할 수 있음

import { useContext } from "react";
import { TodoContext } from "../TodoContext";

const TodoItem = ({ id, isDone, createdDate, content }) => {
  const { onUpdate, onDelete } = useContext(TodoContext);
  
}
  • 받아오는 곳에서는 이런식으로 필요한 데이터만 받아오면 됨

TodoList 컴포넌트는 어떻게 됐나?

import { TodoContext } from "../TodoContext";

const TodoList = () => {
  const { todos } = useContext(TodoContext);
  
}
  • 이런식으로 필요한 todos만 받아오고 이전에 사용하지 않고 전달하기만 했던 onUpdate, onDelete는 신경쓰지 않아도 됨

또, 문제 발생

Context를 했더니, 또 항목들이 렌더됨(최적화가 풀린듯)

왜, 이런 문제가 발생하는가?

  • TodoContext.Provider도 결국 하나의 컴포넌트임
  • 따라서 해당 데이터가 변경되면 리렌더됨
  • 그러면 그 하위에 TodoEditor, TodoList, TodoItem이 다같이 리렌더 됨
  • 그런데, TodoItem에는 분명 React.memo를 통해 해당 컴포넌트가 변경되지 않는 이상 렌더되지 않게 최적화를 시켜둔 상태
  • App컴포넌트에서 새로 todo를 조작하면, App컴포넌트에 있는 todos가 변경되어 App컴포넌트 자체가 리렌더 되기 때문임
  • 이때, 전달하고 있는 객체를 다시 생성하게 됨
  • 그렇기 때문에 변화했다고 인지

이럴때는 TodoContext의 분리가 필요함


import { createContext } from "react";

export const TodoStateContext = createContext();
export const TodoDispatchContext = createContext();
  • 이렇게 TodoStateContext, TodoDispatchContext를 만들어줌
    <div className="App">
      <Header />
      <TodoStateContext.Provider value={todos}>
        <TodoDispatchContext.Provider value={{ onCreate, onUpdate, onDelete }}>
          <TodoEditor />
          <TodoList />
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  • App컴포넌트에서도 각각에 value값을 분리해서 전달
  • 단, onCreate, onUpdate, onDelete가 변경되지 않게 재생성되는 것을 막아주기 위해 useMemo로 최적화를 해줌
  const memoizedDispatches = useMemo(() => {
    return {
      onCreate,
      onUpdate,
      onDelete,
    };
  }, []);
<div className="App">
      <Header />
      <TodoStateContext.Provider value={todos}>
        <TodoDispatchContext.Provider value={memoizedDispatches}>
          <TodoEditor />
          <TodoList />
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>

그렇다면

  • 최적화, props drilling의 문제를 모두 해결했음
profile
`나는 ${job} 개발자`

0개의 댓글