포스트

모달을 열 때 비로소 부르는 데이터 — 상세 지연 로딩

목록에선 요약만, 모달을 열 때 상세를 따로 조회하는 온디맨드 로딩 설계. 상세 엔드포인트 분리, 응답 페이로드 다이어트, 모달 캐싱 정책을 정리한다.

모달을 열 때 비로소 부르는 데이터 — 상세 지연 로딩

운영 화면에서 사용자 목록 옆에 “최근 활동” 같은 상세를 붙이려 할 때, 가장 게으른 설계는 목록 쿼리에 상세까지 다 조인해 한 번에 내려주는 것이다. 화면엔 50행이 보이는데, 클릭하지도 않을 50행의 상세를 미리 전부 끌어온다. 목록은 무거워지고, 정작 사용자는 한두 행만 열어본다. 이 글은 상세를 클릭한 순간에만 따로 조회하는 온디맨드 패턴을 다룬다.

핵심: 조회를 두 단계로 쪼갠다

ORM의 lazy/eager 로딩이 “엔티티 그래프를 언제 채우느냐”의 문제라면, 이건 한 단계 위 — 화면 데이터를 언제 네트워크로 가져오느냐의 문제다. 목록 응답에는 식별자와 요약만 담고, 상세는 별도 엔드포인트로 분리한다.

1
2
GET /admin/users?page=1          → [{id, name, lastLoginAt}, ...]   (요약)
GET /admin/users/{id}/activity   → {recentViews:[...], orders:[...]} (상세, 클릭 시)

목록은 가볍게 페이징되고, 상세는 사용자가 모달을 여는 행에 대해서만 1건 조회된다. 50행 × 상세 N건의 곱이 사라지고, 실제 열람한 행 수만큼만 비용이 든다.

상세 엔드포인트 설계

1
2
3
4
5
6
7
8
@GetMapping("/admin/users/{id}/activity")
public UserActivityResponse getActivity(@PathVariable Long id) {
    User user = userService.findById(id);          // 권한/존재 검증
    return UserActivityResponse.builder()
        .recentViews(activityService.recentViews(id, 20))  // 상한을 둔다
        .orders(orderService.recentOrders(id, 10))
        .build();
}

두 가지가 중요하다. 첫째, 권한 검증을 상세 조회에서도 반복한다. 목록을 볼 수 있다고 모든 행의 상세를 볼 수 있는 건 아니다. 식별자만 바꿔 다른 사용자의 상세를 부르는 접근(IDOR)을 막아야 한다. 둘째, 상세에도 건수 상한(20, 10)을 둔다. “최근”이라는 말 뒤에 무한 목록이 숨어 있으면 모달 하나가 응답을 폭발시킨다.

sequenceDiagram
  participant B as 브라우저
  participant S as 서버
  B->>S: GET /users?page=1 (요약만)
  S-->>B: 50행 요약
  Note over B: 사용자가 한 행 클릭
  B->>S: GET /users/42/activity
  S-->>B: 42번 상세 (권한 검증 후)

캐싱: 같은 모달을 다시 열 때

같은 행의 모달을 닫았다 다시 열면 또 서버를 부를 필요가 있을까. 클라이언트에서 식별자 기준으로 응답을 짧게 캐시하면 재요청이 사라진다. 다만 상세가 자주 바뀌는 데이터(예: 실시간 활동)면 캐시가 오히려 낡은 화면을 보여준다. 정적이면 캐시, 실시간이면 매번 조회 — 데이터의 변동성에 맞춰 정한다. 서버 측은 응답에 적절한 Cache-Control을 실어 클라이언트 캐시 수명을 명시하는 편이 안전하다.

운영 함정

  • 빠른 연속 클릭의 경쟁 상태. 사용자가 A행을 열고 곧바로 B행을 열면, A의 응답이 늦게 도착해 B 모달에 A 데이터가 그려질 수 있다. 요청에 토큰/식별자를 달아 “지금 열린 모달의 것”이 아니면 응답을 버려야 한다.
  • 모달을 안 닫고 목록을 갱신. 목록을 다시 불러도 열려 있던 모달의 상세는 그대로다. 모달이 참조하는 행이 삭제되면 상세 엔드포인트가 404를 줄 수 있으니, 상세 조회 실패를 모달에서 우아하게 처리해야 한다.

핵심 요약

  • 목록은 요약, 상세는 별도 엔드포인트. 곱셈(행 × 상세)을 덧셈(열람한 행 수)으로 바꾼다.
  • 상세 조회마다 권한을 재검증하고 건수 상한을 둔다.
  • 캐싱은 데이터 변동성에 맞춰. 연속 클릭의 응답 순서 역전을 식별자로 방어한다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.