D
Archive
커리어
  • SEO
  • 컴포넌트
  • 블로그
  • 성능
  • AI
  • 아키텍처
  • 회고
  • 블로그 제작기 - 캐시 적용기
    블로그
    2026.01.31

    블로그 제작기 - 캐시 적용기

    서버컴포넌트의 등장이 불러온 새로운 캐싱 국면 1) pages router 에서의 데이터 기존 pages router 체제에서는 getServerSideProps나 getStaticProps같은 서버측에서만 실행되는 함수를 통해서 데이터를 불러오고 페이지에게 props로 넘겨주는 방식을 사용했었죠. 최상위에서 단 한 번의 데이터 요청만 발생하는 장점이 있었으나 페이지가 실제로 이 데이터가 필요한 자식 컴포넌트에게 넘겨주기 위해서는 tanstack-query같은 라이브러리로 클라이언트에 캐싱해두어 사용하지 않는 한 prop drilling 등 DX 차원의 문제가 있었습니다. 2) app router 에서의 데이터 이제 App Router에서는 리액트의 서버 컴포넌트를 이용하여 컴포넌트별로 데이터를 직접 페칭하는 방식을 사용합니다. 이 방식은 컴포넌트가 독립적으로 데이터를 처리하도록 만들어 prop drilling을 줄일 수 있는 등 개발 편의성을 높였지만, 그 반향으로 아래와 같은 문제가 발생할 수 있는데요. 여러 컴포넌트, 예약함수 등에서 같은 용도의 데이터를 중복 요청 발생 중복된 데이터 요청이 많아질수록 그만큼 네트워크 비용이 증가 따라서 Next는 이를 개선하고자 Request Memoization과 Data Cache 기법을 제공합니다. 편리하게도 Next는 fetch API에 기본적으로 이러한 기능들을 옵션으로 넣을 수 있게끔 해두어 간편히 사용할 수 있습니다. 하지만 저는 supabase client를 사용하고 있는데 공식문서에서는 이렇게 fetch가 아닌 DB client를 사용할 때 각 캐싱 기법을 사용하기 위해서 요구하는 함수로 감싸야했습니다. 뒤늦게 캐싱 확인 작업을 하면서 안 사실인데 supabase client도 리퀘스트 메모이제이션이 자동으로 적용이 되더라구요..? 하단 캐싱 적용 목차에서 자세히 보겠습니다 요구하는 함수는 위와 같고 아래부터는 캐싱 기법에 대해 알아보겠습니다. Request Memoization 알아보기 1) 현재 피드 페이지의 개선점 현재 피드 페이지에서는 모든 카테고리 데이터를 가져오는 fetchAllCategories API를 사용 중입니다. generateStaticParams에서 all-categories를 slug로 내려주기 위해 한 번 all-categories로 UI를 그리는 컴포넌트에서 한 번 으로 동일한 데이터임에도 중복 요청이 발생하게 됩니다. 더군다나 피드 페이지는 동적인 경로를 n개 정도 특정하여 정적으로 빌드하므로 페이지의 개수만큼 요청이 증가하는데요. 즉, /category/[slug]의 동적 경로 페이지에서 generateStaticParams가 반환할 값이 4개라고 할 때 generateStaticParams 자체에서 요청문 1번 정적으로 빌드될 페이지는 4개이므로, 각 페이지당 컴포넌트에서 요청문 4번 이렇게 총 5번이 호출되는 문제가 있습니다. 물론 정적으로 빌드할 피드 페이지 수가 4개 정도로 많지 않기도 하고, API가 런타임에서 수백 수천번 호출되는 것도 아니고, 당장은 slug가 몇 십개씩 추가될 일도 없어 빌드타임에 겨우 10번 미만 정도로 호출될 것이기에 크게 문제삼지 않을 수 있습니다. 그럼에도 slug가 100개까지도 증가할 수 있는 미래를 대비(?)하여 불필요한 요청은 최대한 줄여보자는 생각과 캐싱을 적극 도입하고자 하는 마음가짐으로 임해봅시다. 2) Request Memoization이란? 이러한 현상에 대한 개선책으로 Request Memoization이 있습니다. 컴포넌트, 페이지, 레이아웃, 일부 예약함수 등 어디서든 똑같은 API가 여러번 호출되었을 때 이를 묶어 단 한 번의 요청만 보내는 캐싱 방식입니다. ❗️ 다만 주의할 점! 리퀘스트 메모이제이션은 동일한 render pass에 한하여 발생하는 요청에 대해서만 적용됩니다. 동일한 render pass인 경우에만 리퀘스트 메모이제이션이 유효하다라는 말인데, 이 언급이 왜 중요하냐면 아래에서 볼 수 있듯 generateStaticParams는 페이지가 생성되기 전에 별도로 실행되기 때문입니다. 당연하긴 합니다. generateStaticParams를 통해 페이지를 몇 개 생성할 지 미리 알아둬야 페이지를 생성하는 행위가 가능해지는 것이니까요. 즉, 피드 페이지에서 n개의 정적 페이지 빌드로 인해 발생하는 n번의 fetchAllCategories API 요청을 리퀘스트 메모이제이션으로 하나로 묶는 데에는 유효하나, generateStaticParams에서 호출되는 fetchAllCategories API 요청까지는 하나로 묶지 못합니다. 페이지 생성과는 다른 render pass일 것이므로 별개로 보아 아무리 잘묶어도 적어도 2번의 요청은 발생한다는 것이죠 Data Cache 알아보기 리퀘스트 메모이제이션이 동일한 render pass에 대해 중복된 API요청을 방지하고자 하는 것이라면, 데이터 캐싱은 목적 자체가 백엔드로부터 가져온 데이터 보존에 있기에 서버에 항상 남습니다. 따라서 데이터 캐싱은 설령 다른 render pass라고 하더라도 같은 API 요청이라면 1회로 줄일 수 있게 됩니다. 즉, 이론을 지금 제 상황에 대입하자면 빌드 시에 fetchAllCategories API 요청은 아무 캐싱기법을 사용하지 않았다면 5번 리퀘스트 메모이제이션을 붙였다면 2번 데이터 캐싱을 붙였다면 1번 이 발생할 것입니다. 캐싱 기법 적용해보기 1) 아무런 캐시도 없이 빌드해보자 바로 빌드를 해보니 slug 개수만큼인 4개의 피드 페이지가 생성이 되었고, 그만큼의 API 요청횟수가 발생했는지 확인하기 위해 DB로그를 들어가보니..! 예상했던 결과인 DB 액세스가 5번이 아니라 2번만 로깅된 것을 볼 수 있습니다. 헛.. 당황스럽네요. 리퀘스트 메모이제이션같은 기능이 supabase client에도 자체적으로 있는 것인지, 아니면 내부적으로 fetch를 사용하여 Next가 이를 잡아 확장하고 있는 건지 리퀘스트 메모이제이션이 자동으로 적용된 모습입니다. 실제로 supabase client 라이브러리의 소스를 들어가보니 fetch를 사용하는 정황이 보이네요. 어떻게 이 메모이제이션을 제거할 수 있을지 고민해보았는데요. 공식문서에 AbortController를 쓰면 된다는 말도 있고해서 supabase client에 심어보는 등 이래저래 시도해보았으나 결국 해결책은 메모이제이션이라고 하는 매커니즘에 대한 돌파였습니다. 메모이제이션은 '같은 입력이면 같은 출력이 나온다'는 전제가 있어야 재사용할 수 있는 매커니즘입니다. 따라서 요청문 header에 요청마다 다른 값을 넣게 되면? 각 요청은 다른 입력이 되지 않을까? 라는 생각이 들더군요. 위처럼 코드를 수정한 후 다시 빌드를 한 후 DB로그를 들여다보니..! 의도했던 결과인 5건의 로그가 발견되었습니다! supabase client 자체적인 캐싱인건지, 아니면 Next에 의해 확장된건지 당황스러웠는데 메모이제이션의 특징을 생각해서 회피해본게 잘 들어먹혔군요 2) 리퀘스트 메모이제이션을 써보자 이제는 정말 리퀘스트 메모이제이션을 의도적으로 적용해보겠습니다. 헤더 세팅을 없애고 빌드를 해보면..! 예상했던 generateStaticParams에 의한 1건, 페이지 빌드단에서 리퀘스트 메모이제이션되어 1건의 로그까지 총 2건을 볼 수 있습니다. 참고로 제일 위에서 보았듯 명시적으로 react의 cache 함수로 요청문을 감싸서 리퀘스트 메모이제이션을 할 수도 있습니다. Next도 fetch 내부에서 이 cache 함수를 쓴 것이라고 하는데요. 저는 지금 supabase client가 의외로(?) 리퀘스트 메모이제이션을 지원해주므로 사용하지 않았으나 다른 DB 클라이언트를 사용하는 분들은 참고 부탁드립니다. 3) 데이터 캐싱까지 써보자 역시 fetch가 아닌 supabase client처럼 DB 클라이언트를 사용할 때 Next는 데이터 캐싱 사용을 돕기 위해 unstable_cache라는 기능을 제공합니다. 최근 use cache라는 지시자 방식도 있던데, 나온지 얼마 안 된 터라 더 안정적이어보이는 unstable_cache를 사용해보겠습니다. (이름은 unstable인데 그나마 stable해서 사용하는 상황이 아이러니하네요 ㅎㅎ) 빌드를 하고 DB 로그를 보니 의도대로 1번의 로그만 남은 것을 볼 수 있습니다. 원했던 결과를 얻었습니다. 리퀘스트 메모이제이션 더 자세히 보기 적용은 완료했으나 마지막으로 살펴볼 것이 있습니다. 위에서 저희는 의아한 것을 발견했었죠. 분명히 supabase client에 아무 것도 한 것이 없었음에도 자동으로 리퀘스트 메모이제이션이 적용된 현상이었습니다. 그 원인을 파악하고자 리퀘스트 메모이제이션쪽 소스코드를 분석해보았는데요. 중복 fetch 제거 관련 파일로 보이는 dedupe-fetch.ts를 보니 위에서도 말했듯 내부적으로 React.cache로 리퀘스트 메모이제이션을 구현한다는 것의 증거(빨간 박스)를 볼 수 있고, AbortController의 signal로 리퀘스트 메모이제이션을 제거할 수 있다는 등의 정황(파란 박스)이 여기서 보이네요 하단에 존재하는 이 부분이 중복 페칭을 하나로 묶는 곳으로 보입니다. cloneResponse가 리퀘스트 메모이제이션의 핵심 로직으로 보이는데요. 들어가서 보면 여기서 기존에 캐시된 페칭의 응답을 만들어 변수에 할당하여 사용하는 것을 볼 수 있습니다. 이러한 중복 요청 제거 함수를 patch-fetch.ts쪽 코드에서 globalThis.fetch에 재할당하여 fetch API를 확장합니다. 그리고 app-render.ts 부분에서 이 patchFetch 함수를 호출하여 로직이 적용되게 됩니다. 이쪽 소스코드 전반을 해석하기 위한 난이도가 좀 있지만 결국 중요하게 보면 되는 것은 NextJS가 리퀘스트 메모이제이션 기능을 붙인 fetch를 저희가 사용하게 될 네이티브 fetch에 global로 재할당하고 있다는 것입니다. 따라서 저희가 아무런 작업 없이도 fetch만 사용하면 리퀘스트 메모이제이션을 누릴 수 있는 것이었고, 내부적으로 fetch를 사용하는 supabase client가 자동으로 리퀘스트 메모이제이션을 갖게 된 이유가 여기 있었군요. 내용이 좀 길어졌네요. 다음 시간에는 역시 블로그를 위해 했던 작업으로 SEO를 직/간접적으로 개선할 수 있는 작업들에 대해 작성해보려고 합니다 :)

  • 블로그 제작기 - 정적 빌드와 SEO
    블로그
    2026.01.25

    블로그 제작기 - 정적 빌드와 SEO

    정적 빌드를 해보자 지난 글에서 제 페이지의 성격과 잘맞는 렌더링 방식을 탐구하였고, 정적 페이지 빌드와 풀라우트 캐싱(HTML + RSC Payload) 방식을 채택하였습니다. 이번 글에서는 정적 빌드를 하면서 겪은 점들을 SEO와 엮어 공유해보려 합니다 :) 빌드 명령을 내리면 Next는 명시적(라우트 세그먼트)으로 페이지 렌더링 방식을 지정하지 않는 한 몇몇 기제가 존재하는지에 따라 정적/동적 페이지인지 판단하여 빌드를 시작합니다. 저는 정적으로 빌드를 원하기에, 동적 빌드 기제만 주의하면 되었는데요. 피드 페이지를 빌드하는 과정에서 아래와 같이 의도치 않게 동적 빌드를 하게 되는 문제를 겪었습니다. Supabase SSR client의 cookies 사용 이 블로그는 모든 페이지가 SEO가 중요한 페이지로 서버측 렌더링을 활용하기로 했고, 따라서 데이터를 가져오기 위한 supabase client를 SSR용으로 미리 셋업을 해놓은 상태였습니다. 다만 이 client는 next/headers의 동적함수인 cookies의 사용을 요구하였는데요. cookies는 유저로부터 요청이 들어왔을 때 서버에서 쿠키로 받은 유저의 토큰 정보를 기반으로 페이지를 그리기 위한 용도이므로 런타임 서버에서만 사용할 수 있는 정보입니다. 그러기에 Next는 이 cookies라는 동적 함수를 호출한 페이지에 대해서는 항상 동적 빌드를 진행하게 됩니다. 제가 동적빌드를 하지 않는 이유는 아래와 같은데요. 피드 페이지는 cookies로 유저정보를 토대로 그려야 하는 페이지도 아니고 동적 렌더링 시 TTFB가 현저히 늦어지기 때문 정보 전달이 목적인 페이지이므로 여타 페이지에서도/가까운 미래에서도 유저정보를 기반으로 페이지를 구성할 일이 없어보인다는 점 나중에 좋아요나 싫어요같은 유저 정보가 필요한 기능이 추가되더라도 브라우저에서 유저측에서 직접 요청을 보내게하여 CSR을 할 것이기 때문 유저정보 기반 기능을 CSR로만 고려하는 이유? 추후 페이지의 CDN 캐싱을 고려하고 있기 때문입니다. 그게 무슨 상관이 있어요? SSR을 한다면 오리진이 될 내 Next 서버가 응답한 페이지가 CDN에 캐싱될 것인데요 이 때 유저정보가 입혀진 페이지가 캐싱이 되면 안되기 때문입니다. - ex) 좋아요 버튼이 눌러진 채로 페이지가 캐싱되는 것을 방지 따라서 유저정보가 입혀지지 않은 상태의 페이지를 유저가 응답받은 후 CSR을 통해 좋아요를 눌렀다는 것을 표시할 것 입니다. 따라서 SSR 클라이언트를 제거하고 일반 JS 클라이언트를 채택하였습니다. searchParams 사용 우선 앞서, 제가 왜 searchParams를 사용했는지 설명드리자면 1) 필터된 리스트 페이지의 인덱싱 피드에서는 글의 카테고리에 따라 리스트를 필터하는 기능이 있습니다. 저는 이 필터된 페이지들 각각이 모두 검색엔진에 인덱싱되길 바랬는데요. 그 이유는 현업에서 비슷하게 리스트 형태의 페이지가 있었고 카테고리에 따라 필터가 가능한 기능이 있었습니다. 기존에는 필터된 페이지들에 대해 캐노니컬로 하나로 묶어서 처리했었으나 💡 캐노니컬이란? 중복 콘텐츠 문제를 해결하기 위한 SEO 요소입니다. 예를 들어 https://example.com/products/shoes https://example.com/products/shoes?color=red https://example.com/products/shoes?utm_source=facebook https://www.example.com/products/shoes 같이 비슷한 주소가 여러개 있다고 할 때 검색엔진의 크롤링 리소스가 분산됩니다. 필터된 리스트 페이지 각각이 다른 의미로서 고유한 가치를 제공한다고 생각하여 캐노니컬을 제거하는 것을 원하였고 그 페이지에 대한 대규모 리뉴얼이 이루어질 때 필터별 페이지의 캐노니컬을 제거하여 모두 인덱싱이 가능하게 처리되면서 노출량이 급증하게 되어 지표에 좋은 영향을 끼쳤기 때문입니다. 2) 필터수단으로서 searchParams 사용하려 했으나.. 따라서 이 매커니즘을 블로그에 구현하기 위해 카테고리로 피드 리스트 페이지를 필터하는 수단으로 searchParams를 사용했는데요. ex) https://www.choiseongjun.com/?categoryId=1 이는 런타임에서 요청이 들어왔을 때 알 수 있는 동적인 값이라 일반적으로는 동적 빌드로 처리됩니다. 저는 path 역할을 하는 params가 미리 빌드타임에 특정 몇개의 값을 뽑아내어 정적빌드를 할 수 있도록 돕는 generateStaticParams를 갖고 있듯, 페이지의 변수값 역할을 하는 searchParams도 이에 상응하는 함수가 있을 것이라 생각했는데요... (얼추 generateStaticSearchParams라는 이름으로..) 아니더군요..🥲 params로 구분되는 path는 엄연히 페이지의 성격을 가지나, searchParams는 변수값정도로 여겨지기 때문이 아닐까 싶습니다. Next가 의도한 바는 SEO가 중요하면 개별 리소스로 표현해라. 즉, 핵심 콘텐츠는 경로로 표현해라라는 규칙을 강제한 것 같습니다. 그러니까 프레임워크인거죠. id대신 slug path를 사용해보자 정적 빌드를 위해서 generateStaticParams를 활용하기로 하였고, 처음에는 리스트 페이지를 필터할 식별자로 카테고리의 id를 고려했습니다. ex) https://www.choiseongjun.com/category/1 다만 공식문서 등 요새 SERP에 등장하는 정보 전달성 페이지를 보면 이렇게 유저 입장에서는 이해할 수 없는 식별자가 아닌 읽기 쉬운 유니크한 단어를 path로 쓰는 케이스가 많이 보이는데요. ex) https://f-lab.kr/blog/developer-blog-tips 이 것이 실제로 SEO 차원에서 의미가 있는지 문득 궁금해지더군요. 따라서 구글 검색엔진의 공식문서를 조사해보았습니다. 네. 설명 URL이라는 항목으로 명확하게 나와있네요. 네이버에서도 검색 친화적인 URL에 대해 언급한 바가 있구요. 즉, 구글과 네이버 모두 유저에게 도움이 될 만한 키워드를 URL에 포함시킬 것을 권장합니다. 다만 스크롤을 내리다보니 좀 모순되는 것 같은 의아한 워딩도 발견했는데요. 도움이 될 만한 키워드를 path로 구성하는 것은 유저 입장에서 더 많은 선택을 유발할 수 있으니 권장한다. 다만 검색엔진이 너의 페이지의 순위를 매기는 요소는 아니다. 라는 듯한 느낌으로 공식문서에서는 말합니다. 개인적으로도 /post/1보단 /post/how-to-make-pizza 라는 주소가 더 클릭하고 싶게 생겼긴 합니다. 결론은 기술적으로 장점은 없으나 유저 관점에서 클릭을 유발할 수 있으니 적용을 권장함이므로 적용하지 않을 이유가 없네요. 이렇게 id 대신 slug를 path 식별자로 적용을 하게 되었습니다. 그리고 비슷한 고민을 가진 포스트 상세 페이지에서도 마찬가지로 적용하게 되었습니다. ex) https://www.choiseongjun.com/post/caching-strategy-i-implemented 지금까지 정적 페이지로 빌드하기 위해 고민하며 적용한 것들을 정리해보았는데요. 내용이 좀 길어지는 것 같아 캐싱에 대한 내용은 다음 글에서 다루겠습니다 :)

  • 블로그 제작기 - 캐싱 고민
    블로그
    2026.01.22

    블로그 제작기 - 캐싱 고민

    블로그 만들기의 첫 걸음 일단 만들자 처음은 MVP 차원에서 성능은 고려하지 않고 UI/기능 구현에 집중하였습니다. 냅다 운영용으로 빌드를 해보았을 때 API 요청 용도로 세팅한 supabase server client에서 동적 함수인 cookies를 호출하고, 이 것이 전역적으로 사용되다보니 전부 동적 페이지로 빌드된 모습입니다.  이렇게 첫 빌드 후 페이지에 접근을 해보니 역시 느리네요..! 피드, 커리어 페이지를 각각 들어가보니 1.2초 정도로 데이터가 전무한 페이지임에도 TTFB에 꽤나 큰 시간을 잡아 먹고 있었습니다. TTFB : Time To First Byte의 약자로 네트워크 요청의 응답의 첫 번째 바이트가 도착하기까지 걸린 시간이다. 프론트에서는 HTML을 받고 그 문서 안에 담긴 리소스들을 요청하기에 보통 HTML 문서의 첫 바이트를 받기까지 걸린 시간을 뜻한다. 반드시 서버측 렌더링을 이용한다 문제의 원인을 짚기 전에, 내가 하고 있는 블로그 개발이라는 작업이 무엇을 위한 것인가를 고민해보면 블로그 콘텐츠는 사람들에게 공유되어야 하므로 무료 마케팅이라고 볼 수 있는 검색엔진에 노출되는 것이 가장 중요한 도메인 분야라고 생각합니다. 버전1 기준으로 현재 이 프로젝트에 존재하는 피드 페이지 커리어 페이지 포스트 상세 페이지 모두 다 SEO가 중요한 페이지이고, 따라서 검색엔진의 크롤러들이 내 콘텐츠를 더 잘 읽어갈 수 있도록 서버측에서 페이지의 정보를 담아둬야 하기 때문에 CSR은 배제하였습니다. 문제 규정과 해결책 고민 문제의 원인으로는 페이지가 동적 빌드되어 SSR로서 매 요청마다 서버에서 실시간으로 페이지를 그려야하기 때문이라고 판단했습니다. 따라서 정적 빌드와 Next가 제공하는 캐시 기법에 대해 고민했는데 이를 중심으로 해결 수단을 내보자면 1) 데이터 캐싱 🤔 데이터 캐싱은 한번 요청해서 받아온 응답 데이터에 대해 Next서버가 특정 시간동안 캐싱을 해두는 기법인데요. 즉 아래처럼 요청마다 DB에 액세스하게 되는 문제를 방지합니다. RTT를 줄일 수 있기에 너무나 확실한 개선 수단이라 후보 1로 두었습니다. RTT : Round Trip Time의 약자로 요청이 시작점에서 목적지로 갔다가 다시 시작점으로 돌아오는 데 걸리는 시간을 뜻합니다. 데이터 캐싱으로 API 요청을 스킵할 수 있어 RTT를 줄일 수 있습니다. 2) 풀라우트 캐싱 ✅ 1)에서 말한 매번 데이터를 요청하지 않고 캐싱해두는 것도 좋지만, 유저입장에서 더 가까운 단계에서 캐싱을 해두면 어떨까요? 일반적으로 유저가 SSR 페이지를 받기 위해서 유저가 프론트 서버에 요청 프론트는 요청을 받아 페이지를 그리기 시작함 페이지를 그리기 위해 데이터가 필요하다면 API 요청/응답까지 진행 하는 절차를 거칩니다. 위에서 TTFB가 1.2초가 걸렸던 이유는 이 모든 과정이 일어났기 때문입니다. 별 데이터가 없었음에도 1.2초는 꽤나 긴 시간이며 만약 제 블로그를 방문하는 유저도 많고, 이 페이지를 그리기 위해서 필요한 데이터가 많았다면 모든 유저의 각 요청마다 위 1~3의 플로우를 거친 후 응답하기에 프론트 서버도 바쁘고 DB도 많은 요청을 받게 되는 문제점이 있을텐데요. 데이터 캐싱으로 3은 생략하여 DB부하를 줄일 수 있겠지만 결국 서버까지 들어와서 캐싱해둔 데이터로 페이지를 그리는 과정이 필요하므로 서버에 대한 부하 위험은 여전히 존재합니다. 따라서 이보다 앞선 단계에서 페이지 자체를 캐싱해두면 서버가 페이지를 그리는 과정 또한 생략될 수 있겠죠. 즉, 위 1~3의 플로우를 거치지 않고 1에서 바로 응답을 할 수 있게 됩니다. 풀라우트 캐싱은 이러한 바램의 해결책으로서 미리 빌드단계에서 페이지를 정적빌드한 후 캐시해두었다가 들어온 요청에 대해 이 페이지를 유저에게 곧바로 서빙합니다. 현재는 이 풀라우트 캐싱이 적용되어 있지 않아 Cache MISS가 뜬 것을 볼 수 있습니다. 풀라우트 캐싱의 재생성 주기에 관하여 - ISR 제가 작성한 콘텐츠가 최초 공개 상태로 계속 유지되지 않을 수 있습니다. 수정으로 인해 내용이 바뀔 수 있고, 삭제로 인해 콘텐츠 자체가 삭제될 수도 있죠. 그런 변화에도 불구하고 풀라우트 캐싱된 페이지는 이러한 사항이 자동으로 반영되지 않고 변화 전 상태로 유저에게 서빙되는 문제가 있습니다. 그러기에 페이지를 주기적으로 재생성할 수 있도록 revalidation을 사용합니다. 다만 말그대로 주기적으로 재생성하는 것이기에 수정/삭제가 이루어졌더라도 그 주기가 지나지 않으면 기존에 캐싱된 페이지를 유저에게 서빙합니다. 만약 유저가 콘텐츠를 제작가능한 UGC 서비스라면 UX를 해치지 않기 위해서 유저의 수정/삭제 사항에 대해 페이지에 곧바로 반영해야 하므로 즉각 페이지를 재생성하기 위한 ODR까지 도입해야할 수 있으나 콘텐츠의 제작 권한이 오로지 어드민인 저에게만 있는 상황에서 즉각 재생성까지는 필요없다고 생각되네요. 유저 입장에서는 콘텐츠의 변화 사실을 당장은 알 필요가 없기 때문입니다. 즉, 수정/삭제된 페이지가 곧바로 반영될 필요는 없어서 ODR을 적용하지 않고 revalidation을 적용하는 선에서 마무리합니다. 💡 ODR: On-Demand Revalidation으로 정적 페이지를 유저가 콘텐츠를 수정/삭제 하는 등 원하는 시점에 재생성할 수 있는 기법 ODR이 정식 약어은 아니나 개인적으로는 ODR이라고 부르고 있습니다. 정리 1) 결론 결국에는 2가지 캐싱을 둘다 적용하기로 했습니다. 풀라우트 캐싱이 적용되었다면 데이터 캐싱이 크게 의미는 없을 수 있습니다만 해서 캐싱을 두 겹으로 두는 것이 나쁠건 없습니다. 이제는 풀라우트 캐시가 적용되어 Cache HIT가 뜬 것을 볼 수 있죠. 실제 개선된 시간을 같이 보자면 초기 HTML을 응답받는 시간이 20ms대까지 줄어든 것을 볼 수 있습니다. 최초 빌드 후 접근했을 때 걸렸던 1.2초(1200ms) -> 20ms 어마어마한 차이죠? 60배 정도의 차이가 납니다 2) 가치관과 캐시 저는 개발 가치관으로 늘 속도를 꼽는데요. 우리 프로젝트에게 느끼게 될 유저 경험도 큰 이유이지만 현업의 도메인 분야가 SEO를 중요시하였기 때문입니다. 2와 관련하여서 SEO와 속도와의 상관관계를 보자면 구글은 SERP에 노출하는 순위를 매기는 중요 지표로 코어 웹바이탈을 언급합니다. 그 코어웹바이탈 중 하나인 LCP 지표의 좋고나쁨은 페이지 응답 속도가 거의 대부분 좌우합니다. 페이지의 제공이 빨라야 그 다음 리소스들의 요청/응답도 따라오기 때문이죠. SEO가 곧 매출인 환경에서 일해왔던지라 속도를 위해 서버에서 네트워크 요청 수를 줄여보거나, 렌더링에 필요한 응답 데이터의 개수를 줄여보거나, HTML 문서 크기를 줄여보는 등 해봤지만 네트워크 홉 자체를 줄여버리는 캐시만큼 속도에 직빵인 것은 없어보입니다. 이 것이 캐시의 힘이고, 또 캐시를 어디서 하느냐가 중요한 이유입니다. 저 역시 현업에서 올해 하반기에 대규모 리뉴얼이 계획되어 있어 캐시 전략을 위한 인프라 설계 논의가 한창입니다 3) 추가적인 캐싱 방향성 위에서 언급한 캐싱들로도 충분해보이나 저는 AWS CF같은 CDN에 페이지를 캐싱해두는 것 까지 원하는데요. 풀라우트 캐싱을 걸어도 어쨌든 제 서버까지 요청을 보내야하는 것은 마찬가지인데, CDN을 붙이면 제 서버가 아닌 유저에게 가장 가까운 곳에서 리소스를 서빙받을 수 있습니다. 현업의 도메인에서 메인으로 다루는 질문/답변 페이지와 블로그 페이지는 ‘정보를 전달하는 글’이라는 특징상 노출이 중요하고, 따라서 SEO라는 공통점이 있기에 더 몰입하게 되었네요. 이렇게 풀라우트 캐시 + 페이지 revalidation (ISR)의 힘을 체감해보았고 실제 적용했던 과정은 다음 글에서 적어볼까 합니다. ...

  • 블로그 제작기 - 프롤로그
    블로그
    2026.01.19

    블로그 제작기 - 프롤로그

    블로그를 만들어보자! 블로그를 만들게 된 이유는 두 가지입니다. 1) 그냥 내 이름 박힌 도메인이 간지나보여서.. 귀여운(?) 이유일 수 있지만 어느덧 3년하고도 7개월차 접어든 내 경험을 풀어낼 장소를 나만의 디자인과 나만의 구조로 녹여보고 싶었어요. 2) SEO 스킬을 녹여보기 현 회사에서 2년반동안 MAU 1200만, DAU 70만까지 성장시켰고 그 과정에서 SEO에 집착하게 되었는데 그렇게 배운 스킬들을 녹일 나만의 작품을 갖고 싶었습니다. 딱 블로그가 좋은 재료 아닌가? 싶었고 내 블로그가 숏테일이든 롱테일이든 어떤 키워드를 검색했을 때 내 이름 박힌 도메인이 SERP 상위에 위치한다면? 시간을 헛되이 보내지 않았구나 느낄 것 같았어요 SERP: Search Engine Result Page의 약자로 검색엔진에 노출된 페이지를 뜻함 고려해야 할 점 전반적인 디자인 구현, 댓글, 인피니티 스크롤, 상태 관리 등 기본적인 기능 구현 등등이 가장 먼저 생각나지만 역시 눈이 가는 쪽은 SEO 관련 요소들입니다. sitemap 자동화 robots.txt 페이지 렌더링 기법 JSON-LD 서치콘솔 관리 semantic 태그 사용 meta 태그 사용 내부 링크 선순환 구조 만들기 Cloudfront같은 CDN 사용으로 FCP/LCP 확보 이 블로그를 만들면서 어떠한 SEO적 고려를 했는지 등등 풀어나가보려 합니다. 코어 기술 1) NextJS 말했듯 저는 SEO가 이 블로그의 최고 컨셉입니다. SEO 인덱싱 점수에 중요한 영향을 끼치는 것으로 2가지가 있다고 생각하는데요. 검색엔진의 크롤러가 제 글의 정보를 더 잘 읽어갈 수 있게 하기 유저가 더 빨리 제공받아 코어웹바이탈 지표에 유리 입니다. 두 가지의 공통점은 페이지 렌더링 기법에 따라서 영향을 받는다는 것이며, 따라서 ISR, SSR, SSG 등 다양한 페이지 렌더링 기법이 필요했고 또 신버전에서 PPR, streaming처럼 시도해보고 싶은 기능도 있어서 공부 차원에서도 적합하겠다 싶었습니다. 2) TailwindCSS 새로운 css 기능도 심심치않게 나오는 요즘 plain하게 css를 써볼까했지만 현업에서 TF업무를 진행하느라 시간상 여유가 없는 상태여서 css툴의 도움을 받기로 했습니다. 그 중에서도 TailwindCSS를 고른 이유는 Next와의 서버 컴포넌트 궁합, 또한 Next가 스타일링의 표준으로 삼고 있으므로 택하지 않을 이유가 없습니다. CSS in JS는 Next 공식문서단에서 예외사항에 언급하는 경우가 많음