이번 포스트는 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는 의도치 않게 정적 처리될 수 있으며, 이 경우 클라이언트에서 캐시를 우회하려는 시도는 효과가 없다. 자동 정적 최적화는 강력한 기능이지만, 그 동작을 이해하지 못하면 디버깅이 어려운 함정이 될 수 있다.