그럼에도 불구하고

👨‍💻

[React with TS] 계속되는 List 최적화하기 본문

React/React basics

[React with TS] 계속되는 List 최적화하기

zenghyun 2023. 7. 29. 14:31

 

진행 중인 프로젝트 해서 계속되는 List를 최적화하는 법에 대해 알아보겠습니다.

 

 

🧑🏻‍💻  Palette Project 

이 프로젝트는 현재 제가 진행하고 있는 Palette라는 프로젝트입니다. 

 

위의 사진과 같이 게시물을 차례대로 나열하고 있는데 이 게시물의 수가 무수히 많아진다면, 성능 면에서 어떻게 개선할 수 있을지 알아보겠습니다.  

 

 

실험 조건: 현재 10명의 사람이 각각 250개의 포스트를 작성하여 총 2500개의 게시물이 있는 상태

 

측정 방식:  Redux Profiler를 이용하여 렌더링 되어있는 post의 reaction 버튼을 눌렀을 때, 다시 렌더링되는 시점을 측정 

 

 

 

📌 PostList.tsx

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { PostStateType, selectAllPosts, fetchPosts } from "../../features/posts/postsSlice";
import { RootStateType } from "../../app/store";
import { Spinner } from "../common/Spinner";
import PostAuthor from "../common/PostAuthor";
import { TimeAgo } from "../common/TimeAgo";
import { ReactionButtons } from "../common/ReactionButtons";
import { useAppDispatch } from "../../app/store";
import { fetchUsers } from "../../features/users/usersSlice";
 
 
const PostExcerpt = ({post} : {post: PostStateType}) => {
  return(
    <article className="post-excerpt">
        <h3>{post.title}</h3>
        <div>
          <PostAuthor userId={post.user} />
          <TimeAgo timestamp={post.date} />
        </div>
        <p className="post-content">{post.content.substring(0100)}</p>
 
        <ReactionButtons post={post} />
        <Link to={`/posts/${post.id}`} className="button feed-button">
        View Palette
        </Link>
    </article>
  )
};
 
 const PostsList = () => {
  const dispatch = useAppDispatch();
  const posts = useSelector((selectAllPosts));
  const postStatus = useSelector((state: RootStateType) => state.posts.status);
  const error = useSelector((state: RootStateType) => state.posts.error);
  
  useEffect(() => {
    if(postStatus === 'idle') {
      dispatch(fetchPosts());
      dispatch(fetchUsers());
    }
  }, [postStatus, dispatch]);
 
  let content;
 
  if(postStatus === 'loading') {
    content = <Spinner text="Loading..." />
  } else if(postStatus === 'succeeded') {
    // Sort posts in reverse chronological order by dateTime string 
    const orderedPosts = posts.slice().sort((a : PostStateType, b: PostStateType) => b.date.localeCompare(a.date));
    content = orderedPosts.map(post => (
      <PostExcerpt key={post.id} post={post} />
    ))
  } else if (postStatus === 'failed') {
    content = <div>{error}</div>
  }
 
 
  return (
    <section className="posts-list">
      <h2>Posts</h2>
      {content}
    </section>
  );
};
 
export default PostsList;
cs

 

 const PostExcerpt = ({post} : {post: PostStateType}) => {
  return(
    <article className="post-excerpt">
        <h3>{post.title}</h3>
        <div>
          <PostAuthor userId={post.user} />
          <TimeAgo timestamp={post.date} />
        </div>
        <p className="post-content">{post.content.substring(0, 100)}</p>

        <ReactionButtons post={post} />
        <Link to={`/posts/${post.id}`} className="button feed-button">
        View Palette
        </Link>
    </article>
  )
};
 
...

 const orderedPosts = posts.slice().sort((a : PostStateType, b: PostStateType) => b.date.localeCompare(a.date));
    content = orderedPosts.map(post => (
      <PostExcerpt key={post.id} post={post} />
    ))

 

'posts'배열을 복사한 뒤, 해당 배열을 날짜 기준으로 내림차순으로 정렬하여 새로운 배열 orderedPosts를 생성합니다.

 

