Project๐Ÿ› ๏ธโš™๏ธ

[Project/ReactTS] ์žฌ์‚ฌ์šฉ๋˜๋Š” ๊ฒŒ์‹œํŒ ๋ ˆ์ด์•„์›ƒ ๋‚ด ์ปจํ…์ธ ๋ฅผ ํŽ˜์ด์ง€๋ณ„๋กœ ๋‹ค๋ฅด๊ฒŒ ๋‚˜ํƒ€๋‚ด๊ธฐโš’๏ธ

Gaeun Lee 2024. 1. 3. 23:17

 

 

๋ฆฌ๋‰ด์–ผ๋  ์ดํ•™์ƒํšŒ ์‚ฌ์ดํŠธ๋Š” ํšŒ์น™, ๊ณต์ง€, ์ฒญ์› ํŽ˜์ด์ง€ ๋“ฑ์˜ ๊ฒŒ์‹œํŒ ๊ธ€ ๋ชฉ๋ก์„ ๊ฐ™์€ ๋ ˆ์ด์•„์›ƒ์œผ๋กœ Infinite Scrollํ•˜์—ฌ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์ด ํŽ˜์ด์ง€๋“ค์€ ๋ ˆ์ด์•„์›ƒ์€ ๊ฐ™์•„๋„ ๋ชฉ๋ก์„ ๊ตฌ์„ฑํ•˜๋Š” ์ปจํ…์ธ ๊ฐ€ ๊ฐ๊ฐ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ• ์ง€ ๊ณ ๋ฏผํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

์˜ˆ์ „ ๊ฐ™์•˜์œผ๋ฉด ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€๋ฅผ ์ž‘์„ฑํ•  ๋•Œ๋งˆ๋‹ค ๊ธฐ์กด ๋ ˆ์ด์•„์›ƒ์˜ ์ฝ”๋“œ๋ฅผ ๋ณต์‚ฌ ๋ถ™์—ฌ๋„ฃ๊ธฐํ•˜์—ฌ ๋ช‡๋ช‡ ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•˜์˜€๊ฒ ์ง€๋งŒ, ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Atomic Pattern์„ ๋„์ž…ํ•˜๊ธฐ๋กœ ํ•˜์—ฌ ๊ฐœ์„ ํ•ด๋ณด๊ธฐ๋กœ ํ•˜์˜€๋‹ค!

๋˜ํ•œ ์ด์ „ ์ดํ•™์ƒํšŒ ์‚ฌ์ดํŠธ์—์„œ๋Š” ํŽ˜์ด์ง€๋งˆ๋‹ค ์ค‘๋ณต๋˜๋Š” ํ•จ์ˆ˜๋“ค(axios(),useState()...)์ด ๋งŽ์•„ ์ˆ˜์ •์‚ฌํ•ญ์ด ์žˆ์œผ๋ฉด ํŽ˜์ด์ง€๋ณ„๋งˆ๋‹ค ๋ชจ๋‘ ๋ฐ”๊ฟ”์•ผ ํ–ˆ๋˜ ๊ฒŒ ๋ถˆํŽธํ•ด์„œ ์ด ์ ์„ ๊ผญ ๊ณ ์น˜๊ณ  ์‹ถ์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ด๋ฒˆ์—๋Š” ๋งˆ์Œ ๋จน๊ณ  ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ๊ธฐ์กด ๋ ˆ์ด์•„์›ƒ ์•ˆ์— ๋“ค์–ด๊ฐˆ ์ปจํ…์ธ ์™€ ๊ธฐ๋ณธ ์ •๋ณด๋งŒ ์ž‘์„ฑํ•˜๋ฉด ๋ ˆ์ด์•„์›ƒ์— ์•Œ์•„์„œ ๋งž์ถฐ์ง€๋„๋ก ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ๋กœ ํ•˜์˜€๋‹ค.

๋ณด๋‹ค์‹œํ”ผ ๋ ˆ์ด์•„์›ƒ์ด ๋™์ผํ•˜๋‹ค

 

  

๋ ˆ์ด์•„์›ƒ์„ ๋„์ž…ํ•˜๊ธฐ ์ „์˜ ์ฒญ์›๊ฒŒ์‹œํŒ ์ฝ”๋“œ์ด๋‹ค.

import Board from 'components/common/board';
import Text from 'components/ui/text';
import { API_PATH } from 'constant';
import { useInfiniteScroll } from 'hooks/useInfiniteScroll';
import React, { useEffect } from 'react';

interface IPetitionPost {
   id: string;
   title: string;
   // ์ƒ๋žต
}

