Zapier를 사용한 Next.js 프로젝트 업데이트 자동화
Webhook, ISR 기능으로 Vercel 배포 프로젝트의 캐시를 자동 갱신하여 실시간 업데이트를 구현
서론
Zapier를 이용하여 노션 데이터베이스가 변경될 때, Route Handlers API
를 호출하는 웹훅을 만들어 배포된 페이지의 변경사항이 업데이트 되도록 자동화하였습니다.
Zapier
자피어, 혹은 재피어는 웹 앱과 서비스를 연결하는 데 사용되는 웹 자동화 도구입니다.
Zapier를 사용하면 Zaps 라고 알려진 자동화된 워크플로를 만들 수 있습니다. 이러한 워크플로는 트리거
로 시작하여 액션
으로 끝납니다.
트리거
는 워크플로를 시작하는 첫 번째 앱의 이벤트이고, 액션
은 두 번째 앱에서 자동으로 완료되는 이벤트입니다.
- Zapier는 다양한 웹 애플리케이션과 서비스를 연결하여 자동화 워크플로우를 구축할 수 있는 온라인 자동화 도구입니다.
- 서로 다른 앱 간의 트리거와 액션을 설정하여, 반복적인 작업을 자동으로 처리할 수 있습니다.
- Zap은 하나의 자동화 워크플로우를 의미하며,
트리거
(Trigger)와액션
(Action)으로 구성됩니다.
ISR
Incremental Static Regeneration(ISR)은 Next.js에서 제공하는 기능으로, SSG
의 이점과 SSR
의 동적 업데이트를 결합한 기술입니다.
ISR
을 통해 개발자는 빌드 시점에 모든 페이지를 생성하지 않고도, 필요한 페이지를 동적으로 생성하고 재생성할 수 있습니다.
ISR의 동작 방식
- 초기 생성: 특정 경로에 대한 페이지가 처음 요청되면, 해당 페이지가 존재하지 않을 경우 서버는 페이지를 생성하여 사용자에게 전달하고, 이 페이지를 캐시에 저장합니다.
- 캐시 활용: 이후 동일한 경로에 대한 요청이 들어오면, 서버는 캐시된 정적 페이지를 즉시 반환하여 빠른 응답 시간을 제공합니다.
- 재생성(revalidation): 개발자는 각 페이지에 대해 재검증 주기(
revalidate
시간)를 설정할 수 있습니다. 설정된 시간이 지나면, 다음 요청 시 서버는 백그라운드에서 페이지를 재생성하고 캐시를 업데이트합니다. - 무효화: 페이지 내용이 변경되었을 때, 개발자는
on-demand
방식으로 특정 페이지나 경로의 캐시를 무효화하고 재생성할 수 있습니다.
ISR의 장점
- 성능 최적화: 초기 요청 이후 캐시된 정적 페이지를 제공하므로, 빠른 응답 시간을 유지할 수 있습니다.
- 빌드 시간 단축: 모든 페이지를 빌드 시점에 생성할 필요가 없으므로, 대규모 사이트의 빌드 시간을 크게 줄일 수 있습니다.
- 동적 콘텐츠 지원: 데이터 변경 사항을 일정 주기마다 반영하거나, 필요에 따라 즉시 반영할 수 있습니다.
- SEO 최적화: 정적 페이지로 제공되므로, 검색 엔진 크롤러에 대한 친화도를 높일 수 있습니다.
캐시는 3가지 방법을 사용하여 재검증할 수 있습니다.
1. 시간 기반 재검증
interface Post {
id: string
title: string
content: string
}
export const revalidate = 3600 //second
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts: Post[] = await data.json()
return (
<main>
<h1>Blog Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
)
}
/blog
의 게시물 목록을 가져와 표시하는 코드입니다.
1시간동안 이 페이지의 캐시가 유효합니다. 다음번에 이 페이지를 방문할 때 캐시가 무효화되며, 백그라운드에서 최신 블로그 게시글이 포함된 페이지의 새 버전이 생성됩니다.
시간 기반 재검증의 단점으로는 정해진 시간이 지나면 변경 사항이 없는 캐시가 만료되고, 새로운 캐시를 생성한다는 점입니다.
재검증 시간을 길게 설정하면 노션 데이터베이스에 변경이 일어나도 실시간으로 반영되지 않아 곤란해지게 됩니다.
2. revalidatePath를 사용한 주문형 재검증
- Server Side Router Cache:
revalidatePath
는 지정한 특정 경로만 무효화하고, 해당 경로에 대한 캐시만 갱신됩니다. - Client Side Router Cache: 현재는 모든 Router Cache가 비워지는 방식으로 작동하지만, 추후 업데이트로 특정 경로만 무효화하는 방식으로 변경될 예정이라고 합니다.
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/post-1')
이렇게 하면 다음 페이지 방문 시 특정 URL(/blog/post-1
)이 다시 검증됩니다.
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/[slug]', 'page')
// or with route groups
revalidatePath('/(main)/blog/[slug]', 'page')
위와 같이 제공된 파일과 일치하는 모든 URL이 다음 페이지 방문 시 다시 검증할 수도 있습니다.
type
: type은 선택사항입니다. 'page' | 'layout' 를 명시할 수 있습니다.
type
은 기본적으로 선택 사항이지만, 경로에 동적 세그먼트가 포함될 때는 꼭 명시해줘야 합니다.
예를 들어, /product/[slug]/page
같은 동적 세그먼트를 포함한 경로에서는 type
을 반드시 설정해 줘야합니다.
만약 경로가 구체적인 고정 경로(예: /blog/post-1
처럼 동적 경로가 아닌 실제 페이지)일 경우에는 type
을 생략해도 됩니다.
특정 페이지 아래의 페이지는 무효화되지 않습니다. /blog/[slug]
는 무효화되지만, /blog/[slug]/[author]
는 무효화되지 않습니다.
import { revalidatePath } from 'next/cache'
revalidatePath('/', 'layout')
이렇게 하면 모든 클라이언트 측 라우터 캐시가 제거되고 다음 페이지 방문 시 데이터 캐시가 다시 검증됩니다.
3. revalidateTag를 사용한 주문형 재검증
revalidateTag
는 경로가 다음에 방문될 때만 캐시를 무효화합니다. 즉, 한 번에 여러 재검증이 즉시 트리거되지 않고 무효화는 경로가 다음에 방문될 때만 발생합니다.
//fetch를 사용할 경우
fetch(url, { next: { tags: ['posts'] } });
//ORM, 혹은 데이터베이스에 연결하는 경우
import { unstable_cache } from 'next/cache'
const getCachedPosts = unstable_cache(url,
['posts'],
{ tags: ['posts'] }
)
Next.js는 fetch
를 확장하여 tag
를 설정할 수 있도록 하였습니다.
tag 기반 재검증은 fetch
이외에도 unstable_cache
의 tag
도 재검증 할 수 있습니다.
//app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
revalidateTag('posts')
}
revalidateTag
를 사용하여 특정 태그의 캐시를 무효화하고 새로운 데이터를 가져올 수 있습니다.
이 3가지 방법 중, revalidatePath
, revalidateTag
를 사용하여 캐시를 무효화하고, 재검증하도록 하였습니다.
동작 흐름
- 트리거 설정: Notion 데이터베이스에서 변경 사항이 발생하면 Zapier가 이를 감지합니다.
- 액션 설정: Zapier의 액션 단계에서 JavaScript 코드를 작성하여 Next.js의
route.ts
에 정의된 Route Handlers API를 호출합니다. - revalidateTag 실행: Next.js의 라우트 핸들러에서는
revalidateTag
함수를 호출하여 특정 캐시 태그를 무효화하고 페이지를 재검증합니다.
Zapier가 Webhook을 사용하여 노션 데이터베이스의 변경사항을 감지하는 줄 알았지만, polling 기반으로 동작합니다.
무료 플랜이면 15분마다 Notion 데이터베이스를 확인하여 새로운 아이템이 생성되거나 업데이트 되었는지 확인합니다. (pro 체험기간이라 변경사항이 빠르게 감지되어 웹훅인줄 알았습니다..)
1. 트리거 설정
Zapier 가입 후 Create
→ Zaps
화면으로 이동합니다.
Trigger를 클릭하여 App으로 Notion을 선택해줍니다.
Trigger event로 Updated Database Item
을 누르고 Notion 계정에 로그인하여 연동해줍니다.
연동한 노션 계정 내에 블로그의 Database
를 선택하면 트리거 설정은 완료입니다.
2. 액션 설정
트리거 테스트 통과했다면, 액션을 설정해줍니다.
저는 Code
를 사용하여 javaScript 코드를 작성했습니다. Code
를 선택한 후 Action event로 Run Javascript를 클릭해줍니다.
Input Data
를 설정해주면, Code에서 해당 변수를 사용할 수 있습니다.
따로 설정해주지 않아도 inputData
변수로 접근할 수 있지만, 코드 내에서 사용할 변수들만 설정해주었습니다.
id
: 트리거에서 감지된 데이터베이스 내 페이지의 id입니다.isRelease
: 노션 DB에서 만든 프로퍼티의 값입니다. DB 내 게시될 페이지인지를 boolean값으로 설정하였습니다.revalidateAuthKey
: Route Handlers API를 호출할 때 Zapier의 요청만 허용하도록 인증할 key입니다. Next.js 프로젝트의 환경변수로 동일한 key를 설정하고, 대조하여 사용합니다.api
: 업데이트 요청을 보낼 블로그의 API입니다.
let id = inputData.id
const isRelease = inputData.isRelease === 'True' || inputData.isRelease === "true";
const revalidateAuthKey = inputData.revalidateAuthKey;
const api = inputData.api;
let ret;
if (isRelease && id) {
const url = `${api}/api/revalidate?id=${id}`;
const opts = {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id,
revalidateAuthKey,
})
}
try {
const res = await fetch(url, opts);
ret = await res.json().revalidated;
} catch(err) {
console.log("Request Error: ", err);
}
} else if (!isRelease) {
ret = false
}
output = { id, isRelease, revalidated: ret };
isRelease
의 값이 boolean이지만, InputData의 모든 값은 string이기 때문에 문자열로 비교해주었습니다.
노션 DB 내 변경이 일어나 감지된 페이지가 게시되어있고, id가 유효하면 해당 페이지의 id
와 revalidateAuthKey
를 body에 담아 API 요청을 보냅니다.
output
은 Zapier의 History탭에서 확인할 수 있습니다.
3. revalidateTag 실행
저는 fetch
대신 notion sdk
를 사용하기 때문에 unstable_cache
를 사용했습니다.
캐시의 tags
를 id
로 설정해주어, 변경된 페이지의 id로 해당 캐시를 무효화시키고 재검증할 수 있도록 하였습니다.
/**
* 노션 페이지의 데이터를 마크다운 형식으로 변환합니다.
*/
export const getNotionArticlePage = (id: string) =>
unstable_cache(
async () => {
const mdblocks = await n2m.pageToMarkdown(id)
const mdString = n2m.toMarkdownString(mdblocks)
return mdString.parent
},
[id],
{ tags: [id] },
)
각 페이지 이외에도 모든 게시글의 캐시 데이터 또한 tags
를 추가하여 재검증 할 수 있도록 하였습니다.
/**
* 노션 데이터베이스에서 모든 게시글을 가져옵니다.
*/
export const getAllPost = cache(
unstable_cache(
async () => {
const res = await notion.databases.query({
database_id: dbID,
//...
})
const data = res.results.map((page: any) => {
return {
index: page.properties.index.number,
id: page.id,
//..
}
})
return data
},
["posts"],
{ tags: ["posts"] },
),
액션에서 호출하는 endpoint
를 app/api/revalidate/route.ts
에 생성해줍니다.
위에서 설정한 tag
를 무효화하고, 재검증하도록 revalidateTag
를 호출하였습니다.
클라이언트 사이드 라우트 캐시 또한 만료시켜 주기 위해 revalidatePath
를 호출하도록 작성하였습니다.
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache"
import { NextRequest } from "next/server"
export async function PATCH(request: NextRequest) {
const { id, revalidateAuthKey } = (await request.json()) as {
id: string
revalidateAuthKey: string
}
if (revalidateAuthKey === process.env.REVALIDATE_AUTH_KEY && id) {
revalidateTag(id)
revalidateTag("posts")
revalidatePath("/", "page")
return Response.json({ revalidated: true, message: id, now: new Date() })
}
return Response.json({ revalidated: false, message: id, now: new Date() })
}
4. 동작 테스트
설정을 마친 후, 노션DB 내 페이지를 수정하면 15분 이내에 자동으로 ISR
이 실행되어 배포된 페이지가 업데이트됩니다. 로그는 Zap history에서 확인할 수 있습니다.
Zapier는 1초 이내에 요청에 대한 응답이 오지 않으면 Time out
에러로 간주합니다.
종종 히스토리에 Error라고 표시되지만, Vercel 로그에서는 정상적으로 revalidate
가 완료된 것을 확인할 수 있습니다.
이 문제는 Vercel의 서버리스 환경에서 발생하는 cold start 지연과, Notion SDK의 응답 시간이 2초 이상 걸리는 경우로 인해 발생하는 것으로 보입니다.
오류를 없애려면 비동기 작업을 기다리지 않고 즉시 200 응답을 반환하도록 설정하면 됩니다.
회고
이렇게 데이터 변경 사항을 일정 주기마다 반영하지 않고 Zapier를 사용하여 필요에 따라 즉시 반영할 수 있도록 할 수 있습니다.
Zapier 내에 코파일럿이 상세하게 알려주어 손쉽게 설정할 수 있었습니다. AI 없인 못살아 정말 못살아
cold start문제는 server action을 호출하면서도 겪었는데 여기서도 말썽입니다.. 업데이트에 문제가 되는 것은 아니지만 Error라고 하니 마음이 꽁기한 건 어쩔수가 없는 거 같습니다.