2026년 3월 31일

|블로그 개발

개인 블로그 개발기

coverImage

Next.js + Fastify로 블로그를 직접 만들었습니다

Next.js 14 App Router, Fastify, PostgreSQL, Oracle Cloud — 프론트엔드부터 서버, 인프라까지 직접 설계하고 배포한 개인 블로그 개발기입니다. 인증 아키텍처, 토큰 갱신 전략, 포트폴리오 애니메이션 등 구현 과정에서 고민했던 부분들을 정리했습니다.


어떻게 생겼냐면

블로그 메인

블로그 상세

포트폴리오 메인


기술 스택

프론트엔드

역할기술
FrameworkNext.js 14 (App Router)
LanguageTypeScript
StylingTailwind CSS + CSS Variables
Data FetchingTanStack Query
StateZustand
FormReact Hook Form
Markdownreact-markdown, remark-gfm
AIGoogle Gemini API

백엔드

역할기술
FrameworkFastify 5
LanguageTypeScript
ORMPrisma 7
DatabasePostgreSQL
AuthJWT + OAuth (GitHub)
StorageSupabase (이미지)
InfraOracle Cloud + Nginx + PM2

핵심 아키텍처 결정들

1. BFF 패턴으로 인증 구현

가장 고민이 많았던 부분입니다.

JWT를 localStorage에 저장하면 XSS 공격에 노출됩니다. 그래서 HttpOnly 쿠키를 선택했습니다. 브라우저 JS에서 접근할 수 없으니 탈취 위험이 없습니다.

Client → Next.js /api 프록시 → Backend API

클라이언트는 백엔드를 직접 호출하지 않습니다. Next.js의 API Route가 중간에서 쿠키를 읽어 Authorization 헤더로 변환해서 백엔드로 전달합니다.

Loading...

[다이어그램: BFF 인증 흐름]

2. SSR과 CSR 인스턴스 자동 분기

서버(SSR)와 클라이언트(CSR)는 API 호출 방식이 다릅니다.

  • SSR: 백엔드 직접 호출 + cookies() 헤더 전달
  • CSR: /api 프록시 경유 (브라우저가 자동으로 쿠키 전송)
Loading...

typeof window로 환경을 판단해서 자동으로 올바른 인스턴스를 선택합니다. 서비스 레이어를 사용하는 쪽에서는 신경 쓸 필요가 없습니다.

3. 로그인 깜빡임(Flicker) 없애기

초기에는 클라이언트에서 /me API를 호출해 로그인 상태를 확인했습니다. 그러면 이런 일이 생깁니다.

1. 페이지 로드 → SSR HTML (비로그인 UI 포함)
2. JS 실행 → /me API 호출
3. 응답 수신 → 로그인 UI로 교체

이 1→3 과정에서 "로그인" 버튼이 잠깐 보였다가 "로그아웃" 버튼으로 바뀝니다. 사용자 입장에서 거슬리는 동작입니다.

해결책: 서버 컴포넌트에서 유저 정보를 받아 props로 내려줍니다.

Loading...

SSR HTML부터 올바른 UI가 포함되니 깜빡임이 사라집니다.

4. 토큰 갱신은 미들웨어에서

Access Token(15분) 만료 후 Refresh Token으로 갱신하는 로직이 필요합니다.

처음엔 서버 컴포넌트에서 cookies().set()을 시도했는데, Next.js는 Server Action이나 Route Handler에서만 쿠키 쓰기를 허용합니다. 렌더링 중에는 불가능합니다.

문제: Server Component → cookies().set() → 무시됨 (에러 없음!)

해결책: 미들웨어는 모든 요청을 가로챌 수 있고, 응답 쿠키를 자유롭게 설정할 수 있습니다.

Loading...

미들웨어에서 갱신 후 새 토큰을 쿠키에 심어두면, 이후 서버 컴포넌트에서 cookies()로 읽을 수 있습니다.

경로위치트리거
SSRmiddleware.ts페이지 요청 시
CSRapi/[...path]/route.ts클라이언트 API 호출 시

포트폴리오 상세 보기 애니메이션

포폴 카드 애니메이션