export default function PetitionBoard() {
   const { list, isLoading, bottom } = useInfiniteScroll<IPetitionPost>(
      CONSTANTS.SERVER_URL + API_PATH.POST.PETITON,
   );
   const [isEmpty, setIsEmpty] = React.useState(false);

   useEffect(() => {
      setIsEmpty(list && list.length === 0);
   }, [list]);

   return (
      <>
         <Board>
            {isEmpty ? (
               <Board.NoData />
            ) : (
               list?.map(({ id, status, title, agreeCount, expiresAt }) => (
                  <Board.Cell key={id}>
                     <Text length={4}>{status}</Text>
                     <Text length={4}>{title}</Text>
                     <Text length={4}>{agreeCount}</Text>
                     <Text length={4}>{expiresAt}</Text>
                  </Board.Cell>
               ))
            )}
            {!isLoading && !isEmpty && <div ref={bottom} />}
         </Board>
      </>
   );
}

 

 

 

๊ฐ ๊ฒŒ์‹œํŒ๋งˆ๋‹ค ํ•„์š”ํ•œ ์ •๋ณด = API ์ฃผ์†Œ, ๊ธ€ ๋ชฉ๋ก์„ ๊ตฌ์„ฑํ•˜๋Š” JSX Elements

 

๋ ˆ์ด์•„์›ƒ ์ ์šฉ ์ฒซ ๋ฒˆ์งธ ์‹œ๋„!

๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค๊ณ , ๋ ˆ์ด์•„์›ƒ ์•ˆ์„ pathname์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ๋‚˜ํƒ€๋‚ด ๋ณด์•˜๋‹ค.

BoardLayout.tsx

