Skip to content

About

⭐My Portfolio_blog

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

lka09 Blog

사이트와 Google 로그인 동의 화면에 표시되는 이름은 lka09입니다.

개발하며 배운 것을 기록하는 개인 블로그입니다. Next.js 프론트엔드, NestJS API, Firestore DB로 구성했습니다. 로그인은 Firebase Authentication을 거치지 않고 Google Cloud OAuth 2.0 API를 직접 호출합니다.

실행

Node.js 24와 npm을 사용합니다. 프론트엔드와 백엔드는 각각 의존성을 설치합니다.

cd backend
npm ci
Copy-Item .env.example .env
npm run build
npm start

다른 터미널에서 프론트엔드를 실행합니다.

cd frontend
npm ci
npm run dev

사이트는 http://localhost:3000, API는 http://127.0.0.1:4000 입니다. 브라우저의 /api 요청은 Next.js가 NestJS로 전달합니다. 백엔드 개발 중에는 npm run dev로 소스 변경 시 자동 재시작할 수 있습니다. 운영 실행은 npm run build 후 npm start를 사용합니다.

환경 설정

backend/.env.example을 복사한 뒤 아래 값을 설정합니다. 비밀 값과 서비스 계정 JSON은 Git에 커밋하지 않습니다.

변수 용도
FIREBASE_PROJECT_ID 운영 프로젝트 rootnode-blog
GOOGLE_APPLICATION_CREDENTIALS Firestore에 접근하는 서비스 계정 JSON의 절대 경로
FIREBASE_SERVICE_ACCOUNT_JSON Vercel에서 사용하는 서비스 계정 JSON 전체, Sensitive 변수로 저장
GOOGLE_CLIENT_ID Google Cloud에서 만든 웹 애플리케이션 OAuth 클라이언트 ID
GOOGLE_CLIENT_SECRET 해당 클라이언트의 비밀번호, 서버에서만 사용
GOOGLE_REDIRECT_URI 로컬은 http://localhost:3000/api/auth/google/callback
PUBLIC_SITE_URL 로컬은 http://localhost:3000, 운영은 실제 HTTPS 사이트 주소
FRONTEND_ORIGINS 허용하는 프론트엔드 출처, 여러 개는 쉼표로 구분
HOST 기본 127.0.0.1, 컨테이너 배포는 0.0.0.0

Google Cloud의 OAuth 클라이언트에는 실제 GOOGLE_REDIRECT_URI를 승인된 리디렉션 URI로 등록해야 합니다. 운영 도메인이 정해지면 운영 URL도 등록합니다. OAuth 동의 화면이 테스트 모드라면 테스트 사용자에 dev@rootnode.kr을 추가해야 합니다. 배포 후 일반 사용자에게도 로그인을 제공하려면 공개 상태와 Google의 검증 요구사항을 확인합니다.

Firestore는 서울(asia-northeast3)에 생성했습니다. 서비스 계정에는 이 프로젝트의 roles/datastore.user 권한만 부여합니다. Google Cloud 런타임에 배포할 때는 JSON 키 대신 런타임에 연결된 서비스 계정의 ADC를 사용할 수 있습니다. 프론트엔드에는 Firebase API 키나 Google 클라이언트 비밀번호가 필요하지 않습니다.

현재 로컬 환경에는 RootNode Blog Web OAuth 클라이언트와 rootnode-blog-api@rootnode-blog.iam.gserviceaccount.com 서비스 계정을 연결했습니다. OAuth 앱은 테스트 상태이고 관리자 dev@rootnode.kr을 테스트 사용자로 등록했습니다. 로컬 콜백과 https://blog.rootnode.kr/api/auth/google/callback을 등록했습니다. 일반 독자에게 로그인을 열 때는 OAuth 공개 설정을 별도로 적용합니다. 서버 키는 로컬에서 생성한 뒤 공개 인증서만 Google Cloud에 등록했습니다. 개인키와 서비스 계정 JSON은 .local/, OAuth 비밀번호는 backend/.env에 있으며 모두 Git에서 제외됩니다. 등록한 서버 키는 2027년 11월 3일 만료되므로 그전에 교체합니다.

Vercel 배포

운영 주소는 https://blog.rootnode.kr이며 gbin8498@gmail.com 계정의 lka09-blog 프로젝트를 사용합니다. 루트 vercel.json은 Vercel Services(베타)로 Next.js와 NestJS를 함께 배포합니다. /api/*는 NestJS, 나머지는 Next.js가 처리하고 서버 렌더링은 서비스 바인딩으로 API를 호출합니다. NestJS는 tsc로 만든 dist 실행 파일을 사용하며 런타임 지역은 서울(icn1)입니다. Services는 출력 폴더를 함수 루트로 옮기므로 빌드 후 개발 의존성을 제거하고 scripts/prepare-vercel.cjs로 운영 의존성을 dist/node_modules에 함께 배치합니다. 로컬 빌드에서는 이 배포 전용 단계를 실행하지 않습니다.

