NAVIGATION
  • 전체 글
CATEGORIES
  • Develop7
    • Backend3
    • Frontend2
    • Git1
    • Performance1
  • AI3
    • Claude Code3
  • Frontend1
    • Next.js1
POPULAR TAGS
  • API3
  • 클로드코드2
  • 성능최적화2
  • GraphQL2
  • REST2
  • React2
  • JavaScript2
  • 프론트엔드2
  • Claude Code1
  • AI1
···
>steady-one_

© 2025 steady-one Blog. All rights reserved.

Next.js 배포 AWS 비용 이슈 개선하기
Next.js
2025년 10월 14일

Next.js 배포 AWS 비용 이슈 개선하기

Next.js ISR의 잘못된 설정으로 Lambda 함수가 과도하게 실행되던 문제를 SSR과 CloudFront 캐싱으로 해결한 실전 경험을 공유합니다.

.nextjs.awslambda.isr.ssr.cloudfront.성능최적화.serverlessarchitecture

Next.js 배포 AWS 비용 이슈 개선하기

사이트를 SST로 배포하고 나중에 시간이 지나서 AWS를 확인해보니 Lambda가 하루 86,000번 이상 실행되면서 하루에 10달러 씩 과금 되고 있었습니다.

울음짤.jpeg 마치 수도꼭지를 잠그지 않고 외출한 것처럼, Lambda 함수가 쉬지 않고 실행되고 있었던 것입니다. 이 글에서는 Next.js의 ISR(Incremental Static Regeneration) 설정 실수로 인한 Lambda 과부하 문제를 진단하고, SSR과 CloudFront 캐싱으로 해결한 과정을 공유합니다. ISR과 SSR의 차이점을 명확히 이해하고, 프로젝트에 맞는 렌더링 전략을 선택하는 방법을 알아보겠습니다.


🚨 문제 발견: 무엇이 잘못되었나?

증상 분석

📊 비정상적인 Lambda 메트릭
├─ 호출 횟수: 86,522회/일 (초당 약 1회)
├─ 타임아웃: 거의 모든 요청 실패
├─ 실행 시간: 대부분 10초 이상
└─ 사이트 상태: 프로덕션에 최신 데이터 반영 안됨

이상한 점들:

  • 실제 방문자는 0명 인데 Lambda는 86,000번 실행
  • 모든 요청이 10초에 타임아웃
  • 로컬에서는 최신 데이터, 프로덕션에서는 오래된 데이터

프로젝트 구조

전체 아키텍처:
Obsidian 노트 → S3 버킷 (마크다운 파일)
              ↓
        Next.js 15 App Router
              ↓
        AWS Lambda (SST 배포)
              ↓
        CloudFront CDN
              ↓
           사용자

🔍 원인 분석: 3가지 설정 실수

1번 실수: 너무 짧은 ISR Revalidate 시간

문제의 코드:

// 모든 페이지 파일에 설정됨
export const revalidate = 60; // 1분

📚 참고: Next.js Revalidating Data 문서

ISR이 뭔가요?

**ISR(Incremental Static Regeneration)**은 Next.js의 기능으로, 정해진 시간마다 자동으로 페이지를 다시 생성합니다.

비유로 이해하기:

ISR을 알람 시계라고 생각해보세요. revalidate = 60으로 설정하면:

1분마다 알람이 울림
├─ 방문자가 있든 없든
├─ 새벽 3시든 한낮이든
└─ 정확히 1분마다 페이지 재생성

마치 빵집이 손님이 없어도 1분마다 새 빵을 굽는 것과 같습니다.

왜 이렇게 많이 실행됐나?

기본 계산:
7개 페이지 × 24시간 × 60분 = 10,080회/일

실제로는 더 많은 이유:
├─ CloudFront의 각 엣지 로케이션이 독립적으로 요청
├─ 타임아웃 시 AWS가 자동 재시도 (최대 3번)
└─ 결과: 86,522회/일 실제 실행

2번 실수: Lambda 타임아웃이 너무 짧음

설정:

// sst.config.ts
timeout: "10 seconds"

📚 참고: AWS Lambda Timeout 설정

실제 실행 흐름:

Lambda 함수 시작
  ↓
S3에서 12개 마크다운 파일 읽기 (6초)
  ↓
마크다운을 HTML로 파싱 (4초)
  ↓
