TIL - 20250902

juni·2025년 9월 2일

TIL

목록 보기
113/316

0902 React & Spring: Action 함수와 무한 스크롤 구현


✅ 1. 프론트엔드(FE): React Router action을 이용한 데이터 조작

  • loader가 데이터 읽기(Read)를 담당했다면, action 함수는 데이터 생성(Create), 수정(Update), 삭제(Delete) 작업을 처리하는 React Router의 강력한 기능입니다. 이를 통해 컴포넌트의 역할과 데이터 조작 로직을 명확하게 분리할 수 있습니다.

action 함수의 동작 원리

  1. 라우트 설정에 action 함수 연결: loader와 마찬가지로, createBrowserRouter의 라우트 설정 객체에 action 속성을 추가하고, 데이터 조작 로직을 담은 함수를 연결합니다.
  2. <Form> 컴포넌트 사용: React Router가 제공하는 <Form> 컴포넌트를 사용하여 폼을 제출합니다. 이 <Form>은 일반 <form>과 달리, 페이지를 새로고침하지 않고 연결된 라우트의 action 함수로 요청을 보냅니다.
  3. action 함수 실행: action 함수는 request 객체를 인자로 받습니다. request.formData()를 통해 폼 데이터를 쉽게 추출할 수 있습니다.
  4. 서버 요청 및 리다이렉트: action 함수 내부에서 백엔드 API(e.g., POST, PUT, DELETE)를 호출합니다. 작업이 성공하면, redirect() 함수를 반환하여 사용자를 다른 페이지(e.g., 목록 페이지)로 이동시킵니다.
// pages/EventEdit.js
import { Form, redirect } from 'react-router-dom';

function EventEditPage() {
  return (
    // 2. React Router의 Form 컴포넌트 사용
    <Form method="post">
      <input type="text" name="title" />
      <button type="submit">Save</button>
    </Form>
  );
}

// 1. 라우트 설정에 action 함수 연결
export async function action({ request, params }) {
  // 3. 폼 데이터 추출
  const formData = await request.formData();
  const eventData = {
    title: formData.get('title'),
    // ...
  };

  // 4. 서버에 데이터 전송
  const response = await fetch(`/api/events/${params.eventId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(eventData),
  });

  // 5. 작업 완료 후 리다이렉트
  return redirect('/events');
}
  • 이벤트 수정/삭제: 수정 페이지는 loader를 통해 기존 데이터를 미리 불러와 폼을 채우고, action을 통해 수정 요청을 보냅니다. 삭제는 특정 버튼 클릭 시 fetch 요청을 보내는 action을 실행하도록 구현합니다.

✅ 2. 백엔드(BE): QueryDSL을 이용한 무한 스크롤 API 구현

  • 사용자가 스크롤을 내릴 때마다 다음 페이지의 데이터를 동적으로 불러오는 무한 스크롤 기능을 구현하기 위해, 백엔드 API를 페이지네이션(Pagination)이 가능하도록 개선했습니다.

➕ 주요 개념

  1. 커서 기반 페이지네이션 (Cursor-based Pagination): "3페이지를 보여줘" 방식 대신, "ID가 10번인 이벤트 다음부터 10개를 보여줘" 방식으로 요청합니다. 이 방식은 데이터가 실시간으로 추가/삭제될 때 중복이나 누락이 발생할 가능성이 적어 무한 스크롤에 더 적합합니다.
  2. QueryDSL 도입: 복잡한 동적 쿼리(e.g., 커서 ID보다 작은 데이터를 조회)를 타입-세이프(Type-safe)하고 가독성 높게 작성하기 위해 QueryDSL을 설정했습니다.
  3. API 변경: 기존의 전체 목록 조회 API를 수정하여, cursor (마지막으로 조회된 이벤트 ID)와 size (가져올 개수)를 파라미터로 받도록 변경했습니다. API는 요청된 조건에 맞는 데이터와 함께, 다음 페이지의 존재 여부(has-next)를 반환하여 클라이언트가 더 이상 요청을 보낼지 말지 판단할 수 있게 합니다.

✅ 3. 프론트엔드(FE): Intersection Observer를 이용한 무한 스크롤 구현

  • 백엔드에서 준비된 페이지네이션 API를 활용하여, 프론트엔드에서 사용자가 스크롤을 끝까지 내렸을 때 다음 데이터를 요청하는 무한 스크롤 UI를 구현했습니다.

➕ 구현 흐름

  1. loader에서 useEffect로 전환: loader는 페이지 진입 시 한 번만 데이터를 가져오므로, 여러 번 데이터를 요청해야 하는 무한 스크롤에는 적합하지 않습니다. 따라서 데이터 페칭 로직을 다시 컴포넌트 내부의 useEffectuseState를 사용하는 방식으로 변경했습니다.
  2. Intersection Observer API: 이 브라우저 API는 특정 요소(Element)가 뷰포트(화면)에 들어오거나 나가는 것을 비동기적으로 감지할 수 있습니다.
  3. 구현:
    • 목록의 가장 마지막에 보이지 않는 <div> (감지 대상)를 둡니다.
    • Intersection Observer를 설정하여 이 <div>가 화면에 보이면, 다음 페이지의 데이터를 요청하는 함수를 실행하도록 합니다.
    • 새로운 데이터를 받아오면, 기존 데이터 목록에 이어붙여 상태를 업데이트합니다.
    • 백엔드로부터 "다음 페이지 없음" 응답을 받으면, 더 이상 Observer가 작동하지 않도록 처리합니다.

✅ 4. 사용자 경험(UX) 향상: 스켈레톤 UI (Skeleton Fallback)

  • 문제점: 데이터를 불러오는 동안 사용자는 빈 화면이나 로딩 스피너만 보게 되어 답답함을 느낄 수 있습니다.
  • 해결책 (스켈레톤 UI): 실제 데이터가 렌더링될 UI의 윤곽선(레이아웃)을 먼저 보여주는 기법입니다. 사용자는 데이터가 로딩되고 있음을 직관적으로 인지하고, 콘텐츠가 어떻게 채워질지 예측할 수 있어 로딩 시간을 덜 지루하게 느낍니다.
  • 구현: 데이터 로딩 상태(isLoading)일 때, 실제 컴포넌트 대신 회색 박스 등으로 구성된 스켈레톤 컴포넌트를 렌더링하도록 조건부 렌더링을 적용했습니다.

📌 요약

  • React Router의 action 함수<Form> 컴포넌트를 사용하여, 데이터 생성/수정/삭제 로직을 컴포넌트로부터 분리하고 선언적으로 관리하도록 개선했습니다.
  • 백엔드에서는 QueryDSL을 도입하여 커서 기반 페이지네이션 API를 구현함으로써, 효율적인 무한 스크롤의 기반을 마련했습니다.
  • 프론트엔드에서는 브라우저의 Intersection Observer API를 활용하여, 사용자가 스크롤을 끝까지 내렸을 때 다음 데이터를 동적으로 불러오는 무한 스크롤 기능을 완성했습니다.
  • 데이터 로딩 중에는 스켈레톤 UI를 보여주어, 사용자가 느끼는 대기 시간을 줄이고 전반적인 사용자 경험(UX)을 향상시켰습니다.

0개의 댓글