0902 React & Spring: Action 함수와 무한 스크롤 구현
✅ 1. 프론트엔드(FE): React Router action을 이용한 데이터 조작
loader가 데이터 읽기(Read)를 담당했다면, action 함수는 데이터 생성(Create), 수정(Update), 삭제(Delete) 작업을 처리하는 React Router의 강력한 기능입니다. 이를 통해 컴포넌트의 역할과 데이터 조작 로직을 명확하게 분리할 수 있습니다.
➕ action 함수의 동작 원리
- 라우트 설정에
action 함수 연결: loader와 마찬가지로, createBrowserRouter의 라우트 설정 객체에 action 속성을 추가하고, 데이터 조작 로직을 담은 함수를 연결합니다.
<Form> 컴포넌트 사용: React Router가 제공하는 <Form> 컴포넌트를 사용하여 폼을 제출합니다. 이 <Form>은 일반 <form>과 달리, 페이지를 새로고침하지 않고 연결된 라우트의 action 함수로 요청을 보냅니다.
action 함수 실행: action 함수는 request 객체를 인자로 받습니다. request.formData()를 통해 폼 데이터를 쉽게 추출할 수 있습니다.
- 서버 요청 및 리다이렉트:
action 함수 내부에서 백엔드 API(e.g., POST, PUT, DELETE)를 호출합니다. 작업이 성공하면, redirect() 함수를 반환하여 사용자를 다른 페이지(e.g., 목록 페이지)로 이동시킵니다.
import { Form, redirect } from 'react-router-dom';
function EventEditPage() {
return (
<Form method="post">
<input type="text" name="title" />
<button type="submit">Save</button>
</Form>
);
}
export async function action({ request, params }) {
const formData = await request.formData();
const eventData = {
title: formData.get('title'),
};
const response = await fetch(`/api/events/${params.eventId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData),
});
return redirect('/events');
}
- 이벤트 수정/삭제: 수정 페이지는
loader를 통해 기존 데이터를 미리 불러와 폼을 채우고, action을 통해 수정 요청을 보냅니다. 삭제는 특정 버튼 클릭 시 fetch 요청을 보내는 action을 실행하도록 구현합니다.
✅ 2. 백엔드(BE): QueryDSL을 이용한 무한 스크롤 API 구현
- 사용자가 스크롤을 내릴 때마다 다음 페이지의 데이터를 동적으로 불러오는 무한 스크롤 기능을 구현하기 위해, 백엔드 API를 페이지네이션(Pagination)이 가능하도록 개선했습니다.
➕ 주요 개념
- 커서 기반 페이지네이션 (Cursor-based Pagination): "3페이지를 보여줘" 방식 대신, "ID가 10번인 이벤트 다음부터 10개를 보여줘" 방식으로 요청합니다. 이 방식은 데이터가 실시간으로 추가/삭제될 때 중복이나 누락이 발생할 가능성이 적어 무한 스크롤에 더 적합합니다.
- QueryDSL 도입: 복잡한 동적 쿼리(e.g., 커서 ID보다 작은 데이터를 조회)를 타입-세이프(Type-safe)하고 가독성 높게 작성하기 위해 QueryDSL을 설정했습니다.
- API 변경: 기존의 전체 목록 조회 API를 수정하여,
cursor (마지막으로 조회된 이벤트 ID)와 size (가져올 개수)를 파라미터로 받도록 변경했습니다. API는 요청된 조건에 맞는 데이터와 함께, 다음 페이지의 존재 여부(has-next)를 반환하여 클라이언트가 더 이상 요청을 보낼지 말지 판단할 수 있게 합니다.
✅ 3. 프론트엔드(FE): Intersection Observer를 이용한 무한 스크롤 구현
- 백엔드에서 준비된 페이지네이션 API를 활용하여, 프론트엔드에서 사용자가 스크롤을 끝까지 내렸을 때 다음 데이터를 요청하는 무한 스크롤 UI를 구현했습니다.
➕ 구현 흐름
loader에서 useEffect로 전환: loader는 페이지 진입 시 한 번만 데이터를 가져오므로, 여러 번 데이터를 요청해야 하는 무한 스크롤에는 적합하지 않습니다. 따라서 데이터 페칭 로직을 다시 컴포넌트 내부의 useEffect와 useState를 사용하는 방식으로 변경했습니다.
Intersection Observer API: 이 브라우저 API는 특정 요소(Element)가 뷰포트(화면)에 들어오거나 나가는 것을 비동기적으로 감지할 수 있습니다.
- 구현:
- 목록의 가장 마지막에 보이지 않는
<div> (감지 대상)를 둡니다.
Intersection Observer를 설정하여 이 <div>가 화면에 보이면, 다음 페이지의 데이터를 요청하는 함수를 실행하도록 합니다.
- 새로운 데이터를 받아오면, 기존 데이터 목록에 이어붙여 상태를 업데이트합니다.
- 백엔드로부터 "다음 페이지 없음" 응답을 받으면, 더 이상 Observer가 작동하지 않도록 처리합니다.
✅ 4. 사용자 경험(UX) 향상: 스켈레톤 UI (Skeleton Fallback)
- 문제점: 데이터를 불러오는 동안 사용자는 빈 화면이나 로딩 스피너만 보게 되어 답답함을 느낄 수 있습니다.
- 해결책 (스켈레톤 UI): 실제 데이터가 렌더링될 UI의 윤곽선(레이아웃)을 먼저 보여주는 기법입니다. 사용자는 데이터가 로딩되고 있음을 직관적으로 인지하고, 콘텐츠가 어떻게 채워질지 예측할 수 있어 로딩 시간을 덜 지루하게 느낍니다.
- 구현: 데이터 로딩 상태(
isLoading)일 때, 실제 컴포넌트 대신 회색 박스 등으로 구성된 스켈레톤 컴포넌트를 렌더링하도록 조건부 렌더링을 적용했습니다.
📌 요약
- React Router의
action 함수와 <Form> 컴포넌트를 사용하여, 데이터 생성/수정/삭제 로직을 컴포넌트로부터 분리하고 선언적으로 관리하도록 개선했습니다.
- 백엔드에서는 QueryDSL을 도입하여 커서 기반 페이지네이션 API를 구현함으로써, 효율적인 무한 스크롤의 기반을 마련했습니다.
- 프론트엔드에서는 브라우저의
Intersection Observer API를 활용하여, 사용자가 스크롤을 끝까지 내렸을 때 다음 데이터를 동적으로 불러오는 무한 스크롤 기능을 완성했습니다.
- 데이터 로딩 중에는 스켈레톤 UI를 보여주어, 사용자가 느끼는 대기 시간을 줄이고 전반적인 사용자 경험(UX)을 향상시켰습니다.