총 10초 이상 소요
  ↓
10초 타임아웃! ❌

비유로 이해하기:

마치 10분 안에 마라톤을 완주하라고 하는 것과 같습니다. 실제로는 12분이 필요한데, 10분만 주어진 상황입니다.

타임아웃의 악순환:

1차 시도: 타임아웃 (10초)
  ↓
AWS 자동 재시도 #1 (10초)
  ↓
다시 타임아웃
  ↓
AWS 자동 재시도 #2 (10초)
  ↓
다시 타임아웃
  ↓
AWS 자동 재시도 #3 (10초)
  ↓
결과: 1번의 요청이 4번 실행됨

3번 실수: 재생성 실패로 오래된 데이터 서빙

타임아웃으로 ISR 재생성이 계속 실패하면, Next.js는 마지막으로 성공한 정적 HTML을 계속 제공합니다.

프로덕션 사이트: 10월 4일 데이터
              (ISR 실패로 업데이트 안됨)

로컬 환경: 10월 14일 데이터
         (S3에서 직접 읽어서 항상 최신)

S3 버킷: 10월 14일 데이터
       (Obsidian에서 동기화됨)

마치 고장난 자판기가 같은 음료만 계속 내놓는 것과 같습니다.


💡 해결 방법: 3단계 최적화

Step 1: ISR → SSR로 전환

ISR과 SSR의 차이점 이해하기

먼저 두 방식의 차이를 명확히 이해해야 합니다.

ISR (Incremental Static Regeneration):

export const revalidate = 60; // 초 단위
  • 언제 실행? 정해진 시간마다 자동으로
  • 방문자 없어도? 네, 무조건 실행
  • 속도: 매우 빠름 (미리 만들어둔 HTML 제공)
  • 비유: 신문사가 정기적으로 신문을 인쇄하는 것

📚 참고: Next.js ISR 공식 문서

SSR (Server-Side Rendering):

export const dynamic = 'force-dynamic';
  • 언제 실행? 사용자가 방문할 때만
  • 방문자 없으면? 실행 안 함
  • 속도: 보통 (요청시마다 생성)
  • 비유: 레스토랑이 주문받을 때 요리하는 것

📚 참고: Next.js Dynamic Rendering 공식 문서

코드 변경하기

변경 전:

// app/page.tsx
export const revalidate = 60; // 시간 기반
 
export default async function HomePage() {
  const posts = await getPosts();
  return <PostList posts={posts} />;
}

변경 후:

// app/page.tsx
export const dynamic = 'force-dynamic'; // 요청 기반
 
export default async function HomePage() {
  const posts = await getPosts();
  return <PostList posts={posts} />;
}

변경 대상 파일들

// 1. 홈페이지
// app/page.tsx
export const dynamic = 'force-dynamic';
 
// 2. 포스트 목록
// app/posts/page.tsx
export const dynamic = 'force-dynamic';
 
// 3. 포스트 상세
// app/posts/[...slug]/page.tsx
export const dynamic = 'force-dynamic';
 
// 4. 카테고리 페이지
// app/category/[...slug]/page.tsx
export const dynamic = 'force-dynamic';
 
// 5. 태그 페이지
// app/tags/[slug]/page.tsx
export const dynamic = 'force-dynamic';
 
// 6. 사이트맵
// app/sitemap.ts
export const dynamic = 'force-dynamic';

효과

변경 전: 86,522회/일 (시간 기반 자동 실행)
변경 후: 약 3,500회/일 (실제 방문자 기반)
개선율: 96% 감소

Step 2: Lambda 타임아웃 늘리기

적절한 타임아웃 계산하기

권장 공식:

타임아웃 = 평균 실행 시간 × 2.5 ~ 3배

예시:
평균 실행 시간: 12초
권장 타임아웃: 30초 (약 2.5배)

왜 2.5~3배?

  • S3 응답 시간이 때때로 느려질 수 있음
  • 네트워크 지연 고려
  • 재시도 방지 (타임아웃 없으면 재시도도 없음)

SST 설정 파일 수정

// sst.config.ts
import { SSTConfig } from "sst";
import { NextjsSite } from "sst/constructs";
 
