Web/Next.js

PATCH는 됐는데 GET이 안 바뀐다 : Next.js App Router 자동 정적 최적화 (Implicit Static Optimization)

Aidengoldkr 2026. 2. 5. 02:04

이번 포스트는 Next.js 서버의 자동 정적 최적화 (Implicit Static Optimization) 에 관한 것이다. 최근 진행한 웹 프로젝트인 ESSENTIA Science 홈페이지 (essentia-sci.org). 게시판 기능부터, 특정 사용자(임원진)에게는 PATCH 권한을 부여하여, 회원과 조직도를 수정 할 수 있게 구현하였다. 구조는 다음과 같다.

 

일반적인 공개 페이지인 Member Page와, 조직도는 어떠한 인증 없이 단순 GET 요청으로 DB에서 데이터를 받아올 수 있다.

다만 PATCH가 포함된 Admin Page는 GET, PATCH 등 모든 요청이 로그인 여부와 관리자 권한 여부를 OAuth를 통해 받아와 인증해야 요청을 할 수 있는 구조이다.

 

이슈

이러한 구조에서 다음과 같은 이슈가 발생했다.

1. Admin Page에서 회원 정보를 수정한다 (PATCH)

2. Admin Page에서 변경된 회원 정보를 확인한다 (GET)

Admin Page는 변경한 내용이 반영되어있다.

3. Member Page에서 변경 된 회원 정보를 확인한다 (GET)

Member Page는 변경한 내용이 반영되지 않는다!!

 

이 현상을 보고 먼저 웹이 캐시를 사용한다고 생각했고, 그로 인해 최신 정보가 아닌 기존의 구형 정보를 불러오는 것이다. 

이 문제를 해결하기 위해  cache: 'no-store' 을 추가했다. 하지만 여전히 이 이슈는 해결되지 않았다.

 

다시 한번 Member Page의 GET 요청과 Admin Page의 GET 요청을 비교해보았다. 

export async function GET() {
  const { admin } = await getAdminSession();
  if (!admin) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }

  const { data, error } = await supabase
    .from("members")
    .select("id, name, email, class, provider, depth_1, member_code")
    .order("class")
    .order("name");

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json({ members: data ?? [] });
}

Admin Page의 route.ts GET 함수 파트

export async function GET() {
  const { data, error } = await supabase
    .from('members')
    .select('id, name,email, provider, birth, school, class, subClass, sex, depth_1,depth_2,depth_3,member_code')
    .order('class', { ascending: true, nullsFirst: false })
    .order('member_code', { ascending: true, nullsFirst: false })
    .order('name', { ascending: true });

  if (error) {
    return NextResponse.json(
      { error: error.message },
      { 
        status: 500,
        headers: {
          'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
          'Pragma': 'no-cache',
          'Expires': '0',
        },
      }
    );
  }

  const allMembers = data ?? [];
  const filteredData = allMembers.filter(member => {
    const classValue = member.class;
    return classValue !== null && classValue !== undefined && classValue <= 2;
  }); //회원과 임원진만 필터링

  return NextResponse.json(filteredData, {
    headers: {
      'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0',
      'Pragma': 'no-cache',
      'Expires': '0',
    },
  });
}

Member Page의 route.ts GET 파트

 

두 코드의 차이점은 Canche-control을 통한 캐시 무시 설정과, 멤버 페이지의 회원 필터링, 그리고 getAdminSession() 의 유무이다. 이 오류의 주 원인은 getAdminSession() 의 유무이다.

 

Admin Page의 route.ts 는 getAdminSession()를 호출하며 getServerSession을 호출, 즉 "인증 의존" route 라서 Next 서버가 자동으로 동적 (dynamic) 처리 한다.

 

다만 Member Page는 어떠한 인증,헤더 의존성이 없어서 Next 서버가 정적/캐시 가능으로 판단될 여지가 있으며,  이때 Next 서버의 내부 캐시(특히 Route Handler의 내부 fetch/데이터 캐시)가 남아 있으면, 클라이언트에서 cache: 'no-store'를 붙여도 서버가 이미 만든 결과를 재사용할 수 있다. 이로 인해 Member Pages는 이전 트리(stale)가 계속 내려와 PATCH가 되어도 반영이 안 된 것이다

 

이것이 Next.js App Router의 자동 정적 최적화 (Implicit Static Optimization) 이다.

 

해결책

이 이슈를 해결하기 위해 다음과 같은 해결책을 사용했다.

export const dynamic = 'force-dynamic'; //항상 동적처리
export const revalidate = 0; //ISR 비활성화
export const fetchCache = 'force-no-store'; //라우트 내부 캐시 강제 OFF

이 3줄의 코드를 추가하여 해결했는데,

첫 줄은 Admin Page 와 같이 이 route를 항상 동적처리 하게 설정

둘째 줄은 이 route의 결과를 캐시하지 않고 매 요청마다 새로 생성하도록 만들어 ISR(정적 재검증)을 비활성화하는 설정

셋째 줄은 route 내부 캐시 자체를 꺼버리는 설정이다.

 

마무리

이번 이슈를 통해, Next.js App Router에서 API Route 역시 렌더링 결과물처럼 캐시 대상이 될 수 있다는 점을 깨달았다.

특히 인증,헤더 의존성이 없는 공개 API는 의도치 않게 정적 처리될 수 있으며, 이 경우 클라이언트에서 캐시를 우회하려는 시도는 효과가 없다. 자동 정적 최적화는 강력한 기능이지만, 그 동작을 이해하지 못하면 디버깅이 어려운 함정이 될 수 있다.