그리고 orderedPost 배열의 각 요소를 기반으로 <PostExcerpt> 컴포넌트를 렌더링 하여 결과를 content 변수에 저장하고 있습니다.

 

이 상태에서 이모티콘을 눌러서 렌더링 성능을 측정해 보겠습니다. 

 

 

 

📌 PostList.tsx ( React.memo 사용 )

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { PostStateType, selectAllPosts, fetchPosts } from "../../features/posts/postsSlice";
import { RootStateType } from "../../app/store";
import { Spinner } from "../common/Spinner";
import PostAuthor from "../common/PostAuthor";
import { TimeAgo } from "../common/TimeAgo";
import { ReactionButtons } from "../common/ReactionButtons";
import { useAppDispatch } from "../../app/store";
import { fetchUsers } from "../../features/users/usersSlice";
 
 
const PostExcerpt = React.memo(({post} : {post: PostStateType}) => {
  return(
    <article className="post-excerpt">
        <h3>{post.title}</h3>
        <div>
          <PostAuthor userId={post.user} />
          <TimeAgo timestamp={post.date} />
        </div>
        <p className="post-content">{post.content.substring(0100)}</p>
 
        <ReactionButtons post={post} />
        <Link to={`/posts/${post.id}`} className="button feed-button">
        View Palette
        </Link>
    </article>
  )
});
 
 const PostsList = () => {
  const dispatch = useAppDispatch();
  const posts = useSelector((selectAllPosts));
  const postStatus = useSelector((state: RootStateType) => state.posts.status);
  const error = useSelector((state: RootStateType) => state.posts.error);
  
  useEffect(() => {
    if(postStatus === 'idle') {
      dispatch(fetchPosts());
      dispatch(fetchUsers());
    }
  }, [postStatus, dispatch]);
 
  let content;
 
  if(postStatus === 'loading') {
    content = <Spinner text="Loading..." />
  } else if(postStatus === 'succeeded') {
    // Sort posts in reverse chronological order by dateTime string 
    const orderedPosts = posts.slice().sort((a : PostStateType, b: PostStateType) => b.date.localeCompare(a.date));
    content = orderedPosts.map(post => (
      <PostExcerpt key={post.id} post={post} />
    ))
  } else if (postStatus === 'failed') {
    content = <div>{error}</div>
  }
 
 
  return (
    <section className="posts-list">
      <h2>Posts</h2>
      {content}
    </section>
  );
};
 
export default PostsList;
cs

 

React.memo를 사용하여 렌더링 결과를 메모이제이션하기 때문에 불필요한 리렌더링을 건너뛰는 것을 볼 수 있습니다. 

 

React는 먼저 컴포넌트를 렌더링(rendering) 한 뒤, 이전 렌더링 결과와 비교하여 DOM 업데이트를 결정합니다. 

여기서 만약, 렌더링 결과가 이전과 다르다면 React DOM은 업데이트를 실행합니다. 

 

여기서 React.memo는 컴포넌트를 렌더링 하고 결과를 메모이징(Memoizing)하기 때문에, 다음 렌더링이 일어날 때 props가 같다면, React는 메모리징된 내용은 재사용하기 때문에 렌더링 속도를 많이 줄일 수 있습니다.

 

📌 PostList.tsx ( React.window 사용 )

 

React.memo를 사용하는 것만으로도 성능면에서 큰 개선이 되기는 하지만 더 개선시킬 수 있습니다. 

 

React.window는 대형 리스트를 효율적으로 렌더링 할 수 있는 라이브러리입니다. 

 

React.window는 목록 가상화 또는 "윈도잉(Windowing)"은 사용자에게 보이는 부분만 렌더링 하는 개념입니다.

 

사용자에게 보여줄 창의 크기를 지정하고, 사용자가 스크롤을 계속하는 경우 보이는 콘텐츠의 "창"이 움직이면서 렌더링 하는 것입니다. 

 

이렇게 하면 리스트의 렌더링 및 스크롤 성능이 모두 향상됩니다.

 