export default {
  config(_input) {
    return {
      name: "my-nextjs-blog",
      region: "us-east-1",
    };
  },
  stacks(app) {
    app.stack(function Site({ stack }) {
      const site = new NextjsSite(stack, "Site", {
        path: ".",
        timeout: "30 seconds", // ✅ 10초에서 30초로 증가
        environment: {
          // 환경 변수...
        },
      });
 
      stack.addOutputs({
        SiteUrl: site.url,
      });
    });
  },
} satisfies SSTConfig;

📚 참고: SST NextjsSite Construct

효과

변경 전:
├─ 타임아웃: 매번 발생
├─ 재시도: 3번씩
└─ 총 실행: 요청 1회당 4번

변경 후:
├─ 타임아웃: 0건
├─ 재시도: 0번
└─ 총 실행: 요청 1회당 1번

Step 3: CloudFront 캐싱 추가

SSR로 바꾸면 방문할 때마다 Lambda가 실행됩니다. 하지만 CloudFront 캐싱을 추가하면 같은 페이지를 여러 번 방문해도 Lambda가 한 번만 실행됩니다.

CloudFront 캐싱이란?

비유로 이해하기:

CloudFront는 편의점 배송 시스템과 같습니다:

캐싱 없음 (SSR만):
매번 공장(Lambda)에서 제품 생산 → 배송
= 느리고 비효율적

캐싱 있음 (SSR + CloudFront):
첫 주문 → 공장에서 생산 → 편의점(CDN)에 재고 비치
다음 주문 → 편의점에서 즉시 제공 (5분간)
재고 소진 → 공장에 다시 주문
= 빠르고 효율적

📚 참고: CloudFront 캐싱 동작 방식

Next.js 설정 추가

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 기존 설정들...
  
  async headers() {
    return [
      {
        // 모든 페이지에 캐싱 헤더 추가
        source: '/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=300, stale-while-revalidate=60',
          },
        ],
      },
    ];
  },
};
 
module.exports = nextConfig;

📚 참고: Next.js Headers 설정

캐싱 헤더 설명

'public, s-maxage=300, stale-while-revalidate=60'
 
public
├─ 누구나 캐시 가능 (CDN, 브라우저)
└─ 반대: private (사용자 브라우저만)
 
s-maxage=300
├─ CloudFront가 5분간 캐시
├─ 5분 동안 Lambda 실행 안 함
└─ s-는 "shared"의 약자 (CDN용)
 
stale-while-revalidate=60
├─ 캐시 만료 후 60초간 "오래된 캐시" 제공
├─ 동시에 백그라운드에서 새 버전 생성
└─ 사용자는 빠른 응답, 다음 사용자는 새 버전

실제 동작 흐름

👤 방문자 A (첫 방문, 오전 10:00)
└─ CloudFront: 캐시 없음
   └─ Lambda 실행 (2초)
      └─ 결과를 CloudFront에 저장 (5분간 유효)

👤 방문자 B (오전 10:02)
└─ CloudFront: 캐시 히트! ⚡
   └─ 캐시된 HTML 즉시 반환 (0.1초)
      └─ Lambda 실행 안 함

👤 방문자 C, D, E... (오전 10:00~10:05)
└─ 모두 캐시 히트
   └─ Lambda 실행 안 함

👤 방문자 Z (오전 10:06, 캐시 만료)
└─ CloudFront: 캐시 만료
   └─ Lambda 재실행 (2초)
      └─ 새 캐시 저장 (다시 5분간)

캐싱의 효과

예시: 5분 동안 100명 방문

캐싱 없음:
100명 × 1번 = 100번 Lambda 호출

5분 캐싱:
첫 방문자만 Lambda 호출 = 1번
나머지 99명은 캐시 제공
결과: 99% Lambda 호출 감소

📊 최종 결과

개선 지표

항목 Before After 변화
Lambda 호출 86,522회/일 3,500회/일 -96% ⬇️
Lambda 타임아웃 매번 발생 0건 -100% ⬇️
페이지 로딩 10초+ (타임아웃) 2-5초 ⚡ 빠름
데이터 최신성 10일 전 최대 5분 지연 ✅ 해결
CDN 히트율 낮음 95%+ 📈 향상

안정성 개선

변경 전:
├─ 사이트 상태: 불안정
├─ 에러율: 높음 (대부분 타임아웃)
└─ 데이터: 오래됨

