API
API 페이지네이션: 응답 코드를 봐야 하는 이유
API 페이지네이션을 구현할 때 HTTP 상태 코드를 확인해야 하는 이유와 방법을 정리했다.
API 페이지네이션을 구현할 때 보통 마지막 페이지에 도달하는 방법이 뭔지 생각해본다. 기술적으로는 여러 방법이 있고, 선택에 따라 클라이언트가 처리하는 방식도 달라진다.
HTTP 상태 코드로 상황 알려주기
많은 API는 200으로만 응답한다. 하지만 상황에 따라 다르게 응답하면 클라이언트가 처리하기 쉬워진다:
// 요청
GET /api/items?page=2&limit=10
// 응답 헤더
HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 100
X-Total-Pages: 10
X-Has-More: true
// 응답 본문
{
"data": [...],
"pagination": {
"page": 2,
"limit": 10,
"total": 100,
"totalPages": 10
}
}
X-Total-Count 헤더와 본문의 pagination 객체 둘 다 정보를 담는다. 선택은 API 설계에 따라 다르다.
요청 파라미터 검증
클라이언트에서 잘못된 페이지를 요청할 수 있다:
# 존재하지 않는 페이지 요청
GET /api/items?page=999&limit=10
이때 응답할 방법:
- 200으로 빈 배열 응답: 클라이언트가 더 이상 데이터가 없다고 알아야 함
- 404로 응답: 페이지가 없다는 의미
- 400으로 응답: 잘못된 요청이라는 의미
대부분은 200으로 빈 배열을 응답하는 것이 자연스럽다:
// Node.js Express 예시
app.get('/api/items', (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
if (page < 1 || limit < 1) {
return res.status(400).json({ error: 'Invalid pagination parameters' });
}
const offset = (page - 1) * limit;
const items = db.items.find().skip(offset).limit(limit);
const total = db.items.count();
res.json({
data: items,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasMore: offset + items.length < total,
},
});
});
응답 형식 일관성 유지
# 첫 페이지
curl 'https://api.example.com/api/items?page=1'
# 200 OK, 10개 데이터 반환
# 마지막 페이지 (10개 미만)
curl 'https://api.example.com/api/items?page=10'
# 200 OK, 5개 데이터 반환
# 범위 초과
curl 'https://api.example.com/api/items?page=11'
# 200 OK, 빈 배열 [] 반환
응답 형식이 항상 일정하면 클라이언트 코드가 단순해진다:
// 클라이언트
const response = await fetch(`/api/items?page=${page}`);
const { data, pagination } = await response.json();
if (data.length === 0) {
console.log('No more items');
} else {
// 데이터 표시
}
성능을 고려한 메타데이터
X-Total-Count 헤더는 계산 비용이 크다. 특히 데이터가 많을 때는 전체 개수를 세는 것이 느리다:
-- 느림 (특히 대용량 테이블)
SELECT COUNT(*) FROM items; -- 전체 개수 세기
선택지:
- X-Total-Count를 포함하지 않음: 클라이언트가 "다음 페이지 있음" 버튼만 표시 가능
- 근사값 제공: 마지막 집계된 통계 사용
- 캐시 사용: Redis에서 개수 조회
// Redis 캐시 예시
const cacheKey = `items:total-count`;
let total = await redis.get(cacheKey);
if (!total) {
total = await db.items.count();
await redis.setex(cacheKey, 3600, total); // 1시간 캐시
}
클라이언트에서 처리하는 방법
// 무한 스크롤 구현
let page = 1;
let hasMore = true;
async function loadMore() {
const response = await fetch(`/api/items?page=${page}`);
const { data, pagination } = await response.json();
items.push(...data);
hasMore = pagination.hasMore;
page++;
if (!hasMore) {
// 로딩 버튼 숨기기
}
}
API 페이지네이션은 응답 형식을 명확히 하면 클라이언트와 서버 모두 처리하기 쉬워진다.