Next.js 캐시와 버튼 UX가 공유하는 한 가지 진실

Next.js 캐시와 버튼 UX가 공유하는 한 가지 진실

피드백과 실제 동작이 어긋나는 순간, 사용자는 시스템을 믿지 않는다—캐시 레이어든 버튼 탭이든 원리는 같다.

Next.js 캐싱 App Router Router Cache revalidatePath 버튼 UX 입력 피드백 사용자 신뢰
광고

revalidatePath()를 호출했는데 화면이 그대로다. 버튼을 세 번 눌렀는데 한 번만 실행됐다. 두 상황은 전혀 다른 맥락처럼 보이지만, 뿌리는 하나다. 사용자(또는 개발자)가 기대한 것과 시스템이 실제로 한 것이 달랐다. 그리고 그 간극을 메울 피드백이 없었다.


캐시가 한 개라는 착각

dev.to에 올라온 Next.js App Router 캐싱 분석 글은 이 문제를 정면으로 짚는다. Next.js App Router에는 캐시가 하나가 아니라 네 개가 존재한다. Request Memoization, Data Cache, Full Route Cache, Router Cache. 각각 실행 위치도, 수명도, 무효화 방법도 다르다.

가장 많이 혼동되는 조합은 Data Cache와 Router Cache다. 서버에서 revalidatePath()를 호출하면 Data Cache와 Full Route Cache는 무효화된다. 하지만 Router Cache는 브라우저 메모리에 살아 있다. 서버 액션에서 아무리 revalidate를 외쳐도, 클라이언트가 이미 캐싱한 라우트 세그먼트는 그대로다. router.refresh()를 별도로 호출해야 비로소 브라우저 캐시가 날아간다.

Next.js 14→15 마이그레이션에서 조용히 깨진 앱들도 이 구조를 모르면 이해할 수 없다. 14에서 fetch()의 기본값은 cache: 'force-cache'였다. 15에서는 cache: 'no-store'로 뒤집혔다. 코드는 한 줄도 안 바꿨는데 동작이 달라진다. 이게 사용자 기대와 시스템 동작이 어긋나는 가장 조용하고 위험한 형태다.


버튼도 같은 문제를 갖고 있다

GeekNews에서 회자된 버튼 UX 글은 사진 회전 버튼 하나를 해부한다. iPhone은 빠르게 세 번 탭하면 탭을 기억해뒀다가 애니메이션이 끝나는 대로 순서대로 실행한다. Nothing Phone(Android)은 햅틱과 소리로 탭을 확인해 주면서도, 애니메이션이 끝나지 않은 상태에서 들어온 탭은 무시한다.

여기서 핵심은 피드백의 거짓말이다. Nothing Phone은 탭에 반응했다—소리도 나고 햅틱도 울렸다. 그런데 실제로는 아무것도 실행하지 않았다. 피드백이 등록됐다고 말하는데 동작은 실행되지 않는 것. 이건 버튼이 두 가지 일을 하고 있다는 뜻이다. 입력을 확인하는 일과, 그 입력을 실행하는 일. 둘이 따로 놀 때 사용자는 시스템을 신뢰할 수 없게 된다.

해결책이 반드시 탭 버퍼링일 필요는 없다. 애니메이션을 중단하거나 가속하는 것도 방법이다. 핵심은 하나—사용자가 애니메이션 종료를 기다리도록 강제하지 않는 것. 애니메이션은 상태 전환을 이해하도록 돕는 수단이지, 다음 입력을 막는 장벽이 아니다.


같은 구조, 다른 레이어

두 문제를 나란히 놓으면 패턴이 보인다. Next.js의 Router Cache는 서버에서 무효화 신호를 보냈는데 클라이언트가 모르는 상황이다. Nothing Phone 버튼은 사용자가 입력 신호를 보냈는데 시스템이 무시한 상황이다. 방향만 반대일 뿐, 구조는 동일하다. 신호를 보냈는데 반대편이 받지 못했고, 그 사실이 즉각 드러나지 않는다.

프론트엔드 개발자 입장에서 이 두 사례는 같은 질문을 던진다. 내가 만든 인터페이스에서, 사용자(혹은 시스템)가 보낸 신호는 항상 올바르게 전달되고 있는가? 전달에 실패했을 때 그 사실을 즉각 알 수 있는가?


실무에서 먼저 점검할 것들

Next.js 캐싱을 다루고 있다면 점검 포인트는 명확하다. 서버 액션 뮤테이션 이후 revalidatePath() 혹은 revalidateTag()만 호출하고 끝냈다면, Router Cache가 살아 있을 가능성을 항상 의심해야 한다. 즉각적인 UI 반영이 필요한 흐름이라면 router.refresh()를 함께 호출하는 패턴을 습관화해야 한다. Next.js 15로 마이그레이션했다면 모든 fetch() 호출을 감사해서 의도한 캐싱 전략을 명시적으로 선언해야 한다. 암묵적 기본값에 의존하는 코드는 버전이 바뀌는 순간 조용히 깨진다.

버튼과 인터랙션을 설계하고 있다면 질문은 이것이다. 입력 피드백과 실제 실행이 느슨하게 연결돼 있지는 않은가? 로딩 중에 들어온 반복 입력을 어떻게 처리할지 의식적으로 결정했는가? 디바운싱을 쓸 것인지, 버퍼링할 것인지, 인터럽트를 허용할 것인지—이 결정이 없으면 구현은 어쩌다 동작하는 코드가 된다.


디테일은 기능이 아니라 신뢰다

기능은 완성할 수 있다. 캐싱 레이어를 전부 구현하고, 버튼에 애니메이션을 붙이는 것도 가능하다. 그런데 사용자가 그 기능을 신뢰하지 않으면 완성된 것이 아니다. revalidatePath를 두 번 호출하면 혹시 될까 시도해본 경험, 버튼이 눌렸는지 확인하려고 한 번 더 탭한 경험—이 순간들이 쌓이면 사용자는 시스템을 믿지 않게 된다.

네 개의 캐시 레이어를 이해하는 것, 입력 피드백과 실행을 일관되게 연결하는 것. 이 디테일들은 화려하지 않다. 코드 리뷰에서 칭찬받기 어렵고, 디자인 시안에 표시되지도 않는다. 하지만 이것이 무너질 때 사용자가 잃는 것은 기능이 아니라 신뢰다. 그리고 신뢰는 한 번 잃으면 다시 쌓기가 훨씬 어렵다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요