변경 후:
├─ 사이트 상태: 안정적
├─ 에러율: 0%
└─ 데이터: 최신 (최대 5분 지연)

🎓 언제 ISR을 쓰고, 언제 SSR을 쓸까?

ISR을 선택해야 하는 경우

✅ 트래픽이 많은 사이트

예시: 일 방문자 10만 명

SSR: 100,000번 Lambda 호출
ISR (revalidate=60): 약 10,000번 Lambda 호출
→ ISR이 10배 효율적!

적합한 예시:

  • 뉴스 사이트
  • 대형 쇼핑몰
  • 인기 있는 블로그

📚 참고: Next.js ISR 사용 사례

✅ 콘텐츠가 자주 변경

예시: 실시간 스포츠 점수

revalidate=30 (30초마다 업데이트)
→ 항상 최신 정보 제공

적합한 예시:

  • 실시간 스코어보드
  • 주식 시세
  • 실시간 채팅 (메시지 목록)

✅ 빌드가 빠른 경우

ISR의 위험: 빌드가 느리면 타임아웃

안전한 경우:
├─ 빌드 시간 < 5초
└─ 타임아웃 설정 > 빌드 시간 × 3

SSR을 선택해야 하는 경우

✅ 트래픽이 적은 사이트

예시: 일 방문자 1,000명 (저의 경우)

SSR (5분 캐싱): 약 288번 Lambda 호출
ISR (revalidate=60): 약 10,000번 Lambda 호출
→ SSR이 35배 효율적!

적합한 예시:

  • 개인 블로그
  • 회사 소개 사이트
  • 포트폴리오

✅ 콘텐츠 변경이 드뭄

예시: 기술 문서

업데이트 주기: 며칠 ~ 몇 주
→ ISR로 자주 재생성할 필요 없음

적합한 예시:

  • 기술 문서
  • API 문서
  • 튜토리얼 사이트

✅ 사용자별 개인화 필요

예시: 사용자 대시보드

각 사용자마다 다른 내용
→ ISR로는 불가능, SSR 필수

적합한 예시:

  • 관리자 대시보드
  • 마이페이지
  • 개인화된 추천

의사결정 플로우차트

내 사이트의 특성을 파악해보세요
│
├─ 일일 방문자가 10만 이상인가?
│  │
│  ├─ Yes → ISR 사용 (revalidate=300~3600)
│  │        + CloudFront 캐싱
│  │
│  └─ No → 콘텐츠가 시간/분 단위로 변경되나?
│     │
│     ├─ Yes → ISR 사용 (revalidate=60~300)
│     │        + CloudFront 캐싱
│     │
│     └─ No → SSR 사용 (force-dynamic) ✅
│              + CloudFront 캐싱 (5~10분)
│              
│              💡 개인 블로그는 대부분 이것!

캐싱 시간 선택 가이드

콘텐츠 유형 ISR revalidate CloudFront 캐싱
실시간 (주식, 점수) 30~60초 1분
자주 업데이트 (뉴스) 5~10분 5분
정기 업데이트 (블로그) 1시간 or SSR 5~10분
드물게 업데이트 (문서) SSR 10~30분

🛠️ 실전 적용 가이드

1단계: 현재 상태 진단

Lambda 호출 횟수 확인

AWS Console에서 확인:

  1. AWS Console → CloudWatch 접속
  2. 왼쪽 메뉴에서 "지표(Metrics)" 선택
  3. "Lambda" → "함수별 지표" 선택
  4. 함수 이름 검색 후 "Invocations" 선택
  5. 기간을 "1일"로 설정하여 확인

판단 기준:

✅ 정상: 일일 호출 ≤ 방문자 수 × 페이지뷰
⚠️ 주의: 일일 호출 > 방문자 수 × 10
🚨 문제: 일일 호출 > 10,000 (소규모 사이트)

Lambda 타임아웃 확인

AWS Console에서 확인:

  1. AWS Console → CloudWatch Logs 접속
  2. 로그 그룹에서 Lambda 함수 선택
  3. "로그 인사이트(Logs Insights)" 선택
  4. 다음 쿼리 실행:
fields @timestamp, @message
| filter @message like /timeout|timed out/i
| sort @timestamp desc
| limit 100

결과 해석:

결과 없음 → ✅ 정상
"Task timed out" 많음 → 🚨 타임아웃 문제

