import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const page = parseInt(req.nextUrl.searchParams.get("page")!);
const array = Array.from({ length: 5 }, (_item, idx) => {
return page + idx;
});
return NextResponse.json({ items: array }, { status: 200 });
}
여러분 안녕하세요?
길고 긴 자율 프로젝트도 드디어 끝을 맞이했는데요
다들 보람찬 프로젝트 기간 되셨나요?
저는 Next.js를 사용하여 이번 자율 프로젝트를 진행하였는데요!
React Query를 사용할 수 없는 만큼 무한스크롤을 직접 구현할 필요가 있었습니다.
하지만 필터링이나 페이지 등 ...
조건이 많아 useEffect 사용 시 함수의 state에 많이 신경을 써 주어야 했는데요
오늘은 react 문법 사용 시 많이 헷갈릴 수 있는 state의 변동 타이밍에 대해 알아보도록 하겠습니다!
※ 아래 화면 및 소스코드는 실제 프로젝트 작업물이 아닌 기사를 위해 작성한 간이 소스코드입니다.
-
먼저, 실행 타이밍을 재기 위해 간단한 무한스크롤 페이지를 제작합니다.
가독성을 위해 tailwind를 사용하여 간단한 배경색을 넣었습니다.
page.tsx
import React, { useEffect, useState } from "react";
import Observer from "@/components/Observer";
function MainContainer() {
// 정보를 담을 배열
const [items, setItems] = useState<string[]>([]);
// 페이지
const [page, setPage] = useState(1);
// fetch 함수
const fetchItems = async () => {
const res = await fetch(`http://localhost:3000/api/fetch?page=${page}`);
const { newItems } = await res.json();
setItems((prev) => [...prev, ...newItems]);
setPage((prev) => prev + 5);
};
// 페이지와 배열 reset 함수
const reset = () => {
setItems([]);
setPage(1);
fetchItems();
};
useEffect(() => {
fetchItems();
}, []);
const observerCallback: IntersectionObserverCallback = ([ // observer Callback
{ isIntersecting },
]) => {
if (isIntersecting) {
fetchItems();
}
};
return (
<>
<div className="h-dvh flex justify-center items-end p-10 mb-5">
<div className="flex items-center">
스크롤을 내리면 fetch가 시작됩니다 ↓
<button
className="border border-black p-1 px-3 rounded-2xl ml-3 hover:bg-slate-100"
onClick={() => reset()}
>
번호 초기화 및 배열 리셋
</button>
</div>
</div>
<div>
{items.map((e, idx) => (
<div key={idx} className="w-full h-16 bg-red-200 mb-5 ">
{e}
</div>
))}
</div>
<Observer callback={observerCallback} />
</>
);
}
export default MainContainer;
api/fetch/route.tsx
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const page = parseInt(req.nextUrl.searchParams.get("page")!);
const array = Array.from({ length: 5 }, (_item, idx) => {
return page + idx;
});
return NextResponse.json({ newItems: array }, { status: 200 });
}
주목해야 할 것은 reset함수인데요, 위와 같이 배열 및 페이지 초기화 후 fetch를 실행하게끔 설정해두면...
이렇게, 1번부터가 아닌 11번부터 fetch가 진행된 후, page가 초기화되어 1, 2, 3, 4... 순으로 아이템을 불러오게 됩니다. 그렇다면 해당 함수들의 실행 순서를 체크하기 위해 console창을 확인해보겠습니다.
위와 같이, 페이지 및 리스트 초기화 전 reset함수와 fetchItems가 실행되는 것을 보실 수 있습니다. 즉, state의 변경은 해당 setState 함수를 호출한 함수의 실행이 종료된 후 실행된다는 것을 알 수 있는데요, 그렇다면 무한 스크롤을 어떻게 구현해아 할까요?
방법 1. useRef 이용
// ...
const page = useRef(1);
const fetchItems = async () => {
console.log(`fetchItems 실행, page: ${page.current}`);
const res = await fetch(
`http://localhost:3000/api/fetch?page=${page.current}`
);
const { newItems } = await res.json();
setItems((prev) => [...prev, ...newItems]);
page.current += 5;
};
const reset = () => {
console.log(`reset함수 실행, page: ${page.current}`);
window.scrollTo({ top: 0 });
setItems([]);
page.current = 1;
fetchItems();
};
// ...
첫 번째 방법은 useRef를 활용하는 방법인데요, useState의 경우 state가 변경되면 react의 상태가 변경되며 페이지가 리렌더링되고, 리렌더링된 페이지에서 상태를 가지고 오게 되어 있습니다. 반대로 useRef는 상태가 변경되어도 페이지가 리렌더링되지 않지만, 변경 사항을 바로 사용할 수 있다는 장점이 있습니다.
값의 변화에 따라 페이지가 리렌더링 될 필요가 없다면 useRef를 변수처럼 사용할 수 있는 것!
위 코드의 경우, items는 값의 변화에 따라 렌더링이 필요하므로 useState 를 이용해야하지만, fetch할 페이지에만 영향을 끼치는 page의 경우 useRef를 사용하면 state의 변경 타이밍을 신경쓰지 않고 값을 사용할 수 있습니다.
위 코드를 적용하면
위와 같이 pages와 배열이 잘 초기화되는 것을 확인하실 수 있습니다.
방법 2. useEffect 이용
// ...
const [isFetching, setIsFetching] = useState(true);
const fetchItems = async () => {
//...
setIsFetching(false);
//...
};
const reset = () => {
//...
setIsFetching(true);
};
useEffect(() => {
if (isFetching) {
fetchItems();
}
}, [isFetching]);
두 번째 방법은 데이터 fetch여부가 의존할 변수를 추가하여 해당 변수를 변경 후 useEffect를 통해 fetch를 진행하는 것입니다. 필터나 페이지, 커서 등 fetch시 의존할 값이 추가되어 복잡해졌을 때 useRef와 useState를 나누어 관리하는 것보다 편하게 느껴지더라구요.
번외 - flushSync
flushSync를 호출해서 React가 보류 중인 모든 작업을 강제로 처리하고 DOM을 동기적으로 업데이트할 수 있습니다.
React 18에 추가된 메서드인 flushSync를 이용하여 함수 내에서 업데이트를 할 수 있지 않을까 확인해보았으나 잘 되지 않았습니다. 관련하여 state의 업데이트가 적용되지 않는 이유를 찾아 보았으나...
flushSync를 이용하면 React DOM을 즉시 업데이트 할 수 있으나 state를 바로 업데이트를 하는 것은 아니라는 답변 뿐..ㅠㅠ 안타깝게도 상태를 동기적으로 업데이트 할 수는 없었습니다...
이상, 비슷한 문제를 겪으신 분들께 도움이 되길 바라며 글 마치겠습니다!
혹시 다른 방법으로 해결하신 경험이 있으시다면 덧글 부탁드리겠습니다!!
'SSAFYcial' 카테고리의 다른 글
[SSAFYcial] Socket.io로 10분만에 채팅 만들기 (1) | 2024.02.25 |
---|---|
[SSAFYcial] 이게 왜 안됨? ②채팅 좀 읽게 해줘... 모바일 채팅방 UI/UX 개선기 (React + typescript) (12) | 2024.02.25 |
[SSAFYcial] Zustand에 대해 알아보자 (1) | 2024.01.28 |
[SSAFYcial] 이게 왜 안됨? ①RequestParam name 오류 (2) | 2024.01.28 |
[SSA業탐구] 안드로이드 앱을 만드는 모바일 개발자편 (0) | 2023.12.24 |