250x250
๋ฐ˜์‘ํ˜•
arkhyeon
arkhyeon
arkhyeon
์ „์ฒด ๋ฐฉ๋ฌธ์ž
์˜ค๋Š˜
์–ด์ œ
  • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (88)
    • Spring (5)
    • Java (4)
    • React (25)
      • TypeScript (6)
      • JavaScript (1)
      • Jest (9)
    • NEXT (8)
    • SQL (1)
    • React native (1)
    • CSS (3)
    • Web (1)
    • Git (3)
    • ETC (6)
    • ๋น…๋ฐ์ดํ„ฐDB (8)
    • Docker (4)
    • Tool (1)

๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

  • ํ™ˆ
  • ํƒœ๊ทธ
  • ๋ฐฉ๋ช…๋ก

๊ณต์ง€์‚ฌํ•ญ

์ธ๊ธฐ ๊ธ€

ํƒœ๊ทธ

  • react
  • Spring WebSocket
  • react websocket
  • react19
  • docker tomcat
  • javascript wss
  • HIVE
  • websocket server
  • kudu
  • react usetransition
  • react spring websocket
  • react typescript
  • javasciprt websocket
  • WSS
  • jest
  • react loading
  • usetransition
  • websocket
  • react jest
  • node WebSocket

์ตœ๊ทผ ๋Œ“๊ธ€

์ตœ๊ทผ ๊ธ€

ํ‹ฐ์Šคํ† ๋ฆฌ

hELLO ยท Designed By ์ •์ƒ์šฐ.
arkhyeon

arkhyeon

React/TypeScript

React TypeScript Zustand Infinite Scroll with IntersectionObserver

2024. 3. 14. 16:47
728x90
๋ฐ˜์‘ํ˜•
๐Ÿ’ก ๊ธฐ์กด ์‹์ƒํ•œ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ๋ฅผ ์œ ํŠœ๋ธŒ ๋Œ“๊ธ€๊ณผ ๋น„์Šทํ•˜๊ฒŒ ๋ฌดํ•œ ์Šคํฌ๋กค๋กœ ๊ตฌํ˜„ํ•˜์˜€๊ณ  ์ด๋Š” ์˜ค์ง Zustand ์™€ IntersectionObserver๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

Zustand Setting

๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ด€๋ฆฌํ•  ๊ฐœ์ฒด๋“ค๊ณผ ์š”์ฒญ์— ๋Œ€ํ•œ ํ”Œ๋ž˜๊ทธ๋“ค์„ ์„ ์–ธํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ํ˜น์€ ๋‹ค์Œ์— ๋ถˆ๋Ÿฌ์˜ฌ ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์—†๋‹ค๋ฉด ์š”์ฒญ์„ ๋ฉˆ์ถฐ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์ „์—ญ์œผ๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํŽ˜์ด์ง€๊ฐ€ ์˜ฎ๊ฒจ์กŒ์„ ๋•Œ ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋น„์›Œ์•ผ ํ•จ์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ Store๊ฐ€ ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

import { create } from 'zustand';
import { APICommentType, BoardCommentType } from '../type/BoardType.ts';
import { client } from '../common/axios.ts';

interface CommentStoreType {
	// ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ State
  commentList: BoardCommentType[];
  // ํ˜„์žฌ ์–ด๋–ค ํŽ˜์ด์ง€์ธ์ง€ ๊ด€๋ฆฌ
  page: number;
  // ๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”์ค‘ ์ธ์ง€ ์•„๋‹Œ์ง€
  isFetching: boolean;
  // ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์žˆ๋Š”์ง€ ์—†๋Š”์ง€
  hasNextPage: boolean;
  // ๊ฒŒ์‹œ๋ฌผ ๋ณ€๊ฒฝ ์‹œ ๋Œ“๊ธ€ ๋ฆฌ์…‹ ํ•จ์ˆ˜
  resetComment: () => void;
  // ๋Œ“๊ธ€ ์š”์ฒญ API
  getCommentList: (boardId: string, page: number) => void;
}

