Next.js 태그의 최신글

프론트엔드

BO에서 글을 수정해도 Web이 안 바뀌는 이유: 모노레포 환경 캐시 전략

BO에서 글 수정 시 Web에 미반영 문제 관리자 페이지(BO)에서 게시글을 수정하면 DB 및 BO에는 즉시 반영된다. 문제는 수정 직후 Web에서 해당 글을 확인하면, 내용이 이전 버전으로 계속 노출되었다. 관리자에서 수정이 완료되었는데 Web에는 반영되지 않는 상황이기 때문에, 운영 흐름상 수정 직후 화면에 변경 사항이 보이지 않는 것은 문제가 될 수밖에 없다. Web은 Next.js App Router + fetch 기반 SSR 구조로 구성되어 있었고, App Router의 fetch는 서버 레벨 캐싱을 기본으로 동작한다. 이는 일반적인 클라이언트 캐싱 라이브러리(예: React Query)와는 동작 방식이 다르다. App Router 환경에서 fetch는 기본적으로 force-cache로 동작한다. 단순 새로고침은 서버 재요청을 의미하지 않는다. 데이터는 빌드 시점 혹은 ISR 주기에 따라 캐시된다. 명시적으로 무효화하지 않는 한 캐시는 유지된다. 따라서 DB가 갱신되더라도, Web 서버의 캐시가 무효화되지 않으면 화면에는 반영되지 않는다. Web과 BO가 분리된 모노레포 구조 프로젝트는 pnpm 기반 모노레포로 구성되어 있다. 구조적으로 다음과 같은 특성을 가진다. web: SSR 기반 콘텐츠 렌더링 bo: CRUD 중심 관리자 기능 api: 데이터 처리 백엔드 이 세 애플리케이션은 같은 레포에 존재하지만, 런타임은 완전히 독립적으로 동작한다. 즉, 모노레포는 코드 레벨의 공유를 의미할 뿐, 런타임 공유를 의미하지 않는다. BO에서 게시글을 수정하면 DB는 갱신 되지만, Web 서버 내부 캐시는 별도의 프로세스로 유지된다. 따라서 BO에서 직접 revalidate를 호출하더라도 Web 서버의 캐시는 무효화되지 않는다. 설계한 캐시 무효화 전략 ① Web에 invalidation 전용 API route 생성 Web 서버 내부에서만 실행되는 캐시 무효화 엔드포인트를 별도로 구성하였다. 캐시 무효화는 반드시 Web 서버 내부에서 실행되도록 설계하였다. ② BO → API → Web Invalidation API 호출 게시글 생성/수정/삭제 성공 이후 BO가 호출한 백엔드 api는 Web의 invalidation API를 호출한다. 이를 통해 캐시 무효화 요청이 Web 서버로 전달된다. ③ Web 서버 내부에서 revalidateTag 실행 Web 서버 내부에서 revalidateTag를 호출하여 도메인 단위 캐시를 선택적응로 무효화한다. 게시글 상세는 도메인 단위 태그 + ID 단위 태그를 함께 사용했다. 전체 흐름은 다음과 같다. contracts 패키지에 revalidate 전략 포함하기 이 과정에서 contracts 패키지를 단순 타입 공유 계층으로 사용하지 않았다. 캐시 무효화 전략을 API 계약의 일부로 정의하였다. API contract는 다음과 같은 요소를 포함하였고, 캐시 전략을 위해서 여기에 revalidate 전략을 포함시켰다. HTTP Method Path Request Params / Body Response 타입 revalidate 즉, 특정 API가 호출되었을 때 어떤 캐시 태그가 무효화되어야 하는지까지 명시했다. 예시: UpdatePost Contract 설계 의도 이를 통해 다음과 같은 효과를 얻었다. BO와 Web이 동일한 무효화 규칙을 공유 도메인 단위 무효화 정책의 재사용 가능 캐시 전략을 명시적 설계 요소로 승격 결과적으로 contracts는 단순한 타입 패키지가 아니라 애플리케이션 간 계약을 정의하는 계층으로 확장되었다. 회고 이전에도 모노레포 구조를 구성해본 적은 있었지만, 두 개 이상의 애플리케이션을 동시에 개발하고 실제 데이터 흐름과 런타임 경계를 고려하며 설계한 것은 처음이었다. 특히 web, bo, api 세 애플리케이션과 contracts, ui 패키지가 유기적으로 연결된 구조에서 타입 공유만으로는 해결되지 않는 문제들이 드러났다. 같은 레포에 있다는 사실이 곧 실행 환경까지 공유한다는 의미는 아니었다. 런타임은 명확히 분리되어 있었고, 이번 캐시 이슈는 그 차이를 분명하게 보여준 사례였다. 또한 콘텐츠 운영 환경에서는 단순히 CRUD를 구현하는 것만으로는 충분하지 않다는 점을 알게 되었다. 데이터를 저장하는 것과 사용자 화면에 반영되는 것 사이에는 여러 계층이 존재한다. 이번 경험은 기능 구현을 넘어서 운영을 고려한 설계에 대해 고민하게 된 계기였다.

