본문 바로가기
Recap/bookshelf

[리팩토링 #03.] useMemo()로 리팩토링하여 성능 최적화하기

by yerin.dev 2024. 2. 28.

useMemo()란?

 

 

컴포넌트 내에 함수가 있다면, 컴포넌트가 렌더링 될 때마다 함수는 계속 다시 호출된다. 불필요한 함수 호출을 막기 위해 useMemo를 쓸 수 있는데, useEffect와 사용방법이 비슷하다. 첫 번째 파라미터로 계산하고 싶은 그 함수를 넣어주고, 두 번째 파라미터로 의존성 배열을 넣어준다. 따라서 의존성 배열 내에 들어있는 값이 변경될 때에만 재계산(호출)되고 그렇지 않으면 cache, memoize 된다. (재계산을 하지 않는다.)

 

 

import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
}

 

 

 

useMemo()로 기존 코드 리팩토링하기

 

 

bookshelf 프로젝트의 코드에 useMemo를 적용해서 성능 최적화를 해보았다.

 

 

1.

// 기존 BookShelfPage.tsx

const BookShelfPage = () => {
// ...생략

const allTags: HashTags = [];
  bookList.forEach((book) => {
    if (book.hashtags) allTags.push(...book.hashtags);
  });
  allTags = [...new Set(allTags)];

const sortedAndTaggedBooks =
    sortedBooks.filter((book) => book.hashtags?.includes(selectedTag!))
      .length == 0
      ? sortedBooks
      : sortedBooks.filter((book) => book.hashtags?.includes(selectedTag!));

// ...생략

return // ...
}

 

 

기존 코드는 BookShelfPage 컴포넌트가 렌더링될 때마다 sortedBooks가 새롭게 만들어지게 된다는 문제가 있다.

 

 

// 코드 리팩토링 후 BookShelfPage.tsx
const BookShelfPage = () => {
    const allTags = useMemo(() => {
    const tags: HashTags = [];
    bookList.forEach((book) => {
      if (book.hashtags) tags.push(...book.hashtags);
    });

    return [...new Set(tags)];
  }, [bookList]);

  const sortedAndTaggedBooks = useMemo(
    () =>
      sortedBooks.filter((book) => book.hashtags?.includes(selectedTag!))
        .length == 0
        ? sortedBooks
        : sortedBooks.filter((book) => book.hashtags?.includes(selectedTag!)),
    [selectedTag, sortedBooks]
  );

 

 

selectedTag, sortedBooks가 달라질 때만 재계산되고, 그렇지 않을 경우에는 memoized된 값을 사용한다.

 

 

2.

// 기존 useSort.tsx
export default function useSort() {
  const bookList = useRecoilValue(booksState);
  const [sortBy, setSortBy] = useState<SortOptions>("createdAt");
  const [sortedBooks, setSortedBooks] = useState(
      [...(bookList || [])].sort(
        (x, y) => y.createdAt.seconds - x.createdAt.seconds
      )
    );

    useEffect(() => {
      const newBooks = [...(bookList || [])].sort((x, y) => {
        if (sortBy === "createdAt") {
          return x.createdAt.seconds - y.createdAt.seconds;
        }
        if (sortBy === "author") {
          return x.author > y.author ? 1 : x.author < y.author ? -1 : 0;
        }
        if (sortBy === "title") {
          return x.title > y.title ? 1 : x.title < y.title ? -1 : 0;
        }
        if (sortBy === "rating" && x.rating && y.rating) {
          return x.rating > y.rating ? -1 : x.rating < y.rating ? 1 : 0;
        }
        return 0;
      });
      setSortedBooks(newBooks);
    }, [bookList, sortBy]);

    return { sortedBooks, setSortBy };
    }

 

 

지금 보니 굉장히 불필요한 state들의 향연... 원하는 대로 동작만 하게 하자! 가 목표였기 때문에 코드는 💩일 수 밖에 없었다.
이 코드에서도 마찬가지. 하하..
지금은 다 derived state를 이용해서 쓸 데 없는 state들은 모두 없애주었다.

아무튼 내 책장에 추가한 모든 책인 bookList가 있고, 이 책들을 sort해서 보여주고 싶은데,
처음에 쓴 코드에서는 따로 sortedBooks라는 state를 만들고, sortBy(선택한 분류기준)이 바뀔 때마다 sortedBooks가 업데이트 되도록 했다.

 

 

// useMemo로 리팩토링 후 useSort()

export default function useSort() {
  const bookList = useRecoilValue(booksState);
  const [sortBy, setSortBy] = useState<SortOptions>("createdAt");

  const sortedBooks = useMemo(
    () =>
      [...bookList].sort((x, y) => {
        if (sortBy === "author") {
          return x.author > y.author ? 1 : x.author < y.author ? -1 : 0;
        } else if (sortBy === "title") {
          return x.title > y.title ? 1 : x.title < y.title ? -1 : 0;
        } else if (sortBy === "rating" && x.rating && y.rating) {
          return x.rating > y.rating ? -1 : x.rating < y.rating ? 1 : 0;
        } else return x.createdAt.seconds - y.createdAt.seconds;
      }),
    [bookList, sortBy]
  );

  return { sortedBooks, setSortBy };
}