export const CommentStore = create<CommentStoreType>(set => ({
  commentList: [],
  page: 0,
  isFetching: false,
  hasNextPage: true,
  resetComment: () =>
    set(() => ({ commentList: [], page: 0, isFetching: false, hasNextPage: true })),
  getCommentList: (boardId: string, page: number) => {
	  // Fetching On
    CommentStore.setState(() => ({ isFetching: true }));
    client.get<APICommentType>(`boardComment/${boardId}?page=${page}&size=10`).then(res => {
	    // ๋Œ“๊ธ€ ์—†์œผ๋ฉด
      if (res.findBoardCommentDtos.length === 0) {
        CommentStore.setState(() => ({ hasNextPage: false }));
      } else {
        CommentStore.setState(store => ({
          page: store.page + 1,
          commentList: [...store.commentList, ...res.findBoardCommentDtos],
          isFetching: false,
          hasNextPage: true,
        }));
      }
    });
  },
}));

Infinite Scroll

ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์Šคํฌ๋กค์„ ๊ณ„์† ์ถ”์ ํ•˜๋Š” ๊ฑด ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„๊ฐ€ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ IntersectionObserver๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ๋‘˜๋Ÿฌ๋ณด๋˜ ์ค‘ Target Component๋ฅผ ๋ฐœ๊ฒฌ ์‹œ ๋‹ค์Œ ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ๊ฐ์‹œํ–ˆ์œผ๋ฉฐ isFetching, hasNextPage๋ฅผ ์‚ฌ์šฉํ•ด API ์š”์ฒญ์„ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

import styled from '@emotion/styled';
import Comment from './Comment';
import { MutableRefObject, useEffect, useLayoutEffect, useRef } from 'react';
import { CommentStore } from '../../../store/CommentStore.ts';

function PostComment({ boardId }: { boardId: string }) {
  const { commentList, page, isFetching, hasNextPage, resetComment, getCommentList } =
    CommentStore();
  const targetRef = useRef() as MutableRefObject<HTMLDivElement>;

  const io = new IntersectionObserver(entries => {
    entries.forEach(({ target, isIntersecting }) => {
    // target ์กด์žฌ ์—ฌ๋ถ€ ๋ฐ ์š”์†Œ๊ฐ€ ๋ณด์—ฌ์ง€๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธ
      if (target && isIntersecting) {
        getCommentList(boardId, page);
      }
    });
  });

  useLayoutEffect(() => {
	  // targetRef ํ™œ์„ฑํ™” ์‹œ ๊ฐ์‹œ ์‹œ์ž‘
    if (targetRef.current) {
      io.observe(targetRef.current);
    }
    // ํŽ˜์ด์ง€ ๋‚˜๊ฐ€๋ฉด ๊ฐ์‹œ ์ข…๋ฃŒ
    return () => io.disconnect();
  }, [io]);

  // ํŽ˜์ด์ง€ ๋ณ€๊ฒฝ ์‹œ ๋Œ“๊ธ€ ๋ฆฌ์…‹
  useEffect(() => resetComment(), [boardId]);

  return (
    <CommentWrap>
      {commentList.map(cl => {
        return <Comment key={cl.boardCommentId} comment={cl} />;
      })}
      {!isFetching && hasNextPage && <Target ref={targetRef} />}
    </CommentWrap>
  );
}

const CommentWrap = styled.div`
  width: 100%;
`;

const Target = styled.div`
  height: 1px;
`;

export default PostComment;

์ดฌ์˜์„ ์œ„ํ•ด ๋กœ๋“œ ์†๋„์— 2์ดˆ ๊ฐ์†ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

728x90
๋ฐ˜์‘ํ˜•

'React > TypeScript' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

React Typescript Interface Tuple Generic  (0) 2024.03.25
React RichTextEditor Image Upload - reactQuill  (0) 2024.03.18
React TypeScript useInfiniteQuery Infinite Scroll  (0) 2024.03.15
React Axios TypeScript with JWT Boilerplate  (0) 2024.03.14
React TypeSciprt - Community Project  (0) 2024.03.13
    'React/TypeScript' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
    • React RichTextEditor Image Upload - reactQuill
    • React TypeScript useInfiniteQuery Infinite Scroll
    • React Axios TypeScript with JWT Boilerplate
    • React TypeSciprt - Community Project
    arkhyeon
    arkhyeon

    ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”