2026년 02월 19일 16:20

프론트엔드

Next.js 마이그레이션: 서버 컴포넌트(SSC)와 클라이언트 컴포넌트(CSC) 선택하기

React 프로젝트를 리팩토링 하면서 Next.js로 마이그레이션을 하게 되었고, 처음으로 서버 사이드 컴포넌트(Server-side Component, SSC) 를 사용할 기회를 갖게 되었다. 기존 React에서는 React Query를 활용하여 서버의 부담을 줄이며 데이터를 관리해왔는데, Next.js에서도 기존처럼 React Query를 사용할 수 있을지 고민하게 되었다. SSC와 CSC의 차이 Next.js의 서버 컴포넌트(SSC) 와 클라이언트 컴포넌트(CSC) 는 동작 방식이 다르다. SSC(Server-side Component): 서버에서 데이터를 가져와 미리 렌더링하여 클라이언트에 전달함 → SEO 최적화 CSC(Client-side Component): 클라이언트에서 데이터를 가져오고, 동적으로 업데이트함 → 실시간 변화 가능 이전처럼 React Query를 사용하여 데이터를 가져오려 했지만, 만약 모든 데이터를 클라이언트에서 가져온다면 Next.js의 서버 렌더링 기능을 제대로 활용하지 못하게 된다. Next.js를 사용하면서 서버의 역할을 최대한 활용해야 한다는 점에서 React Query만을 사용할 수는 없었다. 서버 부담 문제와 캐싱 처음에는 페이지를 이동할 때마다 서버가 데이터를 불러오면 부담이 커질 것이라고 생각했다. 하지만 Next.js에서는 서버에서 데이터를 가져올 때 캐싱을 통해 성능을 최적화할 수 있는 방법이 있었다. 위와 같이 revalidate: 60을 설정하면 60초 동안 같은 요청을 반복하지 않고 캐싱된 데이터를 반환한다. 덕분에 SEO를 챙기면서도 서버의 부담을 줄일 수 있다. 어떤 데이터는 SSC, 어떤 데이터는 CSC로? 처음에는 서버와 직접적인 연관이 있는 것은 SSC로, 정적인 것은 CSC로 사용해야 한다고 생각했는데, 사실은 그 반대였다. ✔️ 서버 컴포넌트(SSC)를 사용해야 하는 경우 SEO가 중요한 데이터 (검색 엔진 노출 필요) 초기 렌더링이 빠른 것이 중요한 데이터 변경이 자주 일어나지 않는 데이터 페이지 구조를 구성하는 주요 데이터 ✅ 예시 블로그 글 목록 (게시글 리스트) 블로그 글 내용 (제목, 본문) 사용자 프로필 정보 (변경이 잦지 않은 경우) 카테고리 목록 ✔️ 클라이언트 컴포넌트(CSC)를 사용해야 하는 경우 실시간으로 변화하는 데이터 사용자와의 인터랙션이 필요한 데이터 페이지 내에서 상태 변화가 필요한 데이터 ✅ 예시 좋아요 개수, 댓글 검색 기능 (검색어 입력 시마다 데이터 변경) 무한 스크롤 (React Query의 useInfiniteQuery 활용) 사용자 설정 변경 결론: SSC와 CSC의 최적 조합 초기 데이터를 서버에서 받아와 렌더링해야 하는 경우 → SSC 사용 자주 변경되거나 사용자 인터랙션이 필요한 경우 → CSC + React Query 사용 서버 부담을 줄이기 위해 revalidate 로 캐싱 활용 Next.js 마이그레이션을 하면서 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 나누는 것이 성능 최적화에 매우 중요하다는 것을 깨달았다. SEO가 필요한 데이터는 서버에서 미리 받아오고, 실시간 업데이트가 필요한 데이터는 클라이언트에서 관리하는 것이 핵심이다. 이를 잘 활용하면 서버 부담을 줄이면서도 빠른 렌더링을 유지할 수 있다.

2025년 03월 19일 08:12