전역 레이아웃 및 배포
공통 레이아웃을 연결하고 서점의 작은 학습 완료 흐름, 직접 UI 과제, 로컬·배포 확인 항목을 구분합니다.
구현 가이드의 마지막 글입니다. 이 글에서는 모든 페이지에 공통으로 쓰는 레이아웃을 연결하고, 작은 서점 흐름이 어디까지 완성되었는지 확인할 기준과 직접 마칠 UI 과제를 정리한 뒤, 로컬 확인과 Vercel 배포로 마무리합니다.
전역 레이아웃 및 내비게이션
헤더와 푸터처럼 모든 페이지에 공통으로 적용할 레이아웃을 구성하고, 내비게이션 바를 추가합니다. 먼저 생성기가 만든 루트 레이아웃에 헤더와 푸터를 더합니다.
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import './globals.css';
import Header from '@/components/Header'; // Header 컴포넌트
import Footer from '@/components/Footer'; // Footer 컴포넌트
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: '나만의 온라인 북스토어',
description: 'Next.js로 만든 간단한 온라인 북스토어 프로젝트',
};
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
return (
<html lang="ko">
<body className={`${inter.className} flex flex-col min-h-screen bg-gray-100`}>
<Header />
<div className="grow">
{children}
</div>
<Footer />
</body>
</html>
);
}루트 레이아웃은 <body>를 세로 방향 flex로 만들고 min-h-screen으로 화면 높이 이상을 잡은 뒤, 페이지 내용을 감싼 div에 grow를 줍니다. 그래서 내용이 짧은 페이지에서도 남는 높이를 가운데 영역이 차지해, 푸터가 화면 맨 아래에 붙습니다.
생성기가 만든 app/globals.css에는 body의 배경, 글자색, 글꼴을 정하는 규칙이 레이어 밖에 있습니다. Tailwind CSS 4의 유틸리티는 @layer utilities 안에 들어가고 레이어 밖의 규칙이 레이어 안의 규칙보다 우선하므로, 이 규칙을 그대로 두면 bg-gray-100이 적용되지 않습니다. 회색 배경을 쓰려면 globals.css에서 이 body 규칙을 지웁니다.
헤더에는 도서 목록, 장바구니, 로그인으로 가는 링크를 둡니다. 이 헤더는 세션을 읽지 않으므로 로그인한 뒤에도 같은 메뉴를 보여 주며, 로그인 상태에 따라 메뉴를 바꾸는 일은 뒤의 직접 UI 과제로 남겨 둡니다.
import Link from 'next/link';
export default function Header() {
return (
<header className="bg-white shadow-md py-4">
<div className="container mx-auto px-4 flex justify-between items-center">
<Link href="/" className="flex items-center space-x-2">
<span aria-hidden="true" className="text-2xl">📚</span>
<span className="text-2xl font-bold text-gray-800">My Bookstore</span>
</Link>
<nav>
<ul className="flex space-x-6">
<li>
<Link href="/books" className="text-gray-600 hover:text-blue-600 transition-colors">
모든 책
</Link>
</li>
<li>
<Link href="/cart" className="text-gray-600 hover:text-blue-600 transition-colors">
장바구니
</Link>
</li>
<li>
<Link href="/login" className="text-gray-600 hover:text-blue-600 transition-colors">
로그인
</Link>
</li>
</ul>
</nav>
</div>
</header>
);
}푸터는 간단한 저작권 문구를 보여 줍니다. 연도는 코드에 고정하지 않고 new Date().getFullYear()로 계산합니다.
export default function Footer() {
return (
<footer className="border-t bg-white py-6 text-center text-sm text-gray-600">
<p>© {new Date().getFullYear()} My Bookstore</p>
</footer>
);
}푸터는 서버 컴포넌트이므로 이 연도는 브라우저가 아니라 서버에서 렌더링할 때 계산됩니다. 그래서 빌드할 때 미리 렌더링되는 페이지에는 빌드한 해의 연도가 HTML에 들어가고, 해가 바뀌어도 다시 빌드하거나 다시 렌더링하기 전까지는 이전 연도가 남을 수 있습니다.
작은 서점 흐름의 완료 범위
첫 버전은 로그인, 도서 목록과 상세, 자기 장바구니, 가상 주문까지로 정합니다. 생성기의 홈 화면이 남아 있어도 /login과 /books에서 시작할 수 있으며, 전체 내비게이션까지 완성하려면 아래의 직접 UI 과제도 이어서 마칩니다.
처음 비교할 값은 새 독립 실습 DB에 시드를 처음 넣고, 다른 주문이나 가격 변경 없이 시작한 경우로 정합니다. 이때 Next.js 완벽 가이드의 가격은 35,000원, 재고는 100이고, 계정 A의 장바구니는 비어 있어야 합니다. 기존 데이터로 시작한다면 지금의 가격, 재고, 장바구니를 먼저 기록해 두고 그 값에서의 변화를 비교합니다. 결과를 다시 맞추려고 기존 장바구니와 주문이 있는 DB에 도서를 다시 시드하지 않습니다.
흐름은 다음 순서로 확인합니다. 먼저 /login의 GitHub 버튼으로 로그인합니다. 서버 세션의 소유자 ID는 인증에서 확인한 provider:providerAccountId이며, 이메일이나 브라우저가 보낸 ID로 바꾸지 않습니다. 다음으로 /books에서 그 도서의 상세 페이지로 이동해 제목, 가격, 재고를 확인하며, 이는 도서 목록·상세·검색의 코드와 이어집니다.
그다음 "장바구니에 추가"를 누르고 /cart에서 수량을 2로 바꿉니다. 합계가 70,000원인지 보고, 장바구니 구현에서 다룬 자기 항목만 조회하는 조건을 확인합니다. 담거나 수량을 바꾸는 것만으로는 도서 재고가 줄지 않습니다. 이어서 /order에서 같은 항목과 예상 금액을 확인하고 가상 주문을 제출합니다. 저장할 때는 서버가 가격과 재고를 다시 읽으므로, 앞 화면의 값과 달라질 수 있습니다.
마지막으로 /order-success로의 화면 이동과 DB의 변화를 함께 비교합니다. 위 조건이 유지되었다면 Order에 A의 수량 2, 구매 가격 35,000원, 합계 70,000원, completed가 저장되고, Book의 재고는 98이 되며, A의 CartItem은 지워지는 것이 기대 결과입니다. completed는 가상 주문의 상태일 뿐, 실제 결제나 배송이 끝났다는 뜻이 아닙니다.
커밋은 트랜잭션 안의 DB 변경을 확정하는 단계입니다. 성공 페이지는 인증이나 주문 ID, DB 조회가 없는 정적 안내이므로, URL을 직접 열어도 완료 문구가 보입니다. 그래서 그 화면만으로 주문이 커밋되었다고 확정하지 않습니다. 주문 및 검색의 주문 액션이 같은 session 안에서 재고, 주문, 장바구니를 바꾸는 범위와, 커밋 뒤의 화면 이동을 따로 확인합니다.
제공 코드와 직접 마칠 부분
지금까지 제공한 코드와 직접 준비하거나 이후로 남길 부분을 단계별로 정리하면 다음과 같습니다.
| 단계 | 현재 제공 코드 | 직접 준비하거나 이후로 남길 것 |
|---|---|---|
| 환경 | Next.js 16 설정, Mongo 연결, 세 모델, 두 권 시드와 표지 SVG 예시 | 잠금 파일·호환 Node·독립 실습 DB·GitHub OAuth 앱을 자신의 환경에 준비 |
| 로그인 | GitHub provider, JWT/session 연결, handlers·getSession, /login | Header의 로그인 상태별 메뉴·로그아웃. 영속 User·계정 통합은 운영 확장 |
| 도서 | 목록·상세·loading·404, 제목/저자 검색과 목록 페이지 교체 코드 | 빈 검색 결과의 맞춤 안내와 대량 검색 성능은 추가 과제 |
| 장바구니 | 소유자 조건, 1부터 99까지의 정수·재고 검사, 문자열 DTO와 변경 피드백 | UI 최대 10과 서버 최대 99의 차이, 서버 값과 입력 동기화, 누락 도서·경합 정책 |
| 주문 | 같은 session의 재고 차감·주문 생성·장바구니 삭제, 커밋 뒤 이동 | 실결제·배송, 주문 식별자·멱등성·자동 재시도·불명 결과의 확인/복구 |
| 전역 UI·배포 | layout·고정 Header·Footer, 환경 변수·배포 절차 | 홈 화면, 상태별 메뉴·로그아웃, 실제 배포 URL과 DB/OAuth 연결 확인 |
홈은 생성기의 app/page.tsx를 북스토어의 시작 화면으로 바꾸는 과제입니다. 헤더의 메뉴와 로그아웃은 로그인과 로그아웃의 서버 예제를 참고하되, 그 예제의 src/components 경로와 /account 이동은 여기의 components 경로와 실제 북스토어 주소에 맞춰야 합니다.
검색과 페이지 이동은 기본 흐름을 확인한 뒤에 볼 수 있습니다. 지금의 두 권짜리 시드와 페이지당 12권 조건에서는 두 번째 페이지가 생기지 않으며, 설계에서 정한 대량 데이터와 응답 시간 목표를 이 예제로 달성했다고 판단하지 않습니다.
예상과 다른 결과가 나올 때
다음 표는 직접 비교할 기대 결과와, 다를 때 먼저 확인할 곳입니다. 실제 환경에서 본 결과는 사용한 데이터와 계정과 함께 README에 남기고, 아직 확인하지 않은 항목은 미확인으로 기록합니다.
| 경우 | 볼 결과 | 다를 때 먼저 볼 곳 |
|---|---|---|
| 정상 가상 주문 | 위 조건의 Order·Book·CartItem 세 변화와 화면 이동 | Network·서버 응답, 같은 DB의 주문 소유자·금액·재고·장바구니. 성공 URL만 보지 않음 |
| 빈 장바구니 | /cart·/order의 빈 안내와 도서 링크. 직접 액션은 빈 상태를 업무 거절로 반환 | 세션 userId와 CartItem 조회 조건 |
| ID 형식 오류·없는 도서 | 형식 오류를 조회 전에 거절하거나 유효 ID 조회 뒤 404로 연결 | params·isValidObjectId·Book 조회 |
| 재고 부족 | 유효한 도서·수량을 전제로 업무 거절과 오류 안내. DB 값도 별도 비교 | 같은 session·업무 거절/abort 경로. 커밋 결과 불명 오류와 구분 |
| 계정 A/B | 별도 브라우저 프로필의 유효한 두 GitHub 계정에서 각자 장바구니만 조회 | 서버 세션 ID와 조회·수정·삭제의 userId 조건 |
| A 재로그인 | 직접 완성한 로그아웃 뒤 같은 공급자 계정으로 남아 있는 A의 데이터에 접근 | providerAccountId → JWT → session 연결. 토큰·비밀 원문을 기록하지 않음 |
| 빌드·배포 | 배포 환경에서 같은 흐름과 외부 연동을 따로 확인 | 린터·타입·빌드의 실패와 Mongo/OAuth callback·배포 로그의 실패를 구분 |
커밋이 확정된 뒤 화면 갱신만 실패했다고 주문을 다시 제출하지 않으며, 결과를 알 수 없는 오류도 주문이 없다는 뜻은 아닙니다. 지금의 수동 트랜잭션은 자동 재시도, 멱등성, 주문 내역의 조회와 복구 화면을 제공하지 않습니다. 같은 요청을 반복해도 주문이 중복으로 만들어지지 않는 성질을 멱등성이라고 하며, 운영으로 넓힐 때는 이를 위한 주문 식별자와 결과 확인 경로를 먼저 정해야 합니다.
배포 및 테스트
로컬에서 위 학습 흐름과 직접 마친 UI의 범위를 확인한 뒤, 앱을 공개하려면 Vercel 배포로 이어 갑니다. 로컬 화면과 빌드 성공, 그리고 배포 환경의 DB와 OAuth 연결은 서로 다른 확인입니다.
로컬 테스트
npm run dev개발 서버가 출력한 실제 접속 주소에서 /login과 /books를 열고 위 흐름을 확인합니다. 기본 주소는 http://localhost:3000이지만, 쓰고 있는 서버의 터미널 출력을 기준으로 합니다. 특히 장바구니의 추가, 수량 변경, 삭제와 주문을 확인하고, 다른 계정의 데이터가 섞이지 않는지와 다시 로그인한 뒤에도 소유자 ID가 유지되는지 확인합니다. 이 문서는 코드 예시를 제시할 뿐, 실제 MongoDB, OAuth, Vercel에서 검증한 결과를 제공하지 않습니다.
홈과 상태별 메뉴, 로그아웃을 아직 만들지 않았다면 직접 과제로 남겨 두고, 고정된 헤더 코드만으로 그 기능까지 끝났다고 기록하지 않습니다. 배포하기 전에는 프로젝트에 설정한 린터, 타입 검사, 프로덕션 빌드를 따로 확인합니다. Next.js 16에서는 next lint가 없어졌으므로, 빌드가 ESLint 검사까지 자동으로 대신한다고 가정하지 않습니다.
Vercel 배포
먼저 GitHub, GitLab, Bitbucket 가운데 하나에 프로젝트 저장소를 만들고 코드를 푸시합니다. Vercel에 로그인해 Add New... 메뉴의 Project로 그 저장소를 가져오면, Vercel이 Next.js 프로젝트임을 자동으로 감지해 설정을 제안합니다.
그다음 프로젝트 설정의 Environment Variables에서 MONGODB_URI, AUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET을 Production 환경에 추가합니다. 데이터베이스 값에는 그 배포 환경의 MongoDB Atlas 연결 문자열을, AUTH_SECRET에는 충분히 긴 무작위 문자열을 씁니다. Preview에서도 검증한다면 Preview 범위에 별도의 환경 값을 설정하고, 실습용 시드가 운영 DB를 지우지 않도록 대상을 나눕니다. GitHub OAuth 앱에는 https://배포도메인/api/auth/callback/github를 Authorization callback URL로 등록합니다. 설정을 확인한 뒤 Deploy 버튼을 누르면 Vercel이 자동으로 빌드하고 배포합니다.
이 단계별 가이드는 온라인 북스토어의 핵심 기능을 구현하는 데 필요한 주요 과정과 코드 예시를 제공합니다. 실제 프로젝트에서는 계정 모델과 역할, 사용자용 오류 UI, 관측과 복구 절차를 요구사항에 맞춰 넓혀야 합니다. 배포 URL에서도 로그인, 목록과 검색, 사용자별 장바구니와 가상 주문을 확인하고, 세션이나 DB 오류는 배포 환경의 로그와 응답을 맞추어 보며, 빌드 성공만으로 이런 외부 연동까지 통과했다고 판단하지 않습니다.
정리
- 루트 레이아웃에 헤더와 푸터를 두어 모든 페이지에 공통 내비게이션을 적용합니다.
- 첫 버전의 완료 범위는 로그인, 도서 목록과 상세, 자기 장바구니, 가상 주문이며, 홈과 상태별 메뉴, 로그아웃은 직접 마칠 과제입니다.
- 주문의 성공은 성공 페이지가 아니라 같은 DB의 Order, Book 재고, CartItem 변화로 확인합니다.
- 결과를 알 수 없는 오류에서는 곧바로 다시 주문하지 않으며, 운영에서는 주문 식별자와 멱등성, 결과 확인 경로를 먼저 정합니다.
- 배포는 환경 변수와 OAuth 콜백 URL을 배포 환경에 맞게 설정하고, 빌드 성공과 외부 연동의 동작을 따로 확인합니다.
연습 문제
1. 주문 뒤의 DB 값 예상하기
새 실습 DB에서 Next.js 완벽 가이드(가격 35,000원, 재고 100)를 장바구니에 담아 수량을 3으로 바꾼 뒤 주문했습니다. 다른 요청이 없다면 Order의 합계, Book의 재고, 계정의 CartItem은 어떻게 될까요?
해설 보기
Order에는 수량 3, 구매 가격 35,000원, 합계 35,000 × 3 = 105,000원, 상태 completed가 저장됩니다. Book의 재고는 같은 트랜잭션 안에서 100 - 3 = 97로 줄고, 그 계정의 CartItem은 지워집니다. 이 세 변화가 모두 같은 DB에서 확인되어야 주문이 커밋되었다고 판단할 수 있으며, 성공 페이지가 보인 것만으로는 확인되지 않습니다.
2. 배포한 뒤 로그인하자마자 오류가 날 때
로컬에서는 GitHub 로그인이 잘 되었는데, Vercel에 배포한 뒤에는 로그인 버튼을 누르면 GitHub에서 콜백 URL 오류가 납니다. 무엇을 먼저 확인해야 할까요?
해설 보기
GitHub OAuth 앱에 배포 도메인의 콜백 URL인 https://배포도메인/api/auth/callback/github가 등록되어 있는지 먼저 확인합니다. 개발용 localhost 콜백만 등록되어 있다면 배포 환경의 로그인은 거절됩니다. 그다음 Vercel의 Production 환경 변수에 AUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET이 설정되어 있는지, 환경 변수를 바꾼 뒤 다시 배포했는지도 확인합니다.
3. 헤더의 로그인 링크
지금의 헤더는 로그인한 사용자에게도 "로그인" 링크를 보여 줍니다. 로그인 상태에 따라 "로그인"과 "로그아웃"을 바꾸어 보여 주려면, 헤더를 어떻게 만들면 좋을까요?
해설 보기
헤더를 async 서버 컴포넌트로 바꾸어 getSession()으로 세션을 읽고, 세션의 사용자가 없으면 /login 링크를, 있으면 사용자 이름과 로그아웃 버튼을 보여 줍니다. 로그아웃 버튼은 10장 2절처럼 signOut을 호출하는 서버 액션을 form에 연결해 만들 수 있습니다. 이렇게 하면 헤더를 클라이언트 컴포넌트로 바꾸지 않고도 서버 렌더링 결과에 알맞은 메뉴를 넣을 수 있습니다.
Vercel 배포와 환경 변수 설정은 15장의 Vercel 배포 과정과 환경 변수 설정을 함께 참고하세요.
다음 글 코드 리뷰 및 최적화에서는 완성한 북스토어 코드를 검토하고, 성능과 안정성을 높일 부분을 찾아 고칩니다.