https://web.dev/i18n/ko/virtualize-long-lists-react-window/

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import React, { CSSProperties, useEffect } from "react";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { FixedSizeList } from "react-window"// Import the FixedSizeList from react-window
import { PostStateType, selectAllPosts, fetchPosts } from "../../features/posts/postsSlice";
import { RootStateType } from "../../app/store";
import { Spinner } from "../common/Spinner";
import PostAuthor from "../common/PostAuthor";
import { TimeAgo } from "../common/TimeAgo";
import { ReactionButtons } from "../common/ReactionButtons";
import { useAppDispatch } from "../../app/store";
import { fetchUsers } from "../../features/users/usersSlice";
 
const PostExcerpt = React.memo(({ post } : { post: PostStateType}) => {
  return (
    <article className="post-excerpt">
      <h3>{post.title}</h3>
      <div>
        <PostAuthor userId={post.user} />
        <TimeAgo timestamp={post.date} />
      </div>
      <p className="post-content">{post.content.substring(0100)}</p>
 
      <ReactionButtons post={post} />
      <Link to={`/posts/${post.id}`} className="button feed-button">
        View Palette
      </Link>
    </article>
  );
});
 
const PostsList = () => {
  const dispatch = useAppDispatch();
  const posts = useSelector(selectAllPosts);
  const postStatus = useSelector((state: RootStateType) => state.posts.status);
  const error = useSelector((state: RootStateType) => state.posts.error);
 
  useEffect(() => {
    if (postStatus === "idle") {
      dispatch(fetchPosts());
      dispatch(fetchUsers());
    }
  }, [postStatus, dispatch]);
 
  let content;
 
  if (postStatus === "loading") {
    content = <Spinner text="Loading..." />;
  } else if (postStatus === "succeeded") {
    // Sort posts in reverse chronological order by dateTime string
    const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date));
 
    const itemKey = (index: number) => orderedPosts[index].id;
 
    const renderItem = ({ index, style } : {index: number, style: CSSProperties} ) => {
      const post = orderedPosts[index];
      return (
        <div style={style} key={itemKey(index)}>
          <PostExcerpt post={post} />
        </div>
      );
    };
 
    content = (
      <FixedSizeList
        height={700// 보여줄 전체 높이
        width={800// 보여줄 넓이
        itemCount={orderedPosts.length// post 개수
        itemSize={240// 개별적 post의 높이 
      >
        {renderItem}
      </FixedSizeList>
    );
  } else if (postStatus === "failed") {
    content = <div>{error}</div>;
  }
 
  return (
    <section className="posts-list">
      <h2>Posts</h2>
      {content}
    </section>
  );
};
 
export default PostsList;
 
cs

 

 

 

📌 최종 결과

React.memo 사용전 렌더링 :  261.4ms   

  ⬇️  렌더링 속도 96.36% 개선 

React.memo 사용후 렌더링 :  9.7ms 

  ⬇️  렌더링 속도 25.77% 개선 

React.window 사용후 렌더링 : 7.2ms 

 

최종적으로 렌더링 속도가  97.17% 개선되었습니다!!

 

 

⭐️ 만약 게시물에 사진이 있다면? ( test ) 

Post 한 개마다 사진이 있다면 React.window를 사용했을 때 더욱 큰 효과를 볼 수 있지 않을까?라는 의문을 갖고 진행한 테스트입니다. : )

 

🧐 React.window를 사용했을 때

 

 

스크롤을 할 때마다 보이는 부분만 렌더링 하는게 보이시나요?

 

 

🧐 React.windw를 사용하지 않았을 때 

 

 

 

React.window를 썼을 때는 이미지를  가져오는데 11ms가 걸렸다면 React.window를 쓰지 않았을 때는 500ms가 걸리는 걸 볼 수 있습니다. 

 

렌더링할 요소가 많으면 많을수록 더욱 큰 효과를 볼 수 있겠다는 결론을 내렸습니다.

 

🏷️ Ref

https://web.dev/i18n/ko/virtualize-long-lists-react-window/

https://brunch.co.kr/@devapril/48

 

 

 

Comments