운영 환경에는 FIREBASE_PROJECT_ID, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, FIREBASE_SERVICE_ACCOUNT_JSON, PUBLIC_SITE_URL, GOOGLE_REDIRECT_URI, FRONTEND_ORIGINS를 등록합니다. 마지막 세 주소는 운영 도메인 기준으로 맞춥니다. 서버 자격증명과 OAuth 비밀번호는 Sensitive 변수로 보관합니다. 로컬 파일 경로인 GOOGLE_APPLICATION_CREDENTIALS는 등록하지 않습니다.

가비아 DNS에는 blog CNAME을 Vercel에서 안내한 대상에 연결하고 _vercel TXT로 소유권을 확인합니다. 이번 배포는 현재 로컬 소스를 직접 올렸으며 Git 자동 배포는 연결하지 않았습니다.

글 관리

/login에서 Google 계정으로 로그인합니다. Google에서 검증된 이메일이 dev@rootnode.kr일 때만 /admin과 관리 API를 사용할 수 있습니다. 일반 회원도 로그인할 수 있지만 글을 변경할 수는 없습니다.

  • 마크다운 작성, 코드 구문 강조, 표와 본문 이미지, 미리보기
  • 제목, 고유 주소, 요약, 카테고리, 태그, 커버, 추천 글 설정
  • 초안 저장, 발행, 수정, 비공개 전환, 삭제 전 확인
  • 브라우저 입력 백업과 복구, 다른 창의 수정 내용 덮어쓰기 방지
  • 공개 글 검색, 카테고리·태그 필터, 정렬, 페이지 이동

이미지도 Firestore 문서에 보관하므로 Firebase는 DB 용도로만 사용합니다. 이미지 한 장은 700KB 이하이며 PNG/JPEG/GIF/WebP의 실제 헤더를 검사합니다. 초안의 이미지는 관리자만 조회할 수 있고, 공개 글에서 참조하는 이미지만 공개됩니다. 대량 이미지 서비스로 확장할 때는 전용 이미지 저장소를 별도로 도입하는 편이 적합합니다.

인증과 데이터 구조

OAuth 인증 코드를 NestJS가 Google 토큰 API에서 교환한 뒤 ID 토큰의 서명, 발급 대상, 이메일 인증, nonce를 검증합니다. state와 PKCE로 로그인 요청을 연결합니다. 브라우저에는 HttpOnly 세션 쿠키만 전달하고, DB에는 세션 토큰의 SHA-256 해시만 저장합니다. 세션 유효기간은 7일이며 로그아웃하면 서버에서도 폐기합니다. 변경 API는 허용된 Origin을 검사합니다.

컬렉션 저장 내용
posts 초안을 포함한 관리용 원본
publishedPosts 발행된 공개본
postSlugs 중복 주소 방지를 위한 예약
media 이미지 바이트와 MIME 타입
oauthStates 일회성 OAuth 요청과 PKCE 검증 정보
sessions 해시된 세션과 인증된 사용자, 만료 시각

글과 공개본, 주소 예약은 Firestore 트랜잭션으로 함께 갱신합니다. Firestore 클라이언트 보안 규칙은 전체 접근 거부이며 서버의 서비스 계정만 IAM으로 접근합니다. 목록 조회는 본문을 제외한 메타데이터를 읽어 검색·정렬합니다. 개인 블로그 규모를 전제로 하며, 글이 많아지면 별도 검색 색인과 커서 페이지네이션으로 확장할 수 있습니다. 만료된 OAuth 요청과 세션의 정기 삭제는 운영 환경에서 별도로 구성할 수 있습니다. 만료 여부 자체는 서버가 항상 검사합니다.

로컬 검증

실제 계정과 운영 DB를 변경하지 않고 Firestore 에뮬레이터로 테스트할 수 있습니다. Java 21 이상이 필요합니다.

# 저장소 루트, 첫 번째 터미널
npx firebase-tools emulators:start --only firestore --project demo-rootnode

# 두 번째 터미널
cd backend
npm test

에뮬레이터 포트는 8085, UI는 4001입니다. 로컬 API를 에뮬레이터에 연결하려면 백엔드 실행 전에 설정합니다.

$env:FIREBASE_PROJECT_ID='demo-rootnode'
$env:FIRESTORE_EMULATOR_HOST='127.0.0.1:8085'
npm start

테스트는 실제 HTTP와 Firestore 에뮬레이터를 통과해 권한, CSRF, 입력 검증, 발행 수명주기, 동시 주소 충돌, 검색, 세션 폐기를 검증합니다. 테스트용 세션은 테스트 코드에서만 DB에 생성하며 앱에는 우회 로그인 경로가 없습니다. Google 실제 로그인은 에뮬레이터가 대신하지 않으므로 OAuth 자격증명이 설정된 환경에서 별도로 확인해야 합니다.

설계 참고: Google OAuth 웹 서버 흐름, Firestore 서버 클라이언트 인증.

cd frontend
npm run lint
npm run build

About

⭐My Portfolio_blog

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages