ISR 도입 여정기

zena·2025년 1월 26일
1

Front-end

목록 보기
12/12

Beer-Next 프로젝트를 기획하면서 큰 갈래로 경험 및 개발해보고 싶었던 기능 중 하나가 바로 Next.js의 ISR ..!
패기롭게 도전했지만, 다양하게 겪었던 난항들을 기록하고자 한다 🤣

ISR이 뭐냐?!

ISR (Incremental Static Regeneration)

  • 전체 사이트 rebuild 없이 static content 업데이트 가능
  • prerender된 static page를 제공하기 때문에 server load 감소
  • 해당 페이지 헤더에 적절한 cache-control 적용
  • 빌드 타임 없이 충분히 많은 양의 컨텐츠 핸들링 가능

처음 해당 정의만 보고 단순히 'revalidate 시간만 지정해주면, 데이터가 최신으로 자동 패치된다고 생각하고 아주 좋넹!' 하면서 적용을 시작했다 (눈물)

하지만.. 뜻대로 되지 않았고 정보 검색과 학습을 통해 ISR의 진짜 속내을 알게 되었다 호락호락 하지 않음..

  • revalidate 시간 이후 새로운 요청이 들어와야만 데이터 revalidation 트리거
  • revalidate 시간 동안 모든 사람이 동일한 버전의 사이트를 보며, 해당 시간 이후 어떤 사람이 해당 페이지에 방문하면 캐시가 만료되고 최신 데이터를 받아오는 것!

즉!!!

단순히 revalidate 시간이 지난다고 해서 보고 있던 페이지의 데이터가 업데이트 패치되지 않는 것이었고, 나 외의 다른 트리거가 존재해야 한다는 사실을 깨닫고 말았다 ^^ 하루죙일 걸림 ㅋ큐ㅜ

이렇게.. 마음을 가다듬으면서 ISR 개념에 대해 디벨롭해 나아가면서 적용을 진행했다

난항의 시작

Notion API를 활용해 학습 로그 데이터를 패칭하는 부분에 해당 기능을 적용하고자 하였는데, app router를 활용하는 프로젝트 내에 ISR의 revalidate 값을 속성값으로 지정해주기 위해서는 fetch 형태가 필요하다고 했다

첫 번째 시도

Notion API database에 접근해 데이터를 불러오는 방식의 코드가 다음과 같았기에...

    const data = await notion.databases.query({
      database_id: databaseId,
    });

Route Handler를 활용해 request 로직을 분리하고 커스텀해 fetch 형태로 활용하고자 했다

💡가설
1. 첫 사용자의 페이지에서 X-Nextjs-Cache: HIT
2. 데이터베이스 변경 후 revalidate 시간 지남
3. 두번 째 사용자가 해당 페이지에 접근했을 때 X-Nextjs-Cache: HIT 상태로 데이터 최신화
4. 다시 첫 사용자가 새로고침 했을 때 X-Nextjs-Cache: STALE 로 변경되면서 데이터 최신화

/**
 * app > api > logs > route.ts
 * @returns Route Handler 생성
 */
export async function GET() {
  try {
    const data = await notion.databases.query({
      database_id: databaseId,
    });

    const response = NextResponse.json(data);

    response.headers.set(
      'Cache-Control',
      'public, s-maxage=10, stale-while-revalidate=5'
    );

    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch logs' },
      { status: 500 }
    );
  }
} 
export const revalidate = 10; 

const API_BASE_URL =
  process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000';

/**
 * @returns 학습 로그 리스트 서버 컴포넌트
 */
export default async function Logs() {
  const data = await fetch(`${API_BASE_URL}/api/logs`, {
    next: { revalidate },
  });

  if (!data.ok) {
    throw new Error('Failed to fetch logs');
  }

  const logData = await data.json();

  return (
    <div>
      {mapNotionApi(logData).map(log => (
        <LogBox key={log.order} {...log} />
      ))}
    </div>
  );
}