평균 실행 시간 확인

CloudWatch Logs Insights에서 다음 쿼리 실행:

fields @timestamp, @duration
| stats avg(@duration) as avg_duration, 
        max(@duration) as max_duration,
        min(@duration) as min_duration
| limit 1

타임아웃 설정 공식:

권장 타임아웃 = max_duration × 1.5 ~ 2

예시:
최대 실행 시간: 15초
권장 타임아웃: 22~30초

2단계: 코드 변경

Next.js 페이지 수정

모든 동적 페이지를 SSR로 변경:

// app/page.tsx (홈페이지)
export const dynamic = 'force-dynamic';
 
export default async function HomePage() {
  // 기존 코드...
}
// app/posts/[...slug]/page.tsx (포스트 상세)
export const dynamic = 'force-dynamic';
 
export default async function PostPage({ 
  params 
}: { 
  params: { slug: string[] } 
}) {
  // 기존 코드...
}

거의 변경 안 되는 페이지는 긴 revalidate 유지:

// app/about/page.tsx (회사 소개)
export const revalidate = 86400; // 24시간 (하루 1번만)
 
export default function AboutPage() {
  // 기존 코드...
}

SST 설정 수정

// sst.config.ts
import { SSTConfig } from "sst";
import { NextjsSite } from "sst/constructs";
 
export default {
  config(_input) {
    return {
      name: "my-blog",
      region: "us-east-1",
    };
  },
  stacks(app) {
    app.stack(function Site({ stack }) {
      const site = new NextjsSite(stack, "Site", {
        path: ".",
        timeout: "30 seconds", // ✅ 타임아웃 증가
        environment: {
          // 환경 변수...
        },
      });
 
      stack.addOutputs({
        SiteUrl: site.url,
      });
    });
  },
} satisfies SSTConfig;

CloudFront 캐싱 헤더 추가

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        // 모든 페이지에 5분 캐싱
        source: '/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=300, stale-while-revalidate=60',
          },
        ],
      },
    ];
  },
};
 
module.exports = nextConfig;

페이지별 다른 캐싱 시간 (선택사항):

async headers() {
  return [
    {
      // 홈: 짧은 캐싱 (자주 변경)
      source: '/',
      headers: [{
        key: 'Cache-Control',
        value: 'public, s-maxage=60',
      }],
    },
    {
      // 포스트: 긴 캐싱 (드물게 변경)
      source: '/posts/:slug*',
      headers: [{
        key: 'Cache-Control',
        value: 'public, s-maxage=600',
      }],
    },
  ];
}

3단계: 배포 및 검증

배포하기

# 프로젝트 루트 디렉토리에서
npx sst deploy --stage production

CloudFront 캐시 무효화 (필수!)

배포 후 반드시 캐시를 비워야 새 설정이 적용됩니다.

