추가 기능 확장 제안
북스토어 MVP 이후 인증·검색 필터·리뷰·추천·SEO·자동 배포 후보를 사용자 가치와 운영 비용으로 우선순위화합니다.
앞의 글들에서 Next.js로 온라인 북스토어의 핵심 기능을 기획하고 설계하고 구현하는 과정을 살펴봤습니다. MVP(Minimum Viable Product)를 배포했다면, 이제 사용자 경험과 서비스 가치를 높일 추가 기능을 넓혀 갈 단계입니다. 이 글에서는 프로젝트를 더 발전시킬 확장 기능의 아이디어와 구현 방향을 함께 제시하고, 각 기능을 넣기 전에 정해야 할 조건을 살펴봅니다.
사용자 인증 및 권한 관리
17장 2절의 코드는 Auth.js 로그인과 세션의 공급자 계정 ID로 장바구니와 주문의 조회를 제한합니다. 이는 제시한 코드의 범위일 뿐, 실제 배포와 외부 연동, 동시 요청까지 검증을 마쳤다는 뜻은 아닙니다. 이제 계정 정보, 역할, 프로필을 데이터베이스 모델과 연결해 인증을 실제 서비스 수준으로 넓힐 차례입니다.
로그인과 계정을 넓히려면 먼저 OAuth 계정과 애플리케이션의 사용자 모델을 연결합니다. 이메일과 비밀번호 가입이 필요하다면 비밀번호 해시와 계정 복구 흐름을 따로 설계하고, 로그인 페이지에는 공급자별 오류와 다시 시도하는 UI를 더합니다. 브라우저에서 세션이 필요한 UI만 SessionProvider 안에 두어 useSession으로 읽고, 서버 컴포넌트에서는 auth()로 세션 정보에 접근합니다.
사용자 프로필 페이지에서는 사용자가 자신의 주문 내역, 위시리스트, 개인 정보를 확인할 수 있게 합니다. 서버 컴포넌트에서 사용자별 데이터를 가져오고, 데이터를 고치는 일은 클라이언트 컴포넌트에서 서버 액션을 불러 처리합니다.
관리자 페이지에서는 도서 정보의 추가, 수정, 삭제와 주문 관리처럼 관리자만 접근할 수 있는 기능을 구현합니다. 사용자 모델에 role 필드를 더해 admin 같은 역할을 주고, Route Handler나 서버 액션에서 요청마다 사용자의 role을 확인해 권한 없는 접근을 막습니다. Proxy(proxy.ts)로 특정 경로의 접근을 빠르게 확인하고 리다이렉트할 수도 있지만, 최종 권한 검사는 데이터를 다루는 곳에서 다시 합니다.
검색 및 필터링 기능 강화
지금은 제목과 저자로만 검색할 수 있으므로, 사용자가 원하는 책을 더 쉽게 찾도록 검색과 필터를 넓힐 수 있습니다. 고급 검색 옵션의 후보는 다음과 같습니다.
- 장르별 필터: 도서 목록 페이지에 장르를 고르는 체크박스나 드롭다운을 추가
- 가격 범위 필터: 최소와 최대 가격을 입력해 그 범위 안의 도서만 표시
- 정렬: 가격 오름차순과 내림차순, 최신순, 인기순 등으로 정렬하는 옵션 추가
클라이언트에서는 useSearchParams로 지금의 URL 쿼리를 읽고, 바꿀 때는 Link나 useRouter의 탐색으로 반영합니다. 이 훅이 돌려준 객체는 읽기 전용입니다. 서버 컴포넌트에서는 이 쿼리 파라미터를 바탕으로 데이터베이스 쿼리를 만들며, MongoDB의 $match, $sort 같은 집계 파이프라인 연산자를 활용할 수 있습니다.
검색어 자동 완성(autocomplete)은 사용자가 검색어를 입력할 때 관련 추천 검색어를 실시간으로 보여 주어 편의를 높입니다. 클라이언트 컴포넌트에서 useState, useEffect, 디바운스로 입력을 다루고, Route Handler에서 검색어로 데이터베이스를 조회하거나 캐시해 둔 검색어 목록을 돌려줍니다.
가격 범위는 문자열 입력부터 검증해야 합니다. URL의 min과 max는 문자열이고 사용자가 직접 바꿀 수 있습니다. 다음 순수 함수는 원 단위의, 음수가 아닌 안전한 정수만 허용합니다. 빠진 최솟값은 0, 빠진 최댓값은 Number.MAX_SAFE_INTEGER로 두고, 빈 값, 중복된 파라미터, 소수, 뒤바뀐 범위는 거절하는 예제 정책입니다.
export function parsePriceRange(params: URLSearchParams) {
function read(key: string, fallback: number): number | null {
const values = params.getAll(key);
if (values.length === 0) return fallback;
if (values.length !== 1 || !/^\d+$/.test(values[0])) return null;
const value = Number(values[0]);
return Number.isSafeInteger(value) ? value : null;
}
const min = read('min', 0);
const max = read('max', Number.MAX_SAFE_INTEGER);
return min === null || max === null || min > max ? null : { min, max };
}min=10000&max=20000은 양 끝을 포함하는 범위 조건으로 쓸 수 있고, 결과가 null이면 사용자에게 조건 오류를 알리고 DB 검색을 하지 않는 흐름을 설계합니다. 서버 페이지는 자신의 searchParams 입력을 읽어 검증하며 클라이언트 훅을 부르지 않습니다. 위 함수는 별도의 URLSearchParams 객체를 받는 입력 형식의 예제입니다. 정렬값도 허용한 선택지 가운데 골라 DB 쿼리에 넘겨야 하며, 임의의 요청 문자열을 쿼리 코드로 실행하지 않습니다.
사용자 인터랙션 및 경험 개선
애플리케이션의 사용성을 높이고 더 풍부한 경험을 주는 기능들입니다.
위시리스트(찜하기)는 사용자가 구매를 고민하는 도서를 저장해 두는 기능입니다. userId와 bookId를 잇는 Wishlist 모델을 만들고, 도서 상세 페이지에 위시리스트에 추가하는 버튼을 서버 액션으로 구현하며, 사용자 프로필에 위시리스트 목록 페이지를 둡니다.
도서 리뷰와 평점은 사용자가 산 도서에 리뷰를 쓰고 평점을 매기게 합니다. 작성자 userId, bookId, rating, comment를 담은 Review 모델을 만들고, 도서 상세 페이지에 리뷰 작성 폼과 기존 리뷰 목록을 보여 주며, 리뷰는 서버 액션으로 저장하고 도서마다 평균 평점을 계산해 보여 줍니다. 서버에서는 검증한 작성자 ID와 구매 이력을 확인하고 평점을 1부터 5까지의 정수로 제한하며, 요청 본문의 userId만으로 작성자를 정하지 않습니다. 리뷰가 없을 때는 "아직 평점 없음"으로 표시할 수 있고, 평균은 집단의 평균을 다시 단순 평균하지 말고 전체 평점의 합과 건수로 계산합니다.
실시간 재고 알림이나 푸시 알림은 품절된 도서가 들어오거나 위시리스트에 담은 도서의 가격이 바뀔 때 사용자에게 알려 줍니다. 푸시 알림은 직접 등록한 서비스 워커에서 Web Push API를 처리하고, 백엔드 서버가 VAPID 키로 구독 대상에 메시지를 보내며(16장 2절 참조), 실시간 재고나 가격 변화를 곧바로 반영하려면 별도의 WebSocket 서버를 씁니다(16장 4절 참조).
결제 시스템 연동은 Stripe, Toss Payments, PortOne 같은 PG(Payment Gateway) API로 실제 결제를 처리합니다. 카드 정보 입력 같은 결제 UI는 클라이언트에서 처리하고, Route Handler에서 결제 요청의 생성과 승인을 처리해 민감한 정보를 클라이언트에 드러내지 않습니다. 서버가 확인한 결제 결과로 주문 상태를 갱신하며, 중복 알림이나 재시도에서도 결제와 재고 변경이 두 번 일어나지 않도록 처리합니다. 지금의 가상 주문은 이미 재고를 줄이므로, 실제 결제 흐름에 그대로 덧붙여 재고를 두 번 줄이지 않도록 상태 전이를 다시 설계해야 합니다.
결제의 성공, 중복 처리, 동시성은 따로 확인합니다. 결제 완료 웹훅은 결제 서비스가 보낸 요청인지 공급자의 공식 검증 절차로 확인하고, 주문 ID, 금액, 통화를 서버의 주문과 비교합니다. Stripe의 경우 서명 검증에 원본 요청 본문이 필요하므로, 먼저 JSON으로 바꾼 값을 그대로 서명 검증에 넘기지 않습니다. 같은 이벤트 ID뿐 아니라 같은 결제의 다른 이벤트가 와도, 주문의 같은 paid 전이와 재고 변경을 중복으로 실행하지 않아야 합니다.
예를 들어 재고 차감을 pending에서 paid로 바뀌는 전이에 한 번만 묶기로 설계했다면, 성공 알림이 두 번 와도 재고는 100에서 98로 한 번만 바뀝니다. 이때는 기존 가상 주문 시점의 차감을 없애거나 예약 정책으로 다시 설계했다는 전제가 필요하며, 이미 98로 줄인 주문에 paid 전이를 붙여 다시 줄이면 여전히 중복입니다. 메모리의 Set이나 확인한 뒤 따로 쓰는 방식만으로는 여러 서버와 동시 요청에서 한 번만 실행되도록 보장할 수 없으므로, 영속 식별자, 고유 제약, 원자적인 상태 변경을 포함한 저장 정책과 실패 복구를 정해야 합니다.
성능 및 SEO 최적화 심화
기본적인 최적화를 넘어, 더 높은 성능과 검색 엔진 노출을 위한 고급 기법을 적용합니다.
ISR(Incremental Static Regeneration)로는 인기 도서나 자주 바뀌지 않는 상세 정보를 캐시하고, 허용할 오래된 응답의 범위를 정합니다. 주문 액션에서는 가격과 재고를 서버에서 다시 확인합니다. cacheComponents를 쓰지 않는 모델에서는 정적 생성 조건을 갖춘 경로에 revalidate를 설정하고, cacheComponents를 켰다면 그 모델의 use cache와 수명 설정으로 설계하며 두 방식의 세그먼트 설정을 섞지 않습니다.
SEO는 App Router의 정적 metadata 객체나 generateMetadata 함수로 제목, 설명, og:image 등을 설정하며, next/head는 Pages Router의 방식입니다. 도서, 상품, 리뷰 같은 구조화된 데이터를 Schema.org의 JSON-LD 형식으로 페이지에 넣으면 검색 엔진이 콘텐츠를 더 잘 이해하게 도울 수 있습니다. 크롤러가 사이트를 효율적으로 탐색하고 색인하도록 sitemap.xml과 robots.txt를 만들되, robots.txt가 접근 권한 검사나 비밀 URL 보호를 대신하지는 않습니다. 이미지, CSS, JS 같은 정적 자산은 CDN으로 제공해 사용자에게 더 빨리 전하며, Vercel은 기본적으로 CDN을 씁니다.
테스트 및 배포 자동화
안정적인 서비스 운영에는 테스트와 배포의 자동화가 꼭 필요합니다. Jest와 React Testing Library로 개별 컴포넌트와 유틸리티 함수의 동작을 검증하는 단위 테스트, 여러 컴포넌트와 모듈이 함께 동작하는지 보는 통합 테스트, Cypress나 Playwright로 실제 사용자 시나리오에 따라 애플리케이션 전체를 검증하는 E2E 테스트를 갖춥니다. 그리고 GitHub Actions나 Vercel의 Git 연동으로 코드를 저장소에 푸시할 때마다 테스트를 자동으로 실행하고, 성공하면 배포까지 이어지는 CI/CD 파이프라인을 만듭니다.
이 추가 기능들은 온라인 북스토어를 실제 서비스에 더 가깝게 넓힐 때 참고할 항목입니다. 기능마다 먼저 갖춰야 할 조건이 다릅니다. 프로필과 리뷰에는 안정적인 계정과 소유권 검사가, 실제 결제에는 주문 상태와 중복 처리, 환불 정책이 먼저 필요합니다. 원하는 사용자 가치와 감당할 운영 부담을 확인한 뒤 한 흐름씩 넓혀 갑니다.
확장 순서는 작은 완료 조건으로 정합니다. 검색 필터는 유효한 범위, 빈 결과, 잘못된 조건을, 리뷰는 구매하지 않은 사용자의 거절, 본인 수정, 평점 범위를, 결제는 금액 불일치, 중복, 취소 뒤의 재시도를 포함해 한 흐름의 완료 조건을 먼저 적습니다. 버튼이 보이고 단위 함수가 통과한 상태와, 실제 인증, DB, 공급자를 연결해 통합한 성공은 구분합니다. 자동 배포도 필요한 검사가 성공했다는 결과를 전하는 절차일 뿐, 외부 결제의 중복이나 환불 시나리오까지 자동으로 검증되었다는 뜻은 아닙니다.
정리
- 인증은 OAuth 계정을 사용자 모델과 연결하고, 프로필, 관리자 페이지, 역할 검사로 넓히며, 최종 권한 검사는 데이터를 다루는 서버 코드에서 합니다.
- 검색 필터는 URL 쿼리를 서버에서 검증해 허용한 조건만 DB 쿼리에 넘기며, 잘못된 범위는 검색 전에 거절합니다.
- 리뷰의 평균은 전체 평점의 합과 건수로 계산하고, 작성자와 구매 이력은 서버 세션으로 확인합니다.
- 실제 결제는 웹훅을 공식 절차로 검증하고, 결제와 재고 변경이 한 번만 일어나도록 상태 전이와 영속 식별자를 설계합니다.
- 기능마다 먼저 갖출 조건과 작은 완료 기준을 정하고, 단위 검사의 통과와 실제 연동의 성공을 구분합니다.
연습 문제
1. 확장 전에 실패 조건 정하기
가상 주문에서 재고 100권을 2권 줄여 98권으로 만들었다고 합시다. 실제 결제를 더하면서 같은 주문의 결제 완료 알림을 두 번 받았는데, 알림마다 다시 2권을 줄이면 재고는 얼마가 될까요? 기존 가상 주문의 처리까지 포함해 어디를 다시 설계해야 할까요?
해설 보기
기존 차감 뒤 두 알림에서 각각 줄이면 98, 96, 94가 되어, 한 주문의 수량 2를 세 번 반영합니다. 결제와 주문의 상태 전이를 먼저 정하고, 같은 주문과 결제의 중복 알림이 같은 업무 변경을 다시 실행하지 않도록 식별자와 서버 검증을 써야 합니다. 기존 차감을 유지할지, 예약과 결제 확정으로 나누어 바꿀지도 함께 정합니다.
알림을 받았다는 사실만으로 클라이언트가 보낸 결제 성공 값을 믿지 않습니다. 실패, 중복, 취소, 환불을 각각 어떤 상태로 기록하고 사용자에게 무엇을 보여 줄지 정한 뒤 구현합니다. 새 기능의 화면을 더하는 일과 기존 주문의 일관성을 지키는 일은 함께 검토해야 합니다.
2. 가격 범위 함수의 입력
parsePriceRange에 빈 쿼리, min=10000&max=20000, min=20000&max=10000, min=&max=20000, min=1&min=2를 넣습니다. 각 결과와 서버가 검색을 진행할 조건을 판단해 보세요. useSearchParams()가 돌려준 객체에 직접 set을 불러 URL을 바꾸는 방식도 가능할까요?
해설 보기
빈 쿼리는 { min: 0, max: Number.MAX_SAFE_INTEGER }, 정상 범위는 { min: 10000, max: 20000 }이며, 나머지 세 경우는 null입니다. 뒤바뀐 범위, 빈 값, 중복 입력을 예제 정책에서 거절하기 때문입니다. 돌려받은 범위로 양 끝을 포함하는 DB 조건을 적용하고, null이면 조건 오류로 처리합니다.
useSearchParams()의 객체는 읽기 전용이므로 직접 고치지 않습니다. new URLSearchParams(searchParams.toString())처럼 복사한 쿼리를 바꾼 뒤 Link나 router의 탐색으로 반영합니다.
3. 평균 평점 계산하기
리뷰 2개는 각각 5점이고, 다른 리뷰 3개는 각각 1점입니다. 전체 평균은 얼마일까요? 두 집단 평균의 단순 평균을 쓰면 왜 틀릴까요? 리뷰가 없는 책에 0점이라는 실제 평가가 있었다고 표시해도 될까요?
해설 보기
전체 합은 2 × 5 + 3 × 1 = 13이고 건수는 5이므로 2.6점입니다. 집단 평균만으로 (5 + 1) ÷ 2 = 3을 계산하면 집단마다 리뷰 수가 다르다는 점을 무시하게 됩니다. 리뷰가 없는 것은 평가가 없는 상태이므로, 실제 0점 리뷰와 구분해 "아직 평점 없음"이나 건수 0처럼 표현합니다.
4. 상태 전이에 한 번만 묶은 재고 차감
기존의 주문 생성 시점 차감은 없앴고, 검증된 pending에서 paid로의 전이에만 수량 2를 한 번 줄이도록 설계했습니다. 처음 재고 100에서 같은 결제의 성공 알림 두 개를 받아도 상태 전이가 한 번만 적용된다면 최종 재고는 얼마일까요? 메모리의 Set만 쓰거나, 동시 요청 둘이 pending을 먼저 읽었을 때는 어떤 문제가 남을까요?
해설 보기
98입니다. 주어진 정책은 한 주문의 상태 전이 한 번에만 재고를 줄이며, 기존의 생성 시점 차감도 없다는 조건입니다.
메모리의 Set은 재시작하거나 다른 인스턴스에서는 기록을 함께 쓰지 못하고, 읽은 뒤 따로 쓰면 두 요청이 모두 pending을 보고 둘 다 재고를 줄일 수 있습니다. DB에서 상태 전이와 재고 변경을 원자적으로 처리하고 영속적인 중복 식별 정책을 적용한 뒤, 동시 요청, 재시도, 부분 실패를 검증해야 합니다. 알림 두 개를 차례로 계산하는 예만으로 그 통합이 확인되지는 않습니다.
5. UI만 완성된 리뷰 기능
리뷰 버튼과 평점 입력 UI는 완성했지만, 서버에서 구매 이력과 작성자 권한을 검사하지 않습니다. 구매하지 않은 사용자의 요청과 다른 사람의 리뷰를 고치는 요청도 허용된다면, 기능이 완료되었다고 판단할 수 있을까요? 다음에 고치고 검증할 것의 우선순위를 제안해 보세요.
해설 보기
완료가 아닙니다. 먼저 서버에서 검증한 계정으로 구매 이력, 리뷰 소유권, 평점 범위를 확인하고, 거절하면 저장하지 않게 합니다. 그다음 정상 구매자의 작성과 본인 수정의 성공, 그리고 구매하지 않은 사용자, 다른 사람의 수정, 범위 밖 평점의 거절을 각각 확인합니다. UI 확인과 순수 검증 함수의 테스트 뒤에, 실제 인증과 DB를 연결한 통합 검증을 이어 갑니다.
Stripe 웹훅의 서명 검증은 Stripe 웹훅 문서에서, 구조화된 데이터는 Google 구조화된 데이터 안내에서 확인할 수 있습니다.
이것으로 17장을 마칩니다. 17장에서는 온라인 북스토어를 기획하고 설계한 뒤, 인증, 도서, 장바구니, 주문, 검색, 배포를 차례로 구현하고, 코드 리뷰와 최적화, 확장 방향까지 정리했습니다. 다음 장의 일반적인 오류 및 해결 방법부터는 Next.js 개발에서 자주 만나는 오류와 디버깅 방법을 알아봅니다.