포트폴리오 카드를 클릭하면 카드 위치에서 전체 화면으로 확장되는 애니메이션이 재생됩니다.

구현 방식은 이렇습니다.

  1. 클릭 시 카드의 getBoundingClientRect()로 현재 위치/크기 저장
  2. 오버레이를 최종 크기로 렌더링하되, CSS transform으로 카드 크기/위치로 압축
  3. requestAnimationFrame으로 transform 제거 → 자연스러운 확장
Loading...

createPortaldocument.body에 렌더링해서 z-index 충돌도 없습니다.


마크다운 에디터

글 작성1
글 작성2

글 작성 화면은 좌측에 마크다운 입력, 우측에 실시간 미리보기가 나란히 배치됩니다.

에디터 스크롤에 따라 미리보기가 자동으로 따라가는 동기화 기능도 구현했습니다.

Loading...

이미지 업로드는 드래그 앤 드롭붙여넣기 모두 지원합니다. 클립보드에서 이미지를 감지해 바로 업로드하고 마크다운에 삽입합니다.


AI 포트폴리오 요약

포폴 요약 AI

포트폴리오를 발행할 때 Google Gemini API로 내용을 3문장으로 자동 요약합니다.

Loading...

Server Action으로 구현해서 API 키가 클라이언트에 노출되지 않습니다. 토글 버튼으로 켜고 끌 수 있습니다.


댓글 시스템

댓글/답글

로그인한 사용자는 댓글을 달고, 댓글에 답글을 달 수 있습니다. 1단계까지만 중첩을 허용합니다.

댓글 삭제는 soft delete 방식입니다. deleted_at 컬럼에 삭제 시각을 기록하고, 조회 시 WHERE deleted_at IS NULL로 필터링합니다. 부모 댓글이 삭제되면 하위 답글도 함께 soft delete됩니다.

좋아요는 Optimistic Update로 구현해 즉각적인 피드백을 제공합니다. 요청이 실패하면 이전 상태로 rollback됩니다.


인프라

[다이어그램: 인프라 구성]

Oracle Cloud Free Tier에 Ubuntu 서버를 올렸습니다. 무료인데 꽤 안정적입니다.

  • Nginx: 리버스 프록시 + SSL (Let's Encrypt)
  • PM2: Node.js 프로세스 관리, 서버 재시작 시 자동 실행
  • Swap 2GB: Free Tier의 RAM(1GB) 부족 보완

이미지는 Supabase Storage에 저장합니다. 업로드 시 Sharp로 WebP 변환 + 리사이즈해서 용량을 줄입니다.


트러블슈팅 모음

미들웨어에서 토큰 갱신 후 백엔드의 set-cookie를 headers.append()로 추가하고, 이후 cookies.set()으로 다른 쿠키를 설정했더니 앞서 append한 쿠키가 사라졌습니다.

Next.js의 ResponseCookies.set()이 내부 맵을 헤더에 전체 동기화(교체) 하기 때문입니다.

해결: headers.append()를 쓰지 않고, 모든 쿠키를 response.cookies.set()으로 통일했습니다.

POST 요청 빈 body 파싱 오류

POST /auth/refresh처럼 body가 필요 없는 엔드포인트에서 Fastify가 파싱 오류를 냈습니다.

Content-Type은 application/json인데 body가 비어있으니 파서가 실패한 것입니다.

해결: body 없는 POST 요청에도 JSON.stringify({}) 전송.

Loading...

Oracle Cloud 외부 접속 불가

iptables에서 80/443 포트를 열었는데도 외부에서 접속이 안 됐습니다.

Oracle Cloud는 OS 방화벽과 VCN Security List 두 곳 모두 열어야 합니다. 한쪽만 열면 안 됩니다.

해결: Oracle Cloud 콘솔 → VCN → Security Lists에서 Ingress Rule 추가.


직접 만들길 잘했습니다. 플랫폼 블로그에서는 경험하기 어려운 것들을 많이 배웠습니다.

인증 흐름, 서버/클라이언트 렌더링 경계, 인프라 구성까지 전부 직접 다뤄볼 수 있었습니다.

코드는 GitHub에서 볼 수 있습니다.