import Board from 'components/common/board';
import Text from 'components/ui/text';
import { useInfiniteScroll } from 'hooks/useInfiniteScroll';
import React, { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

interface IBoardList {
	// ์ค‘๋ณต๋˜๋Š” ํ•„๋“œ๊ฐ€ ๋Œ€๋ถ€๋ถ„์ด๋ผ IBoardList๋กœ ํ†ตํ•ฉํ•˜์˜€๋‹ค
    // ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ํ•„๋“œ๋Š” Optional Properties๋กœ ์ฒ˜๋ฆฌํ•˜์˜€๋‹ค.
   id: string;
   ...
   status?: string;
   expiresAt?: string;
   agreeCount?: string;
   blinded: string;
}

export default function BoardLayout({ api }: { api: string }) {
   const { list, isLoading, bottom } = useInfiniteScroll<ICell>(api);
   const [isEmpty, setIsEmpty] = React.useState(false);
   const navigate = useNavigate();
   const location = useLocation();

   useEffect(() => {
      setIsEmpty(list && list.length === 0);
   }, [list]);

   return (
      <>
         <Board>
            {isEmpty ? (
               <Board.NoData />
            ) : (
               list?.map(({ id, status, title, createdAt, expiresAt }) => (
                  <Board.Cell
                     key={id}
                     onClick={() => {
                        navigate(`${id}`);
                     }}
                  >
                     {/* ๊ณต์ง€ */}
                     {location.pathname === '/notice' && (
                        <>
                           <Text className='text-xl font-bold' length={4}>
                              {title}
                           </Text>
                           <Text className='text-gray-400' length={4}>
                              {createdAt.slice(0, 10).replaceAll('-', '.')}
                           </Text>
                        </>
                     )}

                     {/* ์ฒญ์› */}
                     {location.pathname === '/petition' && (
                        <div className='flex justify-between leading-9 px-8'>
                           <Text length={4}>{status}</Text>
                           <Text length={4}>{title}</Text>
                           <Text length={4}>{expiresAt}</Text>
                        </div>
                     )}
                  </Board.Cell>
               ))
            )}
            {!isLoading && !isEmpty && <div ref={bottom} />}
         </Board>
      </>
   );
}

 

/petition/index.tsx (BoardLayout ํ™œ์šฉ)

import { API_PATH, CONSTANTS } from 'constant';
import BoardLayout from 'layouts/BoardLayout';
import React from 'react';

export default function PetitionBoard() {
   return <BoardLayout api={CONSTANTS.SERVER_URL + API_PATH.POST.PETITION} />;
}


์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๋ฉด ๋ ˆ์ด์•„์›ƒ ํ™œ์šฉ์‹œ api ์ฃผ์†Œ๋งŒ ๋„ฃ์œผ๋ฉด ๋œ๋‹ค! ์ฝ”๋“œ ๋ผ์ธ ์ˆ˜๋ฅผ ๋งŽ์ด ์ค„์—ฌ์„œ ๋ฟŒ๋“ฏํ–ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ pathname์„ ํ†ตํ•˜์—ฌ ์ง€์ •ํ•˜๋Š” ๊ฒŒ ์˜คํžˆ๋ ค ์ฝ”๋“œ๊ฐ€ ๋” ์ง€์ €๋ถ„ํ•ด๋ณด์ธ๋‹ค๋Š” ๋Š๋‚Œ์ด ๋“ค์—ˆ๋‹ค.

ํŽ˜์ด์ง€๊ฐ€ ๋” ๋งŽ์•„์ง€๋ฉด ์ฝ”๋“œ ๋ผ์ธ๋„ ์˜คํžˆ๋ ค ๊ธธ์–ด์งˆ ๊ฒƒ์ด๊ณ ..

์—ญ์‹œ๋‚˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๊ฒฐ๊ณผ, ๋„๋ฉ”์ธ๊ณผ ์—ฎ์—ฌ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ๋‹ค๋Š” ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๊ฒŒ ๋˜์—ˆ๋‹ค.

 

๋‘ ๋ฒˆ์งธ ์‹œ๋„!

์™ธ๋ถ€ ์ฃผ์ž…์ด๋ผ๋Š” ํ‚ค์›Œ๋“œ๋ฅผ ํ†ตํ•ด ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๋กœ์˜ ์ „๋‹ฌ์„ ๋– ์˜ฌ๋ ธ๋‹ค.

์ฆ‰, BoardLayout ์ปดํฌ๋„ŒํŠธ์— JSX.Element๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

๋ ˆ์ด์•„์›ƒ์ด๋ผ๋Š” ๊ฐœ๋… ๋•Œ๋ฌธ์— {props.children} ๋ฐ–์— ๋– ์˜ค๋ฅด์ง€ ์•Š์•„ ์ค‘๊ฐ„์— ํ—ค๋งธ๋Š”๋ฐ ๋‹ค์‹œ ์ƒ๊ฐํ•ด๋ณด๋‹ˆ ๊ทธ๋ƒฅ props๋ฅผ ํ†ตํ•˜์—ฌ ์ปดํฌ๋„ŒํŠธ๋งŒ ์ „๋‹ฌํ•˜๋ฉด ๋œ๋‹ค๋Š” ๋ช…๋ฃŒํ•œ ๋‹ต์ด ๋‚˜์™”๋‹ค.

๊ทธ ๋‹ค์Œ ๋ฌธ์ œ๋Š” ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ(BoardLayout)์—์„œ API Response๋ฅผ ๋ฐ›๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์ด๋ฅผ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ์ „๋‹ฌํ•ด์•ผ ํ•˜๊ณ , ์ž์‹ ์ปดํฌ๋„ŒํŠธ(PetitionBoard)๋Š” ๋ชฉ๋ก ๊ตฌ์„ฑ์š”์†Œ(JSX.Element)๋ฅผ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์•„์›ƒ ์•ˆ์— ๋‚˜ํƒ€๋‚ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. 

์ด์— ๋Œ€ํ•œ ํ•ด๊ฒฐ์ฑ…์€ BoardLayout๊ณผ PetitionBoard์—์„œ API Reponse์™€ JSX.Element๋ฅผ ์„œ๋กœ ์ „๋‹ฌ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

๋”ฐ๋ผ์„œ BoardLayout์€ API Response๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„ JSX.Element๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ prop์œผ๋กœ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค.

 

BoardLayout.tsx

// ์ƒ๋žต

export interface IBoardList {
   id: string;
	// ์ƒ๋žต
}

export default function BoardLayout({
   api,
   setCell,
}: {
   api: string;
   setCell: (data: IBoardList) => JSX.Element;
}) {
   const { list, isLoading, bottom } = useInfiniteScroll<IBoardList>(api);
   const [isEmpty, setIsEmpty] = React.useState(false);
   const navigate = useNavigate();

   useEffect(() => {
      !list && setIsEmpty(true);
   }, [list]);

   if (isEmpty) {
      return (
         <Board>
            <Board.NoData />
         </Board>
      );
   }

   return (
      <Board>
         {list?.map((data) => (
            <Board.Cell
               key={data.id}
               onClick={() => {
                  navigate(`${data.id}`);
               }}
            >
               {setCell(data)}
            </Board.Cell>
         ))}
         {!isLoading && !isEmpty && <div ref={bottom} />}
      </Board>
   );
}

/petition/index.tsx

import Text from 'components/ui/text';
import { API_PATH, CONSTANTS } from 'constant';
import BoardLayout, { IBoardList } from 'layouts/BoardLayout';
import React from 'react';

export default function PetitionBoard() {
   const cell = (data: IBoardList) => (
      <div className='flex justify-between leading-9 px-8'>
         <Text length={4}>{data.status}</Text>
         <Text length={4}>{data.title}</Text>
         <Text length={4}>{data.expiresAt}</Text>
      </div>
   ); 
   return <BoardLayout api={CONSTANTS.SERVER_URL + API_PATH.POST.PETITION} setCell={cell} />;
}

 

์ฝ”๋“œ ์ค„์ด๊ธฐ ์„ฑ๊ณต !