현재 게시판 앱의 컴포넌트 구조는 위와 같습니다.
그 중 부모 컴포넌트가 PostList
인 서브 컴포넌트 트리가 있습니다.
PostList
컴포넌트는 게시물을 보여주기 위한 로직을 전반적으로 관리하고 있습니다.
그리고 하위의 Post
컴포넌트는 게시물 상세 정보를 보여주는 컴포넌트이며, 컨텐츠를 보여주기 위해 모달 형태로 구성하였습니다.
따라서 상위 컴포넌트인 PostList
에서 Post
컴포넌트가 열려있는지, 닫혀있는지를 알기 위한 상태를 관리하고 있습니다.
또한, PostList
컴포넌트의 하위 컴포넌트에는 Pagination
, ListItem
도 있습니다.
이때, 만약 PostList
컴포넌트에서 관리되고 있는 상태가 변하게 된다면,
이는 PostList
함수 컴포넌트가 다시 실행된다는 것이고, PostList
컴포넌트에서
호출하고 있는 자식 컴포넌트들인 Pagination
, ListItem
, Post
와 CommentList
까지 모두 재랜더링이 발생하게 됩니다.
만약, 부모 컴포넌트의 상태가 하위 컴포넌트들에게 prop
으로 전달되는 경우
즉, 하위 컴포넌트가 상위 컴포넌트의 어떤 상태
에 의존하고 있고, 이 상태가
변한다면 위와 같은 하위 컴포넌트들의 재렌더링은 필요한 과정입니다.
하지만, 부모의 특정 상태에 의존적이지 않음에도 단지, 부모 컴포넌트(PostList
) 재렌더링이 발생했다고 해서 특정 상태와 관련없는 하위 컴포넌트도 재렌더링이 발생하는 경우가 있습니다.
즉, 하위 컴포넌트는 재렌더링 될 필요가 없지만, 단지 부모 컴포넌트가 재렌더링 되었다고 하위 컴포넌트도 재렌더링되는 경우입니다.
PostList
컴포넌트에서 게시물이 열려있는지 닫혀있는지를 관리하는 상태는
Pagination
, ListItem
컴포넌트와는 관련이 없는 상태입니다.
하지만, 아래와 같이 게시물이 열리고, 닫힘에 따라서ListItem
, Pagination
컴포넌트도 재렌더링되고 있습니다.
React.memo와 useCallback을 통해 불필요한 재렌더링을 피하기
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
만으로는 불필요한 렌더링을 막을 수 없습니다.
아래의 코드처럼 상위 컴포넌트인 PostList
는 ListItem
컴포넌트에게 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.memo
와 useCallback
을 이용해 불필요한 렌더링이 발생하지 않도록 개선했습니다
추가 : 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]
);