TIL DAY.49 [React] useMemo & useCallback

Dan·2021년 4월 18일
0

리액트

목록 보기
16/17
post-thumbnail

회사에서 프로젝트를 진행하며 중첩필터를 구현하는 과정에서 처음에는 별거 아닌 것처럼 느껴졌지만 프로젝트가 커질수록 실제로 메모리를 많이 잡고, api요청을 중첩해서 보내는 현상이 일어났다.

여기서 말하는 중첩 필터란,
1. api요청을 통해 필터의 리스트를 받아온다.
2. 받아온 리스트에서 원하는 값을 눌러 값을 보내 또 다른 리스트를 받아오는 구조이다.

내가 현재 구현하고 있는 사이트가 작동하고 있는 방식은
1. 리그 리스트 요청, 값 선택
2. 선택한 값을 갖고 패치 리스트 요청 후 값 선택
3. 리그,패치 값을 가지고 팀 리스트 요청, 값 선택
4. 리그,패치,팀 값을 가지고 플레이어 리스트를 요청 , 값 선택

이렇게 한 필터박스에서 총 4번의 요청을 걸쳐 4개의 값을 저장하게 된다.

물론 이 방식으로 구현하면 기능상으론 아무 문제 없다. 하지만 우연히 백엔드의 콘솔창을 보게 되었을 때 내가 구현한 방식의 심각한 오류를 발견했다.

문제점

React 같은 경우에는 먼저 컴포넌트를 렌더링 한 뒤에 이전 렌더된 결과와 비교하여 DOM의 업데이트를 결정한다. 만약 렌더 결과가 이전과 다르면 React는 DOM을 업데이트 한다.
그래서 내가 처음 리그를 요청할 땐 문제는 없지만, 패치를 요청할 때부터 리그(props)값이 바뀌지 않았는데도 불구하고 리그를 한번 더 요청하는 것을 볼 수 있었다.
즉, 플레이어 리스트를 요청하게 되면 리그,패치,팀,플레이어 총 4번의 요청을 보내는 것을 확인 할 수 있었다.

이러한 불필요한 리렌더링을 막을 수 있는 방법은 없을까 하고 찾아보다가 두 가지 유용한 react hooks를 알게 되었다.

아래 내용부터는 react 공식문서와 다양한 블로그를 토대로 정리한 글입니다.

useMemo

useMemo같은 경우에는 메모이제이션된 값을 반환합니다.
여기서 메모이제이션이란 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 즉 useMemo는 불필요한 리렌더링을 방지해서 성능을 최적화할 때 쓰이는 도구라고 볼 수 있다.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

위의 코드를 보면 a,b 값이 변할 때만 첫번째 인자로 들어온 함수가 실행되어 재계산이 되고, 그렇지 않을 경우에는 메모이즈된 값을 return 하게 되는 방식입니다.

위처럼 useMemo를 사용하면 의존하는 값이 변경될 때에만 연산하므로 최적화가 개선됩니다.

useCallback

useCallback또한 최적화를 위한 훅입니다.
하지만 useMemo와 차이점은 메모이제이션 된 함수를 반환한다라는 것입니다.첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열 내의 값이 변경될 때까지 저장해놓고 재사용할 수 있게 해줍니다.

const memoizedCallback = useCallback(함수, 배열)

예를 들어, 어떤 React 컴포넌트 함수 안에 함수가 선언 되어 있다면 이 함수는 해당 컴포넌트가 랜더링 될 때 마다 새로운 함수가 생성될겁니다. 하지만 useCallback()을 사용하면, 해당 컴포넌트가 랜더링되더라도 그 함수가 의존하는 값들이 바뀌지 않은 한 기존 함수를 계속해서 반환해줍니다.

const add = useCallback(() => x + y, [x, y])

즉, x 또는 y값이 바뀌면 새로운 함수가 생성되어 add변수에 할당되고, x와 y값이 동일하다면 다음 랜더링 때 이 함수를 재사용하게됩니다.
하지만 위와 같은 간단한 계산같은 경우에는 컴포넌트가 랜더링될 때마다 함수를 새로 선언하는 것은 성능에 거의 영향을 미치지 않는 수준이라 usecallback을 사용하는 것은 큰의미가 없거나 오히려 손해인 경우가 됩니다.

그럼 useCallback은 언제 사용해야 할까?

다음 코드를 보면서 같이 이해해보도록 하겠습니다.

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

function Profile({ userId }) {
  const [user, setUser] = useState(null)

  const fetchUser = () =>
    fetch(`https://your-api.com/users/${userId}`)
      .then((response) => response.json())
      .then(({ user }) => user)

  useEffect(() => {
    fetchUser().then((user) => setUser(user))
  }, [fetchUser])

  
}

위의 코드를 보면 컴포넌트에서 API를 호출하는코드는 fetchUser함수가 변경될 때마다 호출되도록 구현했습니다. featchUser는 함수이기 때문에, userId 값이 바뀌든 말든 컴포넌트가 랜더링될 때 마다 새로운 참조값으로 변경됩니다. 그러면 useEffect() 함수가 호출되어 user 상태값이 바뀌고 그러면 다시 랜더링이 되고 그럼 또 다시 userEffect()함수가 호출되 예상치 못한 무한루프를 발생시키게 됩니다. 위와 같은 상황에서 useCallback hook함수를 사용해봅시다.

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

function Profile({ userId }) {
  const [user, setUser] = useState(null)

  const fetchUser = useCallback(
    () =>
      fetch(`https://your-api.com/users/${userId}`)
        .then((response) => response.json())
        .then(({ user }) => user),
    [userId]
  )

  useEffect(() => {
    fetchUser().then((user) => setUser(user))
  }, [fetchUser])

  // ...
}

컴포넌트가 다시 랜더링되더라도 fetchUser함수의 참조값을 동일하게 유지시킬 수 있습니다. 따라서 의도했던 대로, useEffect()에 넘어온 함수는 userId값이 변경되지 않는 한 재호출 되지 않게 됩니다.

마치며..

useMemo는 상태값을 반환, useCallback은 함수를 반환하는 차이를 제외하곤 없다고 할 수 있다. 이를 적절하게 사용하면 최적화에 큰 도움을 줄 수 있지만, 메모제이션용 메모리가 추가적으로 들기 때문에 무분별하게 사용하는 것은 오히려 독이 될수도 있다. 위와 같은 도구를 꼭 필요한 곳에 쓸 줄 아는 개발자가 되기위해서는 앞으로 연습이 많이 필요하겠지만, 언젠간 깨우침이 있을거라 본다.

오늘은 오랜만에 회사에서 프로젝트를 진행하면서 생긴 문제점을 해결하기 위해 공부를 하다가 꼭 정리해야되는 핵심 내용이라서 블로그에 글을 적게되었다. 지금까지는 기능구현도 벅차서 내가 쓰고 있는 코드가 성능에 어떤 간섭을 하는지 생각을 하지 않고 코드를 썼던 것 같다. 하지만 이제 대부분의 기능을 구현 할 수 있는 지금이야 말로 좀 더 프로페셔널 한 개발자가 되기 위해서 성능 최적화에 대해서 공부를 시작해야 할 시기라 생각이 들어서 공부하게 되었다.

profile
만들고 싶은게 많은 개발자

0개의 댓글