AWS Console에서:

  1. CloudFront → Distributions 선택
  2. 해당 Distribution 선택
  3. "Invalidations" 탭 클릭
  4. "Create invalidation" 버튼
  5. Object paths에 /* 입력 (모든 캐시 무효화)
  6. "Create" 버튼 클릭

왜 필요한가요? 이전 설정으로 만들어진 캐시가 CloudFront에 남아있어, 새 설정이 바로 반영되지 않기 때문입니다.

실시간 모니터링

CloudWatch Logs 실시간 확인:

AWS Console에서:

  1. CloudWatch Logs → Lambda 로그 그룹 선택
  2. 최신 로그 스트림 선택
  3. 자동 새로고침 활성화

정상 로그 예시:

START RequestId: abc123...
[INFO] Request: GET /posts/my-article
[INFO] S3 fetch completed in 1200ms
[INFO] Markdown parsing completed in 800ms
END RequestId: abc123... Duration: 2134.56 ms
REPORT RequestId: abc123... Status: 200

# 5분 이내 다음 방문
(로그 없음 = CloudFront 캐시 히트 = 정상 ✅)

# 5분 후 첫 방문
START RequestId: def456...
...

24시간 후 확인

Lambda 호출 횟수 확인:

CloudWatch Metrics에서:

  1. Lambda → 함수별 지표
  2. "Invocations" 선택
  3. 기간: 1일

예상 결과:

변경 전: 86,000회
변경 후: 3,000~5,000회 ✅

⚠️ 주의사항

1. SSR의 트레이드오프

장점

  • 트래픽 적을 때 효율적
  • 항상 최신 데이터 (캐시 시간 제외)
  • Lambda 호출 최소화

단점

  • 첫 방문자 로딩이 느림 (1-3초)
  • 서버 부하가 방문자 수에 비례
  • ISR보다 캐시 히트율이 중요

해결책: CloudFront 캐싱으로 대부분 완화됨

첫 방문자: 1-3초 (Lambda 실행)
이후 방문자 (5분 내): 0.1초 (캐시 히트)

2. 캐시 만료 지연

문제 상황

글을 업데이트했는데...
├─ CloudFront 캐시: 최대 5분 지연
└─ 즉시 반영 안 됨

해결 방법

1. 급한 경우: 수동 캐시 무효화

AWS Console → CloudFront → Invalidations
→ 특정 경로만 무효화: /posts/urgent-article

2. 자주 업데이트하는 페이지: 짧은 캐싱

// 뉴스 페이지는 1분 캐싱
source: '/news/:slug*',
headers: [{
  key: 'Cache-Control',
  value: 'public, s-maxage=60', // 1분
}]

3. 캐시 버스팅 (고급)

// URL에 버전 추가로 캐시 우회
const url = `/posts/article?v=${lastUpdateTime}`;

3. 트래픽 증가 시 전략 변경

현재는 SSR이 효율적이지만, 트래픽이 크게 늘면 ISR이 더 나을 수 있습니다.

재평가 시점:

일 방문자가 다음을 넘으면:
├─ 10,000명: 캐싱 시간 늘리기 (10분)
├─ 50,000명: ISR 고려 시작
└─ 100,000명: ISR로 전환 권장

📚 참고: Next.js 렌더링 전략 선택 가이드

4. 타임아웃 설정 밸런스

너무 짧으면:

타임아웃 발생 → 재시도 → 비효율

너무 길면:

실제 2초만 걸려도 30초 동안 Lambda 점유
→ 동시성 제한에 도달 가능

권장:

평균 실행 시간을 주기적으로 모니터링
└─ CloudWatch에서 확인
   └─ 평균 × 2.5~3배로 조정

📋 체크리스트

배포 전

□ ISR → SSR 전환 완료
  ├─ revalidate 제거
  └─ dynamic = 'force-dynamic' 추가

□ Lambda 타임아웃 증가
  ├─ 현재 평균 실행 시간 확인
  └─ 2.5~3배로 설정

□ CloudFront 캐싱 헤더 추가
  ├─ s-maxage 설정 (300 권장)
  └─ stale-while-revalidate 설정

□ 로컬에서 빌드 테스트
  └─ npm run build 성공 확인

배포 후

□ CloudFront 캐시 무효화 실행
  └─ AWS Console에서 수동 실행

□ 사이트 동작 확인 (30분)
  ├─ 모든 페이지 정상 로딩
  ├─ 최신 콘텐츠 반영
  └─ 콘솔 에러 없음

□ Lambda 로그 확인 (1시간)
  ├─ 타임아웃 0건
  ├─ 에러 0건
  └─ 실행 시간 < 타임아웃

□ 24시간 후 재확인
  ├─ Lambda 호출 횟수 감소
  └─ CloudWatch 알람 없음

장기 모니터링

□ 주간 점검
  ├─ Lambda 호출 횟수 추이
  └─ 평균 실행 시간 변화

□ 월간 점검
  ├─ 트래픽 증가 여부
  └─ 렌더링 전략 재평가 필요성

□ CloudWatch 알람 설정
  ├─ Lambda 호출 > 10,000/일
  ├─ Lambda 타임아웃 > 10건/시간
  └─ Lambda 에러율 > 1%

🔮 추가 최적화 아이디어

1. 하이브리드 전략

모든 페이지를 같은 방식으로 렌더링할 필요는 없습니다. 페이지 특성에 맞게 혼합하세요.

// 홈: 자주 업데이트 → ISR 짧은 주기
// app/page.tsx
export const revalidate = 300; // 5분
 
// 포스트 목록: 보통 → SSR
// app/posts/page.tsx
export const dynamic = 'force-dynamic';
 
// 포스트 상세: 드뭄 → ISR 긴 주기
// app/posts/[slug]/page.tsx
export const revalidate = 86400; // 24시간
 
// About: 거의 안 바뀜 → 정적 생성
// app/about/page.tsx
// (아무 설정 없으면 빌드 시 정적 생성)

📚 참고: Next.js Route Segment Config

2. Edge Runtime 사용 (고급)

Lambda 대신 CloudFront Edge에서 실행하여 지연시간을 더 줄일 수 있습니다.

// app/posts/[slug]/page.tsx
export const runtime = 'edge'; // 🚀 Edge Runtime
 
export const dynamic = 'force-dynamic';

장점:

  • 사용자와 가까운 위치에서 실행 (지연 감소)
  • 콜드 스타트 거의 없음
  • Lambda보다 비용 절감 가능

단점:

  • Node.js API 일부 제한 (fs, child_process 등)
  • S3 SDK 사용 가능하지만 제약 있음

📚 참고: Next.js Edge Runtime

3. 인기 페이지만 미리 생성

// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  // 조회수 상위 10개 포스트만 빌드 시 생성
  const popularPosts = await getPopularPosts(10);
  
  return popularPosts.map((post) => ({
    slug: post.slug,
  }));
}
 
// 나머지 포스트는 첫 방문 시 생성
export const dynamicParams = true;
export const revalidate = 86400; // 24시간

효과:

  • 인기 페이지: 빠른 로딩 (미리 생성)
  • 비인기 페이지: 필요할 때만 생성
  • 빌드 시간 단축

📚 참고: Next.js generateStaticParams


📚 더 알아보기

Next.js 공식 문서

  • 렌더링 개요
  • 데이터 페칭과 캐싱
  • ISR vs SSR 비교
  • Route Segment Config 전체 옵션

AWS 문서

  • Lambda 타임아웃 Best Practices
  • CloudFront 캐싱 전략
  • Lambda와 CloudFront 통합

SST 문서

  • NextjsSite Construct
  • SST 배포 가이드

성능 최적화 리소스

  • Web.dev - 웹 성능
  • MDN - HTTP 캐싱

💭 마치며

이번 경험을 통해 중요한 교훈을 얻었습니다:

1. "최신 기능"이 항상 정답은 아니다

ISR은 Next.js의 강력한 기능이지만, 모든 상황에 맞는 것은 아닙니다. 내 프로젝트의 특성(트래픽, 업데이트 빈도)을 먼저 파악하고, 그에 맞는 렌더링 전략을 선택하는 것이 중요합니다.

2. 설정값은 맥락을 고려해야 한다

revalidate = 60은 누군가에게는 완벽할 수 있지만, 트래픽이 적은 내 블로그에는 과했습니다. 공식 문서의 예제를 복사하기 전에, 내 상황에 맞는지 항상 고민해야 합니다.

3. 타임아웃은 여유있게 설정하라

네트워크 지연, S3 응답 시간 변동 등 예측하기 어려운 요소들을 고려하여, 평균 실행 시간의 2-3배로 설정하는 것이 안전합니다.

4. CloudFront 캐싱은 필수

SSR이든 ISR이든, CloudFront 캐싱 없이는 비효율적입니다. 캐싱은 성능 최적화의 핵심입니다.

5. 모니터링이 없으면 문제를 모른다

CloudWatch 알람 설정 덕분에 문제를 빠르게 발견할 수 있었습니다. 배포 후 모니터링은 필수입니다.


이 글이 Next.js와 AWS Lambda를 사용하는 분들께 도움이 되길 바랍니다. 특히 ISR과 SSR의 차이를 이해하고, 프로젝트에 맞는 렌더링 전략을 선택하는 데 도움이 되었으면 좋겠습니다.

궁금한 점이나 다른 경험이 있으시다면 댓글로 공유해주세요! 🙌

목차

  • Next.js 배포 AWS 비용 이슈 개선하기
  • 🚨 문제 발견: 무엇이 잘못되었나?
  • 🔍 원인 분석: 3가지 설정 실수
  • 💡 해결 방법: 3단계 최적화
  • 📊 최종 결과
  • 🎓 언제 ISR을 쓰고, 언제 SSR을 쓸까?
  • 🛠️ 실전 적용 가이드
  • ⚠️ 주의사항
  • 📋 체크리스트
  • 🔮 추가 최적화 아이디어
  • 📚 더 알아보기
  • 💭 마치며