ISR의 경우 개발 환경에서의 테스트는 원활하지 않다는 것을 알게 되었고, 빌드 후 테스트를 진행해보았다!
빠르고 간단한 테스트를 위해 revalidate 시간은 10초로 짧게 설정했다 (시간 ~ 일 단위를 추천)

결과는....

  1. 첫 사용자 첫 진입

  2. notion DB test 값 추가 및 revalidate 시간 이후 첫 사용자 새로고침 진행

난항2...

🤯 배포 후 데이터 요청 실패 이슈

무슨 일인지 자꾸 데이터 요청 실패 이슈가 생겼고, 방법을 찾던 중 클라이언트 컴포넌트로 작성해서 요청하면 route handler 가 정상 작동한다는 사실을 알게 되었다

export default async function Logs() {
  let logData = [];

  try {
    const data = await fetch(`${API_BASE_URL}/api/logs`, {
      next: { revalidate },
    });

    logData = data.ok ? await data.json() : [];
  } catch (error) {
    console.error('Error fetching logs:', error);
    logData = [];
  }

  if (logData.length === 0) {
    return <div>로그 데이터가 존재하지 않습니다.</div>;
  }

  return (
    <div>
      {mapNotionApi(logData).map(log => (
        <LogBox key={log.order} {...log} />
      ))}
    </div>
  );
}

문제는 useEffect 내에서 해당 로직을 처리하다보니 페이지가 새로고침될 때마다 해당 데이터 패칭 로직이 수행됐다 ㅋㅋㅋㅋ

이렇게 되면 ISR을 활용하는 이유가..??

Route Handler 도입 포기... 그리고 수확..

💡가설
1. Nextjs app router 사용 + @notionhq/client 을 활용해 간단하고 편리하게 데이터 접근
2. fetch 형태 요청에 revalidate 값을 적용해 ISR 구현 목표
3. fetch 형태로 해당 값을 요청하기 위해 route handler를 활용한 request custom 로직 구현

클라이언트 컴포넌트는 제외하고 위 가설을 서버 컴포넌트를 활용해 열심히 구현해보고자 노력했다ㅜ

근데...
서버 컴포넌트에서 호출함에도 불구하고 unauthorized (401) 에러가 계속해서 발생했다
헤더에 notion key를 넣는 것 자체도 불가능, 어떤 영문인지 route handler를 활용해 접근을 하려고 하면 계속해서 실패했다..! ㅠㅠ

결국 route handler를 포기하고 일반 호출 함수로 분리해 revalidate 값을 해당 "페이지"에 적용하는 방식을 선택했는데 해결됐다. (ㅋ)

export const revalidate = 600; // 1시간

export default async function Logs() {
  try {
    const data = await callNotionApi();

    return (
      // UI
    );
  } catch (error) {
    console.error('Failed to fetch data in ISR:', error);

    return (
      // error UI
    );
  }
}

처음 접하기도 하고 공부하면서 놓치는 부분이 있을지도 모르지만, ISR의 정의도 제대로 알게 되고 route handler 도 경험해보고.. 럭키빅키자나?

.... 재밌었다... 유익했다... 진짜... 진짜로...

(+ 근데 지금 생각해보니 route handler를 해당 logs 페이지 컴포넌트에서 호출했으면....?)

참고

https://blog.logrocket.com/using-notion-next-js-isr-sync-content/#enabling-isr-synchronize-content-time

[https://velog.io/@unhyif/Next.js-캐싱-이해를-통해-ISR-문제-해결하기](https://velog.io/@unhyif/Next.js-%EC%BA%90%EC%8B%B1-%EC%9D%B4%ED%95%B4%EB%A5%BC-%ED%86%B5%ED%95%B4-ISR-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0)

https://bttrthn-ystrdy.tistory.com/144

https://blog.logrocket.com/using-next-js-route-handlers/

profile
🐤 FE developer 🎧

0개의 댓글