React.memo와 useCallback을 통한 게시판 앱 최적화

김세현·2022년 8월 6일
1

React

목록 보기
10/10

현재 게시판 앱의 컴포넌트 구조는 위와 같습니다.

그 중 부모 컴포넌트가 PostList인 서브 컴포넌트 트리가 있습니다.

PostList 컴포넌트는 게시물을 보여주기 위한 로직을 전반적으로 관리하고 있습니다.

그리고 하위의 Post컴포넌트는 게시물 상세 정보를 보여주는 컴포넌트이며, 컨텐츠를 보여주기 위해 모달 형태로 구성하였습니다.

따라서 상위 컴포넌트인 PostList에서 Post 컴포넌트가 열려있는지, 닫혀있는지를 알기 위한 상태를 관리하고 있습니다.

또한, PostList 컴포넌트의 하위 컴포넌트에는 Pagination, ListItem도 있습니다.

이때, 만약 PostList 컴포넌트에서 관리되고 있는 상태가 변하게 된다면,

이는 PostList함수 컴포넌트가 다시 실행된다는 것이고, PostList컴포넌트에서

호출하고 있는 자식 컴포넌트들인 Pagination, ListItem, PostCommentList까지 모두 재랜더링이 발생하게 됩니다.

만약, 부모 컴포넌트의 상태가 하위 컴포넌트들에게 prop으로 전달되는 경우

즉, 하위 컴포넌트가 상위 컴포넌트의 어떤 상태에 의존하고 있고, 이 상태가

변한다면 위와 같은 하위 컴포넌트들의 재렌더링은 필요한 과정입니다.

하지만, 부모의 특정 상태에 의존적이지 않음에도 단지, 부모 컴포넌트(PostList) 재렌더링이 발생했다고 해서 특정 상태와 관련없는 하위 컴포넌트도 재렌더링이 발생하는 경우가 있습니다.

즉, 하위 컴포넌트는 재렌더링 될 필요가 없지만, 단지 부모 컴포넌트가 재렌더링 되었다고 하위 컴포넌트도 재렌더링되는 경우입니다.

PostList 컴포넌트에서 게시물이 열려있는지 닫혀있는지를 관리하는 상태는

Pagination, ListItem 컴포넌트와는 관련이 없는 상태입니다.

하지만, 아래와 같이 게시물이 열리고, 닫힘에 따라서ListItem, Pagination 컴포넌트도 재렌더링되고 있습니다.

React.memouseCallback을 통해 불필요한 재렌더링을 피하기

React.memo는 이전 prop과 새로운 prop을 비교하여 달라졌을 때에만 렌더링할 수 있도록 해주는 함수입니다.

다음과 같이 React.memo로 ListItem 컴포넌트를 감싸주었습니다.

export default React.memo(ListItem);

그럼에도 이전과 똑같이 불필요한 재렌더링이 발생하게 됩니다.

이유는 ListItem 컴포넌트는 PostList 컴포넌트로부터 prop을 통해 함수를 전달받고 있기 때문입니다.

위에서 언급했듯이, React.memo는 이전 prop과 새로운 prop을 비교합니다.

하지만, 자바스크립트에서 원시 값의 비교와 참조 타입의 값 비교는 다릅니다.

'a' === 'a' // 원시 값의 비교 true
[] === []   // 참조 타입의 비교 false

prop으로 원시 값만을 전달받고 있을 때에는 적절한 prop의 비교를 통해React.memo만으로도 불필요한 렌더링을 방지할 수 있지만,

prop으로 참조 타입의 값을 전달받고 있을 때에는 이전 prop과 새로운 prop을 비교했을 때는 항상 다른 결과가 나오므로 React.memo만으로는 불필요한 렌더링을 막을 수 없습니다.

아래의 코드처럼 상위 컴포넌트인 PostListListItem 컴포넌트에게 prop으로 함수를 전달하고 있습니다. (=clickPost)

//PostList 컴포넌트
const clickPost = (post) => {
  ...
}

return (
  <ListItem key={post.id} post={post} onClick={clickPost} />
)

만약, PostList 함수 컴포넌트가 재실행된다면, clickPost 함수를 정의한 코드도 재실행되므로, clickPost 함수는 이전과는 다른 메모리 공간을 가리키는 새로운 함수가 됩니다.

그리고 매번 새로운 함수를 prop으로 전달하게 되는 것입니다.

따라서 하위 컴포넌트에서 React.memo에 의해 이전 prop과 새로운 prop을 비교하게 되면 참조 타입의 특성에 의해 항상 다른 결과를 받게됩니다.

따라서 이때에는 React.memo만으로는 불필요한 렌더링을 방지할 수 없습니다.

이때, useCallback을 사용한다면 이를 방지할 수 있습니다.

useCallback은 생성된 함수를 기억하고 있도록 해주는 훅입니다.

따라서 컴포넌트가 다시 실행될 때, 새로운 함수를 생성하지 않도록 해줍니다.

useCallback으로 기억하고자 하는 함수를 감싸주면 됩니다.

const clickPost = useCallback((post) => {
  ...
},[])

return (
  <ListItem key={post.id} post={post} onClick={clickPost} />
)

이후, clickPost는 항상 똑같은 함수를 나타냅니다.

따라서 참조 타입의 값이라도 이제 React.memo는 적절한 prop 비교를 수행할 수 있고, 이를 통해 불필요한 렌더링을 방지할 수 있습니다.

최적화 이후

이전에는 게시물의 열고, 닫힘 상태에 따라서 이 상태와는 관련없는 ListItem, Pagination 컴포넌트도 재렌더링이 발생했지만

React.memouseCallback을 이용해 불필요한 렌더링이 발생하지 않도록 개선했습니다

추가 : useMemo 사용하기

위의 화면에서 게시물 창을 닫았을 때 PostList 컴포넌트의 상태가 변했기 때문에
PostList 컴포넌트는 재실행 되어야 하는 것이 맞습니다.

하지만 이때, PostList의 코드 중에는 게시물 항목(ListItem)을 렌더링하기 위해 이를 계산하는 코드가 존재합니다.

const posts = props.posts
      .slice(offset, offset + postsPerPage)
      .map((post) => (
        <ListItem key={post.id} post={post} onClick={clickPost} />
));

즉, 게시물 창의 열고 닫힘에 따라 PostList의 상태가 변하고, 이에 따라PostList 컴포넌트가 재실행되므로 위의 코드도 그때마다 재실행되고 있습니다.

하지만, 게시물의 열림, 닫힘 상태와 상관없이 posts라는 변수는 다른 상태를 기준으로 계산된 값들이 담겨있습니다.
(posts보고있는 페이지 상태가 변했을 때에만 값이 바뀌는 변수입니다)

즉, 게시물의 열고 닫힘렌더링 할 게시물을 계산하는 작업은 서로 관련이 없으므로, PostList가 재실행될 때 위의 계산하는 작업은 수행하지 않아도 됩니다.

따라서 이 작업을 useMemo를 통해 기억하여, 의존성 배열에 담긴 값들이 변경되었을 경우에만 계산되도록 수정하여 불필요한 계산을 수행하지 않도록 했습니다.

  const posts = useMemo(
    () =>
      props.posts
        .slice(offset, offset + postsPerPage)
        .map((post) => (
          <ListItem key={post.id} post={post} onClick={clickPost} />
        )),
    [props.posts, offset, postsPerPage, clickPost]
  );
profile
under the hood

0개의 댓글