개인 포트폴리오 사이트 구축
Next.js 기반 개인 포트폴리오 웹사이트. 웹 열람용 페이지, PDF 다운로드, 방문자 피드백 수신 기능을 함께 제공하며, S3 + CloudFront + Lambda 서버리스 구조로 AWS에 직접 배포·운영합니다.
담당 역할: Next.js 프론트엔드 개발, Tailwind CSS 디자인, Puppeteer 기반 PDF 생성, EventBridge + SES 기반 피드백 알림 시스템 구축, Terraform IaC 인프라 구축, GitHub Actions CI/CD 파이프라인 구성까지 전 과정 단독 담당

사용자는 외부 DNS(CNAME) → CloudFront(OAC) → S3 경로로 정적 페이지를 제공받습니다. PDF 다운로드 요청은 API Gateway → Lambda(Puppeteer) 흐름으로 처리됩니다. 피드백 제출은 API Gateway → Lambda(feedback-handler) → EventBridge(portfolio-events) → Lambda(email-sender) → SES 흐름으로 처리되며, 방문자의 피드백이 실시간으로 이메일로 전달됩니다. CloudFront 앞단에 WAF를 배치해 L7 보안을 확보했고, Response Headers Policy로 HSTS·X-Frame-Options 등 보안 헤더를 모든 응답에 자동 추가합니다. ACM으로 HTTPS 인증서를 관리하고, PDF Lambda는 ECR 컨테이너 이미지로, 피드백 Lambda는 zip 패키지로 실행됩니다. X-Ray로 API Gateway ~ Lambda 구간의 분산 추적을 구성했으며, CloudWatch 대시보드와 SNS 알람으로 Lambda 오류·Duration·Throttle, API Gateway 5xx를 실시간 감지합니다. GitHub Actions가 코드 변경 시 S3 업로드 → CloudFront 캐시 무효화, Lambda 이미지 빌드 → ECR 푸시 → 함수 업데이트를 자동으로 수행합니다.
설계 근거
PDF 생성처럼 간헐적으로 발생하는 무거운 작업을 상시 서버 없이 Lambda로 분리해 운영 비용을 최소화했습니다. 정적 콘텐츠는 CloudFront + S3로 글로벌 캐싱하고, 서버가 필요한 기능만 서버리스로 붙이는 구조로 단순성과 비용 효율을 동시에 확보했습니다. 모든 인프라는 Terraform으로 코드화해 재현성과 변경 이력 관리를 확보했습니다.
App Router 기반 정적 생성(SSG)으로 S3 배포에 최적화되고, TypeScript로 데이터 구조를 타입 안전하게 관리할 수 있었습니다. Tailwind CSS v4와의 궁합도 선택 이유 중 하나였습니다.
CSS-first 설정 방식으로 설정 파일 없이 globals.css에서 바로 커스텀 테마를 정의할 수 있었고, 다크 모드 지원이 클래스 기반으로 간단하게 구현됩니다.
S3를 퍼블릭으로 열지 않고 OAC(Origin Access Control)로 CloudFront에서만 접근 가능하도록 구성해 보안을 강화했습니다. 직접 도메인 DNS는 외부 공급자에서 관리하며 CNAME으로 CloudFront에 연결했습니다.
관리형 규칙셋으로 SQL 인젝션, XSS 등 일반적인 웹 공격을 차단하고, 개인 포트폴리오임에도 기본 보안 레이어를 갖추기 위해 적용했습니다.
Puppeteer는 Chromium 바이너리가 필요해 일반 Lambda 패키지 크기 제한을 초과합니다. 컨테이너 이미지 기반 Lambda로 배포해 이 제약을 해결했습니다. PDF 생성은 간헐적 요청이므로 상시 서버 대신 Lambda가 비용 효율적입니다.
CloudFront에서 Lambda를 직접 호출하는 대신 API Gateway를 두어 요청 라우팅, 인증 확장, X-Ray 추적 연동을 용이하게 했습니다.
Lambda 컨테이너 배포에 필요한 이미지를 AWS 내부 레지스트리에서 관리해 배포 지연을 최소화하고 이미지 버전 관리를 체계화했습니다.
단순 에러 로그로는 파악하기 어려운 Lambda Cold Start 지연, Puppeteer 렌더링 구간 병목을 시각화해 성능 문제의 원인을 특정하기 위해 도입했습니다.
CloudFront, S3, WAF, API Gateway, Lambda, ECR, ACM, IAM 등 모든 리소스를 코드로 정의해 콘솔 수동 작업 없이 재현 가능한 인프라를 유지했습니다. 모듈 단위로 분리해 환경별 확장을 고려했습니다.
코드 push 시 Next.js 빌드 → S3 업로드 → CloudFront 캐시 무효화, Lambda 이미지 빌드 → ECR 푸시 → Lambda 함수 업데이트를 자동화해 수동 배포를 완전히 제거했습니다.
Lambda 오류·Duration 임계값 초과·Throttle, API Gateway 5xx를 CloudWatch Alarm으로 감지하고 SNS 이메일 구독으로 즉시 알림을 받도록 구성했습니다. 대시보드로 호출 추이·지연 시간을 한눈에 확인할 수 있어 장애 대응 속도를 높였습니다.
CloudFront Function 없이 AWS 관리형 정책으로 HSTS(2년)·X-Frame-Options·X-Content-Type-Options·XSS-Protection·Referrer-Policy·Permissions-Policy를 모든 응답에 일괄 적용해 코드 유지보수 없이 보안 레이어를 완성했습니다.
피드백 수신 Lambda와 이메일 발송 Lambda를 직접 연결하지 않고 EventBridge 커스텀 버스를 중간에 두어 두 기능을 느슨하게 분리했습니다. 향후 Slack 알림, DB 저장 등 처리를 추가할 때 기존 코드 수정 없이 Rule만 추가하면 되는 확장 구조를 확보하기 위해 이 방식을 선택했습니다. 이메일 발송은 자체 도메인(`eomkyeongmun.me`)을 SES에서 DKIM 검증 후 발신자로 사용해 신뢰도를 확보했습니다.
- 이슈
- Lambda에서 Puppeteer로 PDF를 생성할 때 한글 텍스트가 □□□ 로 깨지는 문제가 발생했습니다.
- 분석
- Puppeteer가 사용하는 Chromium은 시스템 폰트에 의존합니다. Lambda 실행 환경(Amazon Linux 2)에는 한글 폰트가 기본 설치되어 있지 않아, 한글 문자를 렌더링할 폰트를 찾지 못하고 □로 출력했습니다.
- 해결
- Lambda 컨테이너 이미지 Dockerfile에 Noto Sans KR 폰트를 직접 포함시켜 빌드했습니다. 또한 /portfolio/print 페이지에서 Google Fonts로 폰트를 로드하고, Puppeteer가 폰트 로딩 완료를 기다린 후 PDF를 캡처하도록 waitForFunction을 추가했습니다.
- 결과
- 한글 폰트가 정상적으로 렌더링되어 PDF에서 모든 텍스트가 올바르게 출력됩니다. 폰트를 이미지에 번들링해 외부 네트워크 의존 없이 일관된 결과를 보장합니다.
- 이슈
- 배포 후 프로젝트 상세 페이지에서 뒤로가기 또는 홈 링크 클릭 시 홈 페이지 대신 프로젝트 페이지로 다시 리다이렉트되는 문제가 발생했습니다.
- 분석
- Next.js App Router의 클라이언트 네비게이션은 RSC(React Server Component) 페이로드 파일을 통해 서버 컴포넌트 렌더링 결과를 받아옵니다. 그런데 GitHub Actions에서 S3에 업로드 시 모든 정적 파일에 Cache-Control: public, max-age=31536000, immutable을 일괄 적용했고, RSC 페이로드 파일(_next/static/ 경로)도 여기에 포함되었습니다. 배포 후 CloudFront 캐시 무효화를 수행해도 브라우저에 이미 캐시된 이전 RSC 페이로드가 남아 있으면 클라이언트가 오래된 라우팅 정보를 참조해 잘못된 페이지로 이동했습니다.
- 해결
- S3 업로드 단계에서 RSC 페이로드가 포함된 경로와 일반 정적 에셋 경로를 분리해 Cache-Control을 다르게 적용했습니다. HTML과 RSC 페이로드(_next/static/chunks/ 중 런타임 관련 파일)에는 no-cache를 적용해 항상 최신 값을 참조하도록 하고, 콘텐츠 해시가 포함된 JS·CSS 파일에만 immutable을 유지해 캐시 효율을 손상시키지 않았습니다.
- 결과
- 배포 후 페이지 이동 시 발생하던 홈 리다이렉트 문제가 해결되었습니다. RSC 페이로드는 항상 최신 서버 컴포넌트 결과를 참조하고, 정적 에셋은 기존과 동일하게 장기 캐시를 유지합니다.
- 이슈
- GitHub Actions에서 Lambda 컨테이너 이미지를 ECR에 푸시한 뒤 Lambda 함수가 새 이미지를 반영하지 않는 경우가 있었습니다.
- 분석
- ECR에 latest 태그로 이미지를 push해도 Lambda는 함수 설정이 변경되지 않으면 기존에 캐시된 이미지를 계속 사용합니다. Lambda가 새 이미지를 인식하려면 함수 자체를 업데이트하는 API 호출이 필요합니다.
- 해결
- GitHub Actions 워크플로에 ECR 푸시 후 aws lambda update-function-code 단계를 추가해 매 배포마다 Lambda가 최신 ECR 이미지를 참조하도록 강제했습니다.
- 결과
- 코드 push 시 Lambda가 항상 최신 이미지로 업데이트됩니다. 배포 누락 없이 변경 사항이 즉시 반영됩니다.
⚙ 개선점
프론트엔드 개발부터 서버리스 백엔드, IaC, CI/CD까지 하나의 서비스를 혼자서 끝까지 구축하며 전체 흐름을 직접 연결했습니다. S3 OAC, WAF, X-Ray, CloudWatch 알람, 보안 헤더 정책까지 실제 운영 수준의 보안·관측 레이어를 단계적으로 추가하며 단순 배포를 넘어선 구조를 완성했습니다.
△ 아쉬운 점
Lambda Cold Start 지연이 PDF 생성 첫 요청에서 체감될 수 있는데, Provisioned Concurrency 적용 여부를 충분히 검토하지 못했습니다. Terraform 모듈 구조도 초기 설계보다 복잡해져 리팩터링이 필요한 상태입니다.
→ 향후 방향
Lambda Provisioned Concurrency 또는 SnapStart 적용으로 Cold Start를 줄이고, CloudWatch 알람과 연동한 이상 트래픽 탐지 체계를 추가할 계획입니다. 피드백 시스템에 EventBridge Rule을 추가해 Slack 알림이나 DB 저장 등으로 확장하고, Terraform 모듈도 환경별 재사용 가능한 구조로 정리할 예정입니다.