
๋ฆฌ๋ด์ผ๋ ์ดํ์ํ ์ฌ์ดํธ๋ ํ์น, ๊ณต์ง, ์ฒญ์ ํ์ด์ง ๋ฑ์ ๊ฒ์ํ ๊ธ ๋ชฉ๋ก์ ๊ฐ์ ๋ ์ด์์์ผ๋ก 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} />;
}
์ฝ๋ ์ค์ด๊ธฐ ์ฑ๊ณต !
