2026-03-13

Next.js after() — 응답 먼저, 무거운 작업은 나중에

글을 저장하면 뒤에서 일어나는 일이 많다. 임베딩 생성, 캐시 무효화, 검색 엔진 알림. 이걸 다 끝내고 응답하면 사용자는 "저장" 버튼을 누르고 몇 초를 기다려야 한다.

그렇다고 Promiseawait 없이 던져버리면? 서버리스 환경에서는 응답이 나가는 순간 실행 컨텍스트가 죽는다. 백그라운드 작업이 중간에 끊길 수 있다.

Next.js 15에서 추가된 after()는 이 문제를 정확히 해결한다.

after()가 뭔가

after()는 응답이 클라이언트로 전송된 뒤에 실행되는 콜백을 등록한다. 응답 스트림이 닫힌 후에도 서버 런타임이 콜백 실행을 보장한다.

import { after } from 'next/server';
 
export async function POST(req: NextRequest) {
  const result = await saveToDatabase(data);
 
  after(async () => {
    await heavyBackgroundWork();
  });
 
  return NextResponse.json(result); // 즉시 응답
}

핵심은 응답 속도에 영향을 주지 않으면서도 작업 완료를 보장한다는 것이다.

실제로 어디에 쓰고 있나

이 블로그에서 글을 발행하면 3가지 백그라운드 작업이 돌아간다.

1. 벡터 임베딩 생성

글 내용을 청크로 나누고 OpenAI API로 임베딩을 생성해서 pgvector에 저장한다. 관련 글 추천에 쓰인다. 글 하나당 API 호출이 여러 번 필요해서 수 초가 걸린다.

after(async () => {
  await createPostEmbeddings(postId, title, description, tags, content);
});

2. 검색 엔진 알림 (IndexNow)

새 글이 올라오면 검색 엔진에 "이 URL 크롤링해라"고 알린다. 외부 API 호출이라 네트워크 지연이 있다.

after(async () => {
  await notifySearchEngines(slug);
});

3. 텔레그램 알림

Contact 페이지에서 메시지가 오거나 에러가 발생하면 텔레그램으로 알림을 보낸다. 알림 전송이 실패해도 본 요청에는 영향이 없어야 한다.

after(async () => {
  await sendTelegram('contact', `새 메시지: ${name}\n${message}`);
});

이 세 작업을 await로 직렬 처리하면 글 저장에 3~5초가 걸린다. after()로 빼면 응답은 200ms 이내로 돌아온다.

after()가 없었다면

Next.js 14 이전에는 선택지가 마땅치 않았다.

방법 1: await 없이 호출

// 이러면 안 된다
createPostEmbeddings(postId, title, description, tags, content);
return NextResponse.json(result);

Vercel 같은 서버리스 환경에서는 응답이 나가면 함수 인스턴스가 정리된다. 작업이 중간에 잘릴 수 있다.

방법 2: waitUntil (플랫폼 종속)

// Vercel, Cloudflare 등 플랫폼별 API
ctx.waitUntil(createPostEmbeddings(...));

동작은 하지만 플랫폼에 종속된다. 로컬 개발 환경에서 동작이 다를 수 있다.

방법 3: 외부 큐 (과도한 인프라)

Redis, SQS 같은 메시지 큐를 도입하면 확실하지만, 개인 블로그에 큐 인프라는 과하다.

after()는 프레임워크 레벨에서 이 문제를 풀었다. 플랫폼 무관하게 동작하고, 별도 인프라가 필요 없다.

주의할 점

after()는 만능이 아니다.

  • 실패 시 재시도 없음: 콜백이 에러를 던지면 그냥 끝이다. 중요한 작업이면 콜백 내부에서 try-catch를 해야 한다.

  • 실행 시간 제한: 서버리스 환경의 타임아웃은 여전히 적용된다. Vercel 기본 10초, Pro 60초.

  • 여러 번 호출 가능: 한 요청에서 after()를 여러 번 호출하면 전부 실행된다. 순서는 호출 순.

// 임베딩 실패해도 글 저장은 유지해야 하니까 try-catch 필수
async function createPostEmbeddings(...) {
  try {
    // 임베딩 생성 로직
  } catch {
    // 실패해도 조용히 넘어감
  }
}

글 수정 시 임베딩 분기 처리

글을 수정할 때는 상태 변화에 따라 다른 백그라운드 작업이 필요하다.

// draft → published: 임베딩 새로 생성
if (oldStatus === 'draft' && newStatus === 'published') {
  after(async () => {
    await updatePostEmbeddings(postId, ...);
  });
// published → draft: 임베딩 삭제
} else if (oldStatus === 'published' && newStatus === 'draft') {
  after(async () => {
    await sql`DELETE FROM post_chunks WHERE post_id = ${postId}`;
  });
// published 상태에서 내용 변경: 임베딩 업데이트
} else if (newStatus === 'published' && contentChanged) {
  after(async () => {
    await updatePostEmbeddings(postId, ...);
  });
}

이런 분기 로직이 응답 경로에 있으면 복잡해지고 느려진다. after() 안에 넣으면 응답은 깔끔하게 나가고, 뒤에서 알아서 처리된다.

정리

after()는 "응답 속도"와 "작업 완료 보장" 사이의 트레이드오프를 없애준다. 개인 프로젝트든 프로덕션이든, API 응답이 느려지는 원인이 후처리 작업이라면 가장 먼저 고려할 방법이다.

import { after } from 'next/server';

이 한 줄이면 된다.

관련 글

벡터 유사도 기반