July 25, 2022

Vercel + Github Action으로 노션 Cron Job 만들기

Vercel의 Incremental Static Regeneration (ISR)

Vercel의 ISR을 사용하면 전체 사이트를 다시 빌드 할 필요 없이 페이지 단위로 페이지를 정적으로 생성할 수 있다. 장점을 구분 지어서 생각해보면
물론 그렇다면 모든 사이트가 ISR을 써야 하겠지만, ISR의 단점 역시 꽤 있는데,
1.
빌드 타임에 페이지를 생성해야 하기 때문에, User-Agent / Cookie 같은 서버 Request 기반으로 페이지를 렌더링 할 수 없다. 그래서 사용자 별로 다른 콘텐츠를 보여주거나 서버-사이드에서 AB 테스트를 하는 것이 불가능하다.
2.
빌드 결과물을 갱신하기 어렵다. Next.js 에서 revalidate 라는 속성을 제공해서 1분, 30초 단위로 빌드 결과물을 갱신할 수 있도록 하지만 정확히 얘기해서 이건 cron job이 아니라 사용자가 방문한 후의 시간을 기준으로 한다. 쉽게 얘기해서, 하루 전에 블로그 내용을 업데이트해도 사용자가 한 명도 방문하지 않았다면 빌드 결과물이 갱신되지 않는다.
1번은 그래서 ‘~~~’ 경우에만 ISR을 사용하면 된다고 조심하면 되는데, 2번의 경우 꽤 치명적이다. 특히 트래픽이 거의 없는 이 사이트 같은 경우, Notion으로 글은 업데이트 했는데 누군가 방문하기 전까지는 페이지가 이전 콘텐츠 내용을 그대로 담고 있게 된다.
그러던 중, Next.js에서 ISR revalidate 라는 기가막힌 기능을 출시했다.
Css
font-size: 18px;

❤️ On-demand Revalidation

Currently, if you set a revalidate time of 60, all visitors will see the same generated version of your site for one minute. The only way to invalidate the cache was from someone visiting that page after the minute had passed. You can now manually purge the Next.js cache for a specific page on-demand.
Typescript
// pages/api/revalidate.js

export default async function handler(req, res) {
  // Check for secret to confirm this is a valid request
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    await res.unstable_revalidate('/path-to-revalidate')
    return res.json({ revalidated: true })
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return res.status(500).send('Error revalidating')
  }
}
Notion이 아직 Webhook을 제공하지 않기 때문에,
Yaml
name: 15-minute-cron
on:
  schedule:
    - cron: '*/15 * * * *'
jobs:
  cron:
    runs-on: ubuntu-latest
    steps:
      - name: Call our API route
        run: |
          curl --request POST \
          --url 'https://yoursite.com/api/cron' \
          --header 'Authorization: Bearer ${{ secrets.API_SECRET_KEY }}'
Typescript
// pages/api/cron.ts

import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  if (req.method === 'POST') {
    try {
      const { authorization } = req.headers;

      if (authorization === `Bearer ${process.env.API_SECRET_KEY}`) {
        res.status(200).json({ success: true });
      } else {
        res.status(401).json({ success: false });
      }
    } catch (err) {
      res.status(500).json({ statusCode: 500, message: err.message });
    }
  } else {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
  }
}