D
Archive
커리어
  • SEO
  • 컴포넌트
  • 블로그
  • 성능
  • AI
  • 아키텍처
  • 회고
  • 내가 코드를 주기적으로 다시보는 이유
    회고
    2주 전

    내가 코드를 주기적으로 다시보는 이유

    첫 커리어, 특이한 도메인 경험 제 첫 커리어는 약 4년 전, 디지털트윈을 산업현장에 접목시켜 소프트웨어를 만들어 납품하는 기업에서 시작했습니다. 당시 KAI(한국항공우주), UIPA(울산정보산업진흥원)같은 공공기관과 주로 협업하였고 이들이 요청한 산업 환경을 3D로 구현하여 현장의 위험요소나 특정 설비의 결함이나 배터리 부족 등 특수 상황을 감지하여 관리자가 현장 전반 상황을 컨트롤 할 수 있게하는 서비스를 만들었습니다. 3년도 더 지난 일을 회고해볼까하는 이유는 첫 회사가 했던 일이 워낙 유니크한 도메인이기도 했고, 입사하기 이전부터 고착화되어 있던 보일러플레이트를 개선하여 주니어로서 커리어 처음으로 프론트엔드 팀 코드베이스에 기여한 것 이 아직 기억에 남기 때문입니다. 무엇이든 처음이 가장 기억에 남는 것 같네요 ㅎㅎ 그리고 이때의 경험으로 코드베이스를 주기적으로 다시 보게 되는 습관이 들었거든요. 제가 처음으로 담당하여 맡았던 일인 UIPA라는 기관이 주관하는 선박 모니터링 프로젝트 중 있었던 일입니다. 보일러 플레이트와 새로운 시선 1) UIPA 프로젝트의 내용 선박 연료탱크에 연료가 얼마나 남았는지, 부품의 배터리가 얼마 남았는지, 물탱크 수위, 증기기관의 열 온도가 안정적인지, 선체의 기울기, 경도 위도 등등 전반적인 선박의 운항 상태를 모니터링하기 위해 웹으로 대시보드를 띄워줘야 했습니다. 2) 보일러플레이트의 구조 기관의 수주를 받아 작업을 진행하게 되면 프론트엔드 팀에서는 메인 페이지에 iframe으로 별도 페이지를 하나 더 띄우고, 주기적으로 백엔드로부터 데이터를 받는다 SignalR 이라고 하는 C# 생태계의 소켓 통신방식으로 웹에 1분마다 주기적으로 데이터를 공급합니다. 그리고 3D 팀이 넘겨준 Unity 3D 빌드파일을 이 iframe에 WebGL 라이브러리로 렌더링한다. Unity가 웹에 렌더링되고 유저가 3D 요소를 클릭하면 그 요소의 id 정보를 iframe으로 보낸다. iframe은 이 id 정보를 받아 메인 페이지로 전송(postMessage)한다. 메인 페이지는 iframe이 보낸 id를 받아 공급받은 데이터셋에서 찾아서 위젯 UI로 띄운다. 구조의 굳어진 보일러 플레이트로 작업을 진행했었는데요. 보시듯 iframe이 플로우 중간중간마다 끼어 브릿지 역할을 하고 있습니다. 결론부터 얘기하자면 입사초기부터 iframe의 존재 이유에 의문이 있어서 제거를 건의하였고, 컨펌받아 평균적으로 1200줄 이상 쓰이던 iframe 레이어 없이 구현할 수 있었습니다. 다른 프로젝트였던 KAI에서 이 iframe 레이어를 위해 작성되었던 코드 라인이 1200~1300줄이었던 것을 기준으로 하였습니다. 3) 보일러플레이트의 개선 리더분 말씀으로는 예전에 모종의 이유가 있어 iframe로 감싸는 형태로 만들었는데 지금은 그냥 이 형태가 룰처럼 굳어져와서 계속 사용하고 있다고 하셨습니다. iframe을 유지해야 할 마땅한 근거도 없고, 코드 가독성과 유지보수의 확실한 개선이 예상되어 PoC 진행을 컨펌받아 동작이상 없음을 증명하였고, 이 UIPA 프로젝트에서 적용하여 iframe 없는 구조를 완성하였습니다. 즉, 메인 페이지에 Unity 3D 빌드파일을 WebGL 라이브러리를 통해 렌더링한다. Unity가 웹에 렌더링되고 유저가 3D 요소를 클릭하면 그 요소의 id 정보를 메인 페이지로 보낸다. 메인 페이지는 이 id 정보를 받아 공급받은 데이터셋에서 찾아서 위젯 UI로 띄운다. 로 전체 플로우에서 2개의 단계가 제거된 것을 볼 수 있습니다. 새로운 시선으로 보려고 하는 이유 이렇게 커리어의 첫 프로젝트에서 새로운 관점을 통한 개선이 만족스러웠다보니 그 이후부터, 제가 지금 다니는 회사의 프론트엔드 리더는 아니지만 새로운 인력의 온보딩에 참여할 기회가 생기면 주로 저희 코드베이스의 개선점을 묻곤 합니다. 아무래도 재직한지 3년이 다 되어가는 저같이 고인(?) 사람보다는 새로운 동료가 본인이 최근까지 겪어왔던 색다른 관점에서 평가나 제안을 할 수 있기 때문이지요. 실제로 저는 지금 다니고 있는 회사의 온보딩 시기에 질문상세 페이지를 SSR로 구성하면서 Promise.all로 5개 정도의 API를 페칭 중이었는데, 그 중 정말 코어 데이터는 2개 정도에 불과했기에 설령 다른 것들이 문제가 발생해도 에러를 내지 않게끔 Promise.allSettled로 바꾸자는 제안을 하여 런타임 안정성에 기여했습니다. 또 작년에 새로 조인하신 분은 온보딩 시 Smart App Banner라는 생각도 못하고 있었던 부분을 제안해주셔서 웹에서 앱으로 인입시키는 기점을 마련해주기도 하셨습니다. 위 작업들이 인상깊었던 것은 모두 코드 10줄 미만의 어렵지 않은 사소한 변경임에 비해 임팩트가 있었다는 점입니다. 조직이 고착화되지 않으려면 이러한 환기가 참 중요하다고 생각됩니다. 그렇다고 매번 새로운 동료를 모실 순 없으니 저는 주기적으로 코드베이스를 한번씩 훑어보곤 합니다. 몇달 몇년간 있었던 코드지만 간혹 새롭거나 의아하게 느껴지는 부분이 있거든요. 그렇게 발견했던 것이 대표적으로 meta keyword입니다. 꽤 오래전부터 SEO에 유의미하지 않은데에도 여기에 API까지 붙여서 성심껏 구성하고 있었었죠. 처음 코드를 짠 사람도, 그 뒤로 손댄 사람도 'meta 태그니까 SEO에 좋겠지'라는 전제하에 의심을 하지 않았던 겁니다. 저 역시도 몇 달간 그 코드를 지나치고 못 봤었구요. 코드가 잘 굴러가기 때문에 보지 않게 됨으로서 발생하는 허점을 예방하고자 저는 주기적으로 코드를 다시 들여다봅니다. 첫 직장에서 iframe을 의심했던 그때처럼요

  • MFE는 (웬만하면) 독이다
    아키텍처
    3주 전

    MFE는 (웬만하면) 독이다

    MFE를 모놀리스로 전환한 이유 작년 10월 말쯤, 회사 웹 인프라에 큰 변화가 있었습니다. 바로 기존 MFE구조를 하나의 모놀리스(SPA)로 통합했는데요. 페이지별로 각 서버를 나눠 둔 수직적 분할 형태의 MFE 구조였는데 결론부터 얘기하자면 MFE가 저희 서비스 특징과 전혀 어울리지 않았고, 많은 단점에 비해 사소한 장점조차 하나도 없다고 판단하여 전환하였습니다. 입사시기부터 MFE 구조였는데 히스토리를 여쭤보니 당시에는 주요 페이지인 질문상세가 죽어도 다른 페이지는 문제없이 서빙하기 위해서 이러한 구조를 택했다고 하셨습니다. 질문상세에 트래픽이 많이 쏠리는 저희 서비스 특성상 당시의 사정이 이해가 가더군요. 하지만 운영을 하면서 느꼈던 점은 그 다른 페이지들이 대부분 질문상세 페이지로 향하는 진입로 역할을 하는 페이지여서 주요 페이지를 제공하는 서버가 죽으면 다른 페이지를 제공하는 서버가 살아있는게 유저에게 크게 의미있나?..싶기도 하고 RUM을 통해 2주 기준으로 페이지 방문 수를 확인해보니 질문상세가 1000만임에 비해 나머지는 10만 언저리로 미미한 수치였습니다. 이런 이유로 각 페이지별로 서버를 띄우는 것은 낭비라고 생각하였습니다. 또한 보통 MFE하면 언급되는 장점인 독립적 개발/배포 프로세스도 저희 서비스에서는 빛을 보지 못했던게 독립적 개발을 언급할 만큼 팀 규모가 크지 않기도 하고 모든 페이지가 유기적으로 연결되어 있어 여러 페이지를 한번에 배포해야 하는 경우가 대다수였기 때문입니다. 장점이 몇가지가 있다고 한들 (실제로 하나도 없기도 했고) 비용 부담이 과했기에 트레이드오프상 마이크로 구조를 하지 않는 것이 압승인 상황이라고 판단했습니다. 그 외에도 여러 단점이 있었는데 약 1년반동안 MFE로 운영하면서 느꼈던 단점을 세부적으로 정리해보겠습니다. UX 악화 각 페이지를 제공하는 서버 오리진이 다르기 때문에 뒤로가기나 내부링크 클릭 시 소스를 새로 받아오기에 완전한 새로고침으로 동작합니다. 따라서 유저입장에서는 무거운 JS번들을 매번 새로 받아와야 하기에 느리다고 느낄 수 있고 무엇보다도 피드 -> 질문상세로 이동 후 뒤로가기를 하면 스크롤 초기화 + 불러왔던 피드 데이터들이 초기화 되어버린다는 참사가 발생합니다. 저희 서비스는 피드 위주 페이지의 비중이 높은 편이기도 하고, 유저층 상당수가 피드를 대충보고 넘어가는게 아니라 보상을 얻기 위해 답변을 달 질문을 찾고자 피드 스크롤을 내리는 빈도가 잦기에 이런 문제는 큰 유저 불편을 초래합니다. 이는 DX 악화와도 연관이 있습니다. DX 악화 위에서 말씀드린 피드 초기화 문제는 서비스 특성상 용납할 수 없으므로 우회 코드의 추가가 필요합니다. 인프라적인 문제를 소스코드 레벨에서 커버를 해야한다는 것인데요. 즉, 평소에는 잘 사용하지 않는 코드를 사용하여 브라우저와 제 애플리케이션간 싱크를 맞춰야합니다. 저는 리스트 데이터와 스크롤을 복구하기 위해 질문상세로 이동 시 스크롤 위치를 세션 스토리지에 저장해두고 리스트 데이터를 indexed DB에 저장했었습니다. 그리고 질문상세에서 피드로 돌아가면 load 이벤트를 감지하여 indexed DB에 저장한 데이터를 상태에 동기화 + referer를 참고해서 세션 스토리지에 저장해둔 스크롤 위치를 강제로 브라우저에 동기화시킵니다. 다만 의아하실 수 있는데요. BF캐시가 있어서 굳이 복구 처리를 안해도 알아서 피드에서 질문상세 페이지로 접근하던 순간의 스냅샷으로 돌려보내기에 문제없지않냐? 라고 할 수 있지만 피드에서 BF 캐시를 비활성화해야 하는 사유가 있었습니다. 💡 BF 캐시란? 사용자가 브라우저에서 '뒤로' 또는 '앞으로' 버튼을 눌러 페이지를 이동할 때, 자바스크립트 힙(Heap)과 DOM 상태를 포함한 페이지 전체의 스냅샷을 메모리에 저장해 두었다가 즉시 불러오는 브라우저 최적화 메커니즘 유저가 질문을 작성하다가 이탈한 경우 임시저장된 질문이 있다는 배너를 피드에 노출하게 되는데 질문작성 페이지에서 이 임시저장 데이터를 초기화하고 뒤로가기 했을 때 배너가 사라져야 함에도 BF 캐시로 인해 이 배너가 사라지지 않고 계속 노출된다는 문제가 있어 기획적인 요청에 따라 BF캐시를 비활성화 해놓은 상태였습니다. 따라서 이전 페이지의 상태를 어딘가에 저장해두는 별도 우회코드가 필요했던 것이었습니다. 또한 질문상세에서 좋아요라도 눌렀다가는 피드와 동기화를 위해 indexed DB에 있는 피드 데이터를 꺼내서 이 질문을 찾고 좋아요 필드를 찾아 업데이트하는 로직까지 추가되어야 하는 등 복잡도가 올라가게 됩니다. 아무래도 indexed DB 컨트롤이나 스크롤 복구 코드는 평소에는 잘 사용하지 않는 코드이다보니 가독성 난이도가 올라가 DX를 해칠 수 밖에 없습니다. 성능 저하 1) 페이지간 중복된 리소스를 매번 새로 로드 레이아웃같이 모든 페이지에 동일하게 적용되는 JS 청크를 페이지 이동마다 매번 새로 받아와야 한다는 문제가 있습니다. 다행히도 저희 서비스의 헤더나 푸터는 다시 받아오는게 부담될 정도로 무거운 코드는 있지 않았지만 불필요한 중복 로드임에는 틀림없습니다. 2) 비즈니스 데이터를 매번 새로 요청 서비스의 주요 비즈니스 데이터로 카테고리라는 데이터가 존재합니다. 피드의 각 아이템 데이터는 카테고리 id를 갖고 있고, 배열 길이 250개에 해당하는 API를 요청하여 이를 매핑하는 형태로 사용하고 있었습니다. 피드 형태의 페이지(메인 피드, 프로필 피드, 단일토픽 피드)를 방문할 때마다 이 긴 배열을 받아오기 위해 API 요청이 발생하게 됩니다. 서버비용 낭비 AWS ECS 체계에서 6개의 페이지 * 적응형 분기(PC/모바일) = 12개의 서비스가 운영되고 있고 각 서비스 당 적어도 2개의 태스크를 띄우고 있었습니다. 따라서 최소 24개의 태스크가 운영되고 있고 태스크 1개(1vCPU, 2GB Mem)가 월별 41.45달러이므로, 24개라면 연간 11,940 달러가 됩니다. 질문상세쪽 태스크 4개를 제외하고 트래픽이 유의미하지 않은 20개의 태스크가 연간 9,948달러라는 엄청난 낭비를 부르고 있습니다. 달러 환율 1450원 기준으로 14,424,600원 최소 기준으로 잡은 비용만 이 정도이고 데이터 트랜스퍼 비용이나 질문상세 서비스에서 스케일 아웃으로 증가하는 태스크 개수 등을 고려하면 +@의 비용도 존재합니다. 다른건 모든 단점을 고사하고 이 비용문제 단 하나만으로도 MFE를 모놀리스로 전환할 이유가 충분합니다. 전환 중 겪었던 문제 피드 페이지와 질문상세 페이지의 데이터 동기화를 위해 indexed DB로 데이터를 컨트롤 했던 로직을 제거하면서 tanstack query의 캐시를 업데이트하는 코드를 추가해야 하거나 location.href를 useRouter같은 history 기반 api로 바꾸거야 한다는 등 예측 가능했던 이슈를 제외하고 전혀 예상못했던 문제가 있었는데요. 앱의 하단 네비게이션 바는 네이티브 로직으로 만들어졌고 노출 조건으로 웹뷰의 URL의 변화를 체크하면서 특정 페이지에서만 노출하도록 화이트리스팅이 되어 있었는데 SPA 기반 네비게이팅에서는 이벤트가 트리거되지 않는다는 문제가 있었습니다. 즉 기존 location.href의 새로고침 방식의 이동에서는 네이티브의 화이트리스팅 로직이 정상 트리거되었으나 useRouter로 이동시에는 네이티브가 감지를 못해서 하단바를 숨기거나 다시 노출하지 못하는 현상이었습니다. 따라서 앱에서 검토했을 때 이 SPA 이동을 감지할 수 있는 방법이 없었기도 했고 차후 화이트리스팅된 목록을 수정 해야 한다면 웹에서 컨트롤하는 것이 배포 대응에 더 유리할 것 같아서 웹에서 URL의 path 변경을 감지하여 하단바 on/off를 컨트롤하는 앱프로토콜을 호출하도록 수정하였습니다. 예상치 못했던 추가적인 수확 언급했던 서버비용 감소, UX/DX 개선 외에도 빌드시간이 크게 감소했다는 수확이 있었습니다. 빌드 대상의 물리적인 숫자가 줄어든거니 당연하다고 볼 수 있는데 실제로 반영하기 전까지 예상하지 못하고 있었네요 ㅎㅎ; 12개의 서비스를 모두 각각 빌드하였을 때를 기준으로 10~12분의 빌드타임이 걸렸었지만 모놀리스 전환 이후로는 2분으로 줄어드는 큰 개선이 있었습니다. 시간을 많이 먹는 타입체크와 린트의 횟수가 1회로 줄어든 점, 번들링도 1회로 줄어든 점이 꽤 커보입니다. MFE는 기술적 고려대상이 아닌 조직적 고려대상 개인적인 생각을 말해보자면 MFE에 대한 악담(?)만 늘어놓았지만 4~5명 규모의 우리 프론트엔드 조직과 맞지 않았기 때문이지 팀 운영 효율을 중시해야하는 정도의 큰 조직 규모라면 도입해볼 법 하다고 생각합니다. 즉, 우리는 폭발적인 트래픽에 대응하고자 웹 인프라 안정성을 위해 MFE를 도입할거야!같은 기술적인 이유보다는 우리 프론트엔드 챕터의 규모가 커졌으니 작업간 책임 소재도 불분명하고 컨플릭도 많이나는 것 같아. UX나 비용적 단점에도 불구하고 챕터 운영 효율이 더 중요해진듯 해. 따라서 페이지나 도메인별로 팀을 분리하고 각 팀의 범위를 제한하고 책임을 끌어올리기 위해 MFE를 도입해야겠어. 같은 관점으로 접근해야 한다고 봅니다. 팀 운영의 효율적 측면 vs UX/DX 및 비용 등등 단점이 크로스오버되는 지점을 넘어선 조직이어야 MFE가 빛을 발휘할 수 있다고 생각해요 히지만 웬만한 스타트업 프론트엔드 챕터의 규모가 팀의 효율을 논하여 분리해야할 만큼 크진 않을 것이고, 트래픽 폭증으로 인한 구조 고민은 실제로 그 상황이 도래했을 때 고려해봐야지 일어나지도 않은 일에 섣불리 대응했다가는 개발적 피로와 비용 낭비만 초래할 뿐이라고 생각합니다. 트래픽이 치솟는 꿈같은 순간이 다가오면 우선 기존 서버의 스케일 아웃으로 대응하다가 MFE 도입을 고려해도 충분하다고 봐요. 따라서 저는 MFE 도입에 대해 보수적인 접근을 추천합니다. 작은 규모의 팀에서 큰 규모의 팀이 일하는 방식이 힙해보인다고 따라했다가는 뱁새가 황새따라하는 모습이 될 가능성이 크다고 생각합니다. 제 경험에 감정 이입이 되다보니 좀 완강한 표현이 있었을 수 있는데 다른 생각이나 관점이 있으시다면 댓글 부탁드립니다!

  • SSR 최적화는 어떻게 해야할까
    성능
    1달 전

    SSR 최적화는 어떻게 해야할까

    코어 웹바이탈 웹 개발자가 서비스를 운영할 때 유념해야할 것으로 코어 웹바이탈이라는 지표가 있습니다. LCP INP CLS 이 3가지는 유저 입장에서 불편함을 느낄 수 있는 항목들을 지표화한 것인데요. 전부 사용자가 직접 인지하는 결과 지표입니다. 콘텐츠가 다 보였는가 내 클릭에 반응했는가 레이아웃이 흔들리지 않았는가 같은 것들이지요. UX적인 이유에서도 이 성능지표들은 중요하지만, SEO 순위와 직결되는 이유이기도 하기에 자연유입이 중요한 제 현업 도메인에서는 유독 더 중요합니다. 그럼 이 코어웹바이탈 항목을 개선하기 위해서는 어떻게 해야 될까요? LCP를 구성하는 요소 공식문서를 보면 각 코어 웹바이탈은 여러 요인을 합쳐놓은 것으로 볼 수 있는데, 그 중 LCP는 TTFB + 리소스 로드 딜레이/시간 + 요소 렌더 딜레이를 계산한 것으로 볼 수 있습니다. INP도 여러 요인의 집합체로 공식문서 참고 따라서 LCP를 개선하려면 하면 이에 속한 TTFB를 개선하거나 리소스 로드 시간을 줄이기 위해 JS 청크를 줄여야하는 것이지요. 즉, TTFB가 600ms면 LCP는 절대 600ms보다 빨라질 수 없습니다 그 중에서도 오늘 메인으로 다룰 주제는 TTFB인데요. 작년쯤 이맘때 쯤 현업에서 진행했던 작업에 대해 회고를 해보려고 합니다. TTFB 개선기 1) 주요 페이지 문제 확인 목적 조직에서 비즈니스 기능을 새로 시작하는 시기여서 백엔드 작업이 시작된 와중 프론트쪽 시간이 살짝 비어서 간단히 할 수 있는 기능 개선점을 찾고 있었습니다. 객관적으로 문제점을 찾기 좋은 영역이 네트워크 탭이든 성능검사 탭이든 브라우저 도구를 통한 것이라고 생각했고, 개발자 도구도 살펴보고 페이지 소스 보기도 살펴보던 도중,, 의아한 부분을 발견했는데요 바로 서버로부터 하이드레이션되는 데이터가 3653줄이나 된다는 점이었습니다. 질문상세 페이지가 주요 페이지이긴 하지만 서버로부터 받고 있는 데이터가 페이지 구성에 비해 너무 많다보니 원인분석을 하였고 아래와 같았습니다. 웹서버에서 SSR할 때 필요없는 유저 정보 기반의 데이터까지 API 서버에서 가져오고 있다. 주요 비즈니스 데이터의 요청 수를 줄이기 위해 비효율적으로 많은 데이터 더미를 한번에 가져오고 있다. 이 두 문제는 문서의 크기가 비대해지게 하며 불필요한 API RTT가 발생하는 원인이므로 개선 시 SSR시 문서 응답 시간 즉, TTFB를 상당히 낮출 수 있는 포인트로 보였습니다. 2) 서버측에서는 불필요한 유저 정보 기반의 데이터 이 블로그를 만들 때에도 언급했었던 내용으로, CDN 캐싱을 염두에 두고 있었기에 서버측에서 유저 정보 기반 UI는 렌더링하지 않도록 구성하였었습니다. ex) 답변 평가 여부, 좋아요/싫어요 여부 등 만약 A라는 유저의 정보를 토대로 렌더링된 페이지가 CDN에 캐싱이 되었다가 전혀 다른 B유저에게 서빙이 되면 곤란하기 때문이지요. 그러한 이유로 SSR을 할 때 사용하지 않고 있음에도 API는 유저정보 기반 데이터를 응답하고 있었습니다. ex) downVoted, upVoted 물론 웹서버측에서는 이 API를 요청할 때 헤더에 토큰을 싣지 않고 있기도 하고, CDN 캐싱 기법이 적용되어 있지 않기에 문제가 발생할 위험은 없지만 API 입장에서는 유저 토큰이 없어도 관련 테이블을 조인을 해보거나 예외처리는 하고 있다는 뜻이고 더욱이 가장 자연유입이 많기도 한 질문상세 페이지에 들어올 때 마다 요청되고 있는 API이다보니 이는 개선 포인트로 보였습니다. 즉, 프론트 입장에서는 불필요하게 하이드레이션 되는 데이터를 줄일 수 있는 작업이고, 백엔드도 MSA구조에서 테이블 조인으로 인해 발생하는 RTT를 개선 or 코드 가독성을 개선할 수 있는 작업이라 판단되어 기존 API를 웹서버측에서 유저 토큰 없이 요청할 정적 데이터 기반 API 클라이언트에서 유저 토큰이 있을 때 요청할 동적 데이터 기반 기반 API 로 분리하는 것이 어떠할지 제안을 드려봤습니다. 이 전략은 주로 저희 서비스에 들어오는 유저는 검색엔진을 통한 자연유입이기 때문에 비로그인 유저가 많아 웹서버측에서의 요청만으로 대부분의 트래픽을 처리가능하다는 기존 장점을 살림과 동시에 가장 다이어트된 형태로 데이터를 제공한다는 것입니다. 다만 해당 기술 작업은 당시 비즈니스 업무의 편중으로 작업 공수가 나질 않아 다음을 기약하게 되었습니다. 밀리고 밀렸지만 올해 하반기 있을 질문상세 페이지 리뉴얼이 비즈니스 업무로 예정되어 있어 같이 진행 가능할 것으로 보입니다 ㅎㅎ 3) 필요한 데이터를 비효율적으로 가져오고 있다 서비스에는 토픽이라는 주요 비즈니스 데이터가 존재하는데 이를 질문상세 페이지에서 렌더링하기 위해서 약 250개의 데이터 배열을 API 요청하여 가져온 후 질문에 맞는 토픽을 찾아서 사용하는 비효율이 있었습니다. 예를 들면 제가 질문상세 페이지에서 필요한 것은 형사 토픽 하나인데 [민사, 형사, 가족이혼, 인사 , ...약 240개]를 모두 벌크로 가져와서 find하는 방식인 것이죠. 실제로 하이드레이션 되는 데이터 중 거의 2/3가 이 토픽 배열 데이터였다보니 앞서 다루었던 문제보다 더 큰 비중을 차지하고 있었습니다. 이 벌크 조회 -> 단건 조회로 수정 작업은 2)에 비해서 아주 간단하면서도 타 챕터와의 협업 없이 가능하다는 장점이 있어 바로 작업하였습니다. 운영환경 기준 성과 1) 기존대비 문서크기 6~7% 감소 개선 작업을 통해 하이드레이션 페이로드 라인이 3653 -> 634줄(88% 감소)이 되었고 운영환경에서 실제 등록된 인기 질문을 기준으로 SSR된 문서의 크기가 7KB~10KB 정도 (기존대비 약 6~7%) 감소하였습니다. 또한 자연스럽게 TTFB 시간에 큰 개선이 있었는데요. 네트워크 탭을 통해 클라이언트가 HTML 문서를 받는데에 걸린 시간을 비교해보니 아래와 같았습니다. 2) 기존대비 TTFB 70% 감소 1️⃣ 개선 전 2️⃣ 개선 후 브라우저 캐시를 끄고 여러 번 반복 측정했을 때 (네트워크 노이즈 때문인지) ±20ms 수준의 변동은 있었으나 TTFB가 기존대비 약 70% 감소하는 경향은 일관되게 관찰되었습니다 3) 컨테이너 메모리 사용량 개선 서버측 메모리 점유율에도 10% ~ 15% 정도의 상당한 개선이 있었습니다. 약 240개 가량의 토픽 배열을 매 요청마다 메모리에 올렸다 버렸다하는 현상이 사라졌기 때문으로 보입니다. 앞으로는 어떻게 해야할까? 1) 렌더링 기법 변화 1️⃣ 콘텐츠의 성격에 맞는 렌더링 방식인가 이번 글에서 SSR 하에서 최적화하는 방법에 대해 고민/개선하였지만 사실 지금 SSR 자체가 서비스에 적합한 방식은 아니라고 생각합니다. 질문상세 페이지의 메인 콘텐츠는 내용의 수정 빈도가 높지 않은 텍스트이므로, 특징상 서버에서 동적으로 매 요청마다 새로 처리해야 될 필요가 없기 때문인데요. 이렇게 SSR로 돌아가는 상황에서 아무리 더 최적화한다한들 본래 적합한 정적 렌더링 방식에 비해 구조상 서버 비용 낭비 지금 언급한 TTFB처럼 서버가 요청을 처리하는데에서 필연적인 성능 저하 가 따라올 수 밖에 없습니다. 2️⃣ 어떻게 바꿔 볼 예정인가 지금까지는 서비스 인프라에 너무 큰 변혁을 가져다주고 비즈니스 업무에 밀려 함부로 작업하지 못했으나 올해 하반기 예정된 주요 페이지 리뉴얼을 통해 근간을 아래처럼 바꿔보려고 합니다. 주기적으로 정적 페이지를 생성하는 ISR 답변이 달렸거나 유저나 어드민에서 콘텐츠를 수정했을 때 이 정적 페이지를 갱신하기 위한 On Demand Revalidation 어드민같은 외부 서비스에서 곧바로 서비스에 갱신 요청을 할 수 있도록 Route Handler로 On Demand Revalidation를 트리거하도록 구성 을 조합하는 방식으로 인프라 변경을 구상하고 있습니다. 장점은 역시 정적 페이지가 생성되었다면 런타임에서 렌더링 단계가 제거되어 코어웹바이탈 지표 향상 - UX 개선 및 SEO 순위에 이점 서버 인프라 축소 가능 - 비용 감소 를 꼽을 수 있습니다. 2) CDN 캐싱 정적 렌더링 방식이 성공한다면 추가로 CDN 캐싱을 선택할 수 있습니다. 위 인프라 변혁이 이루어진다면 Cloudfront를 통해 요청 시 거쳐야할 인프라 RTT를 더 줄일 수 있는데요. 즉 Cloudfront는 인프라 설계 레이어의 제일 앞단에 위치하다보니 요청이 들어왔을 때 곧바로 응답할 수 있게 되므로 TTFB를 더 개선할 수 있고 캐시로 인해 오리진 서버(Next 서버)를 거치는 횟수가 꽤 줄어들테니 데이터 트랜스퍼 비용 절감 효과도 기대해볼 수 있습니다. 오리진 서버에서 정적 서빙을 한다면 CDN까지 붙이는 게 너무 투머치가 아닐까 싶지만 서버의 리전이 서울에 있는 상태이고, 저희 서비스가 글로벌 트래픽을 완전히 무시할 수는 없는 수치인 듯하여 고려해야 된다고 판단했습니다. 정리 이 작업이 저에게 의미가 깊었던 것은 프론트엔드 사이드에서 잘 언급되지 않는 부분을 개선하여 최적화를 성공했기 때문이었습니다. 개인적으로 느꼈던 프론트 성능 최적화 글은 대부분 번들 사이즈, 이미지 최적화, 코드 스플리팅에 집중하는 것을 볼 수 있습니다. 문서크기와 그 안의 하이드레이션 페이로드는 시야 밖에 있는 경우가 많은 것이지요. 그 부분을 찾아 LCP 하한선이라는 코어웹바이탈 개선의 작업이 될 수 있었다는게 뿌듯했네요 ㅎㅎ

  • AI에 대한 가치관 변화
    AI
    1달 전

    AI에 대한 가치관 변화

    대 AI의 시대, 저 역시 클로드 코드 유저로서 PR리뷰 자동화 SKILL과 PR생성 자동화 SKILL등을 만들어 챕터에 공유하는 등 최근에는 주로 번거로우면서도 반복되는 업무를 자동화하기 위해 활용하고 있습니다. 그러다보니 2년전까지만 해도 AI에 유독 박했던 제가 여기까지 온게 인상깊기도 하여,, 제 가치관의 변동을 회고해보려 합니다. 저에게 있어 AI의 인식에 대해 큰 분수령이 두차례 존재했는데요. 2024년 말과 2025년 말 즈음으로 되돌아가보면 ~ 2024년 말 당시에는 AI 코딩 에이전트는 존재하지 않았던건지 개발시장에서 주류는 아니었고 LLM 위주로 사용하였는데, 그 중에서도 ChatGPT가 가장 메이저였습니다. 공식문서 링크를 걸어주고 단순한 설명을 시키는 용도로 사용하는 것은 무난했습니다. 예를 들어 tanstack query에서 캐시를 초기화할 수 있는 방법은 뭐야? 라고 질문을 하면 setQueryData, invalidateQueries 등이 있다면서 용도별로 잘 답변해주었지만 막상 실제로 개발을 하면서 닥치는 시나리오 기반으로 설명한다고 했을 때에는 맥락을 잘 이해하지 못하는 듯한 뉘앙스에 답변이 대부분이었어요. 예를 들면 tanstack query를 사용하여 피드 리스트를 인피니티 스크롤로 구성하고 있어. 피드 중 한 아이템에 좋아요를 누르면 관련 데이터를 캐시를 업데이트해야 되는데 리스트 전체를 새로 다시 요청하는 것은 비효율적일 것 같아. 좋아요를 누른 아이템만 업데이트하는 방법이 없을까? 라는 시나리오를 던지면 제가 원했던 한 아이템만을 갱신할 수 있는 setQueryData가 아닌 리스트 전체를 새로 요청하는 invalidateQueries를 답변하는 경우가 있었습니다. 그 외에도 제가 어떤 상황 기반의 질문이나 가설을 제시했을 때 잘못된 답변을 교묘하게 그럴듯한 근거를 들어 정답인 척하며, 의아한 부분이 있어 반론은 제시하면 그제서야 너 핵심을 찔렀어같은 식의 피드백으로 화를 돋구는 사용감이 대부분이었습니다... 위같은 상황을 겪었던 당시를 회상해보면 AI가 아직 사용할 수준은 아니구나/신뢰할 수 없구나라고 느낀 점을 3가지로 추려볼 수 있을 것 같아요. AI가 같이 컨펌해주었다는 안도감에 저도 모르게 맞다는 전제하에 진행할 수 있는 위험 제 말을 객관적으로 평가하지 않고 그저 어떻게든 맞다고 증명해주려고 애쓰는 듯한 느낌 너 핵심을 찔렀다고 하면서 말하는 답변조차 잘 포장된 거짓일 수 있겠다. 즉 제 답을 객관적으로 볼 수 있는 능력도 없고, 저 또한 이에 속아 큰 스노우볼이 굴러가 시간낭비, 더욱 심하면 서비스에 잘못된 코드가 심어질 수도 있겠다 싶었습니다. 따라서 저는 2024년 말까지는 AI가 창작하는 것을 배제하기 위해 공식문서같은 객관적인 답안지를 링크로 줄 수 있을 때 위주로만 사용하였습니다. ~ 2025년 말 제 기억상으론 새로운 AI 모델이 속속이 등장하던 시기였습니다. 이 시기쯤 유명 기업 채용공고에서는 AI 활용을 잘하는 분을 우대한다라는 말이 보이기도 시작하는 등 업계가 한층 더 AI를 중요하게 바라보고 있구나라는 생각이 들더군요. 공식문서 기반의 잦은 문답으로 신뢰를 회복해서 그런건지, 모델들의 퀄리티가 산업 전반적으로 향상되어서 그런건지 예전보다 타율이 꽤 좋아진 듯한 느낌을 받고 있었던 때입니다. 이 때쯤에는 주요 로직을 짤 때 제가 하면 복잡하지만 AI가 하면 잘할 것 같은 업무들을 LLM에게 맡겨보곤 했습니다. 공식문서 기반인 것은 여전하지만 그것을 기반으로 창작해야 하는 등 조금의 생각이 더 필요한 작업으로요. 다만 컨벤션을 준수하는 등 기존 코드베이스를 이해해야 되거나 개발 도메인을 설명할 필요없는 작업들 위주의 작업을 주로 시켰습니다. 예를 들면 캘린더 로직이 있었습니다. MDN의 Date 객체의 링크를 주고 타겟이 된 달에 며칠이 있는지 세어서 배열에 날짜를 담아두는 로직을 짜달라고 요청했는데 꽤나 좋은 퀄리티로 짜줬던 경험이 있었습니다. ~ 지금까지 1) AI 에이전트 등장 2025년 중후반쯤 됐을 새로운 종류의 AI 도구가 등장했는데요. LLM과 달리 내 컴퓨터에서 직접 돌아가는 AI 에이전트라는 신문물이었습니다. LLM을 사용하여 개발을 할 때 위에서 말했듯 기존 코드베이스나 개발 도메인을 이해하지 않아도 되는 부분에 한정해서만 사용했던 이유는 이런 규칙들은 기본적으로 양이 매우 많아 컨텍스트가 알맹이보다 서론이 더 길어 답변이 멍청해지는 할루시네이션 현상 심지어 매 세션마다 일일이 설명해야 됨 같은 문제로 인해 효율이 안나와서였는데 AI 에이전트는 프로젝트의 코드베이스를 읽을 수 있으므로 컨벤션 및 개발 도메인을 이해하고 있다는 전제하에 소통이 이루어진다는 것이 엄청 큰 메리트로 다가오더군요. 물론 메모리 파일로 가이드를 잡아주기도 해야하고 설명해야 하는 내용이 길수록 토큰 소비량이 많아 발생하는 비용문제 등 새로운 관점의 문제가 발생했지만 ㅎㅎ 장족의 발전이지요. 프롬프트를 짤 때 알맹이에만 집중할 수 있게 되었고, 할루시네이션도 많이 개선되었습니다 2) 자동화 방안 AI 에이전트를 활용한 작업으로 가장 보람을 느꼈던 것은 제가 직접하면 시간을 많이 잡아먹고 수고가 많이 들며 반복되는 작업의 자동화였습니다. 예를 들면 PR 올리기, 팀원 PR 리뷰하기가 있지요. 제가 한 작업에 대해서 일일이 PR 내용을 작성하는 것이 꽤 번거로웠습니다. 어떠한 기획의 일환으로 작업한건지, 주요 파일은 무엇인지, 예외 처리는 어떠한 이유에서 이루어졌는지 등등 팀원을 이해시키기 위한 작문의 양 자체가 길기도 하고 맥락을 고려해서 작성해야 하는데에 시간을 너무 많이 잡아먹는 문제가 있었고, 반대로 제가 팀원의 작업을 리뷰할 때 내용이 잘 적혀있지 않으면 맥락을 이해해야 하는데에 수고가 많이 들었습니다 따라서 AI를 통해 자동화하면 효율이 많이 좋아지겠다 싶은 생각이 들었습니다 리뷰나 템플릿에 따른 내용 생성처럼 일정한 기준으로 이루어져야하는 작업이야말로 인간보다 AI가 더 잘 할 수 있는 영역이니까요. 팀 컨벤션으로 두기도 좋은 내용이다보니 SKILL(클로드 코드 기준)로서 규칙만 잘 정의하여 하네스만 잘채워두면 팀원 모두가 행복할 수 있겠구나싶었습니다. 마침 또 당시 코드래빗 툴을 도입할지 말지에 대한 논의가 있었는데 비싼 리뷰 툴을 사용하는 것보다 경제적이면서도 니즈에 충족되는 방향이었습니다. 팀원들이 각자 다른 AI 에이전트를 쓰다보니 관점이 다양하다는 장점도 있었습니다. 실제로 PR 스킬 사용 2달차인 지금, 누락된 예외처리 부분에 대해 언급을 남기거나 문제 발생이 우려되는 부분을 짚어주는 부분에서 PR 리뷰가 운영에 미치는 영향력이 상승한 것을 볼 수 있었습니다. 월간 웹 버그 리포트 리뷰 자동화 도입 전인 2026년 1월: 7건 리뷰 자동화 도입 후인 2026년 4월: 2건 배포 횟수 차이 등 다른 요인도 작용했겠지만 제가 인지하지 못했던 nullable 케이스나 코드만 봤을 때에는 예상할 수 없었던 race condition으로 인한 버그를 사전에 짚어주는 등 분명 영향이 있었어요. 예를 들면 리스트 필터기능을 팝업으로 구현했을 때, 팝업의 노출 여부도 히스토리로 관리되고 있다보니 필터 적용 시 query string 변경과 팝업 닫힘 사이에 race condition이 발생할 수 있다는 점을 지적해준 적이 있었습니다. 앞으로 개발자는 어떻게 될까요? 최근 비즈니스 업무에서도 AI 에이전트를 적극 활용하면서 느낀 점은 앞으로 개발자에게도 기획자로서의 역량이 요구될 것 같다는 점이었습니다. 어떤 기능에 대한 TF가 시작된다면 기획자분으로부터 전체적인 기획서를 받게 될텐데요. 받은 기획을 한번에 AI에게 그대로 맡긴다면 컨텍스트가 길어져서 할루시네이션 현상이 발생한다던가 기획서상 세부 정책이 누락된 것을 그대로 구현할 위험이 있습니다. 따라서 저는 큰 기획을 적절한 기능 단위로 나눠서 그 각각을 AI에게 컨텍스트로 제공하기 위해 한번 정제합니다. 예를 들어 유저간 서비스 내 재화를 주고받을 수 있는 응원박스라고 하는 기능에 대해 기획서를 받으면 응원박스를 유저간 주고 받을 수 있는 방법 응원박스 전송이 노출되는 페이지 응원박스를 보낼 수 있는 유저 조건 응원박스와 다른 기능 간 플로우 POST API 엔드포인트 전송 에러 시 UI 표출 방법 전송 간 애니메이션 노출 방법 및 이미지 에셋 경로 내가 받은 응원박스는 어디서 보는지 받은 응원박스가 노출되는 페이지 어떠한 형태로 노출할 것인지 - 리스트? 캘린더? GET API 엔드포인트 조회 시 UI 표출 방법 등등 에이전트가 작업을 수월하게 할 수 있도록 나누고 세부 정책을 작성합니다. 이 과정에서 기획서에 누락되어 있던 정책(ex 정지 유저는 응원박스를 보낼 수 있는가?)을 발견할 수도 있지요. 이렇게 시나리오나 플로우, 기능에 대한 요구사항을 잘 정리하는 것이 AI 에이전트 코딩 시대에서는 부각되는 능력인 것 같습니다. 코드를 짜던 사람에서 코드를 어떻게 짜야하는지 설명하는 사람으로 메타가 변하고 있기 때문이지요. 여러분들은 어떻게 생각하시나요?

  • 피드 성능 개선기
    성능
    1달 전

    피드 성능 개선기

    1년반전 즈음 더욱더 주니어였을 시기에 했던 서비스의 피드 성능 개선기를 회고해보려 합니다. 피드 리스트 성능 이슈 당시 기준 몇달 전 서비스 대규모 리뉴얼이 있었고 직후 피드에 심각한 성능 문제가 발생했습니다. 어느 페이지 구간부터는 로드에 짧게는 3초, 길게는 7초 등 적지 않은 시간이 걸렸고 슬랙에서 공론화되기까지 했습니다. 리뉴얼 중 피드를 담당했던 인력이 한달 전 퇴사를 하면서 대응에 공백이 생겼었는데 다행히도? 당시 브라우저 성능 최적화를 위해서 해보고 싶었던 시도가 있었던지라 업무 카드를 겟했습니다. 성능에 얼마나 하자가 있는지 실제 지표로 보려고 성능 모니터 탭을 켜보니 아니 웬걸.. 고작 7번째 페이지를 불러왔음에도 브라우저가 사용하는 CPU가 100%까지 치솟는 것으로 보아 렌더링을 하는데에 너무나 많은 연산이 이루어지는 것으로 예상되었습니다. 원인 찾기 인피니티 스크롤이 수차례 되었을 때 문제가 부각되는 것으로 보아 두 가지를 예상했습니다 1) 백엔드 API의 레이턴시🤔 고려해볼 법한 원인으로 오프셋 페이지네이션 체계에서 조회하려는 데이터의 앞 데이터를 SKIP하는 과정에서 느려지지 않을까 생각도 해보았는데요. 하지만 그렇다고 하기엔 고작 10번도 안되는 스크롤부터 바로 문제가 발생했었고, 순서상 앞에 있는 데이터가 최대 100개 밖에 안되는데 이렇게 느릴리는 없다고 판단하여 제외하였습니다. 네트워크창에서 봐도 실제로 문제가 될만큼 레이턴시가 발생하진 않았기도 하구요. 2) 과다 재연산 이슈✅ 새로운 리스트 데이터를 받아오면서 리스트 상태가 갱신되고, 이로 인해 부모인 리스트 컴포넌트가 리렌더링되면서 자식인 아이템 컴포넌트가 전부 리렌더링되는 것이 문제였습니다. 다만 흔한 프론트엔드 문제 패턴으로 이런 형태의 결점이 있다고 해서 이렇게까지 심각하게 버벅이는 경우는 잘없었는데 각 피드 아이템이 꽤나 많은 상태 관리 로직과 무거운 로직의 함수를 갖고 있던 것이 원흉이었습니다. 즉 아이템 컴포넌트에는 재연산 시 부담이 있는 좋아요, 싫어요 토글함수나 답변 보상 배율 계산같은 무거운 로직이 있어서 리렌더링 1회당 비용도 비싼 편이었고, 또한 아이템 컴포넌트에는 좋아요, 싫어요, 토픽, 유저정보 총 4개의 지역 상태가 존재했는데 부모에서 내려주는 이 값을 자식에서 상태로 관리하고 있었습니다. 따라서 데이터의 정합성을 맞추기 위해 자식에서 useEffect로 이 상태들을 업데이트하는 로직이 있었고 이로 인해 자식당 2번의 리렌더링이 발생하고 있었습니다. derived state 패턴(= 파생 상태)이라고 불리며 리액트는 이를 지양할 것을 강조합니다. 그리고 인피니티 스크롤이 트리거되어 부모의 리스트 상태가 갱신되면 리렌더링되면서 자식인 아이템 컴포넌트들이 모두 영향을 받는데 이 때 새로 불러올 아이템 외에 기존에 있었던 아이템들도 리렌더링이 발생하게 되는 것도 성능 악화에 큰 지분을 차지한다고 생각했습니다. 기존에 있었던 것들은 UI가 그대로 유지되어 리렌더링이 불필요한데도 부모로 인해 영향을 받게 되는 것이죠 즉, 인피니티 스크롤로 6번째 리스트를 불러온다고 예시를 들었을 때 기존 1~50번째 아이템 컴포넌트 새로 불러온 51~60번째 아아템 컴포넌트 자식에서 상태 정합성을 위해 사용한 useEffect 로 인해 총 (50 + 10) * 2 = 120번의 리렌더링이 발생할 수 있게 됩니다. 차차 문제를 해결했던 스토리를 풀어보면 리액트 derived 패턴 제거 부모에서 상태 갱신으로 자식이 리렌더링되는 것은 자연스러운 흐름이므로 가장 최후의 개선 요소라고 판단하여 자식 자체에서 발생하는 리렌더링 원인을 제거하기로 하였습니다. 좋아요, 싫어요, 유저정보, 토픽 총 4개의 값 모두 부모 리스트에서 뿌려준 데이터로 아이템에서 각각 useState 와 useEffect로 관리중이었으나 그렇게 관리할 필요가 없었습니다. 유저정보나 토픽은 런타임에서 AJAX 처리될 필요 없는 정보이므로 상태로 다룰 필요가 전혀 없었고 좋아요나 싫어요는 토글 기능이 필요하므로 상태로 컨트롤 되긴해야 하나 리스트 데이터가 이미 tanstack-query에 의해 캐시(상태)처리 중이므로 이 캐시를 조작하면 되어 useState와 useEffect를 사용할 필요가 없어져 제거했습니다. 그 결과 아이템당 리렌더링 횟수가 2회에서 1회로 줄어 6번째 리스트를 불러온다고 하면 총 리렌더링 횟수를 벌써 절반인 60회로 줄였습니다. 동시에 코드 가독성도 굉장히 개선됐구요. 리스트를 새로 받아오면서 발생하는 리렌더링 현상 제거 1) memo로 컴포넌트 메모이징 적용 6번째 리스트를 불러올 때의 리렌더링 횟수를 120번 -> 60번으로 줄였지만 여전히 아쉬운 점이 있죠. 새로 불러올 데이터는 51~60번째 컴포넌트에 해당하는 데이터이지만 기존의 1~50번째 컴포넌트는 전혀 달라진 사항이 없음에도 부모 컴포넌트의 리스트 상태값이 갱신되었다는 이유로 다시 리렌더링 되어야 합니다. 따라서 기존 컴포넌트의 데이터가 변하지 않았다면 리렌더링하지 않도록 memo를 적용합니다. memo는 부모가 리렌더링되었더라도 그에 속하는 자식 컴포넌트의 prop값이 변하지 않았다면 리렌더링 없이 이전 값을 그대로 사용할 수 있는 기법으로 특히나 이러한 리스트성 UI에서 큰 빛을 발휘합니다. 2) 리스트 데이터를 메모이징할 때 흔한 함정 memo를 적용해보면? 예상대로 동작하지 않습니다. 분명 부모가 리렌더링되기 이전과 똑같은 prop값을 받았을텐데 리액트는 다른 값을 prop으로 받았다고 인식한 것이죠. 왜일까요? 이 현상을 이해하려면 자바스크립트의 원시타입과 참조타입에 대해서 이해를 해야합니다. 라는 원시타입의 값을 할당한 변수가 있을 때 a === b 는 true입니다. foo === foo를 비교하는 것이므로 값이 같기 때문이죠. 그럼 아래 코드는 어떨까요? 라는 코드가 있을 때 a === b 는 false입니다. 겉으로는 값이 같아보이지만 내부적으로는 변수 a, b가 각자 가진 객체값은 별도의 메모리에 저장되고, 각 변수는 메모리 주소를 참조하게 되기 때문이죠 즉 a === b는 내부적으로 메모리주소A === 메모리주소B를 비교하는게 되어 false가 나오게 되는 것입니다. 이러한 것이 참조타입의 특징으로 자바스크립트에서는 객체, 배열, 함수가 참조타입으로서 취급됩니다. 따라서 부모의 리스트 상태값이 바뀌어 리렌더링되면 자식이 prop으로 내려받고 있던 함수도 재연산되어 메모리주소가 이전과 달라지므로 리액트는 이미 존재하던 리스트 아이템들이 이전과 다른 prop 값을 받았다고 판단하게 되어 리렌더링을 해버리게 되는 것입니다. 그럼 어떻게 해야되는 걸까요? 이런 참조타입의 데이터 자체를 메모이징해서 내려줘야 합니다. 즉, 부모의 상태가 갱신되어 리렌더링되어도 동일한 레벨에 존재하는 함수의 재연산을 막는 방법인 것이죠. useCallback을 이용해서 의존성 배열에 넣은 값에 변화가 생겼을 때에만 함수가 재연산되도록 하면, 이제 부모 리스트 상태값이 바뀌더라도 함수는 로직에 영향을 미치는 값이 변하지 않는 이상 재연산되지 않아 이전 메모리주소 값을 유지할 수 있게 되어 정상적으로 memo가 동작하게 됩니다. 이제 120번에 달했던 렌더링 횟수가 딱 새로 불러온 개수만큼인 10번으로 줄일 수 있게 되었습니다. 마무리 1) 브라우저 성능 최적화 확인 보이듯 약 15페이지까지 계속 스크롤을 내렸을 때 크롬 성능 모니터 탭 기준으로 As Is CPU 최고 100%, 메모리 최고 220MB To Be CPU 최고 20%, 메모리 최고 80MB 정도로 상당히 개선이 되었습니다. 브라우저 이외 웹뷰 환경에서도 유의미하게 줄어든 것을 볼 수 있었습니다. 웹뷰의 경우 자원을 과하게 사용하게 되면 앱이 크래시되거나 하얀 화면이 되어버리는 문제가 있었는데 운영환경 배포 후에 관련 CS가 줄어드는 결과도 얻을 수 있었습니다. 2) 메모이징이 능사는 아니다 메모이징은 참 좋아보입니다. 하지만 그렇다고 남용은 있어선 안돼요. 왜냐하면 연산한 값을 '메모리'에 저장해둔 후 리렌더링/재연산이 될 때 추가 연산없이 메모리에서 꺼내 재활용하는 방식이기에 결국 메모리 자원을 잡아먹으며 매 렌더마다 이전과 비교하기 위해 CPU도 사용하게 됩니다. 지금처럼 리스트 형태인 경우 대개 데이터가 몇 백개씩 렌더링에 관여될 때 관련 값들의 재연산으로 인해 CPU 부하가 심해질 수 있을 때에 도움이 되는 것이죠. 즉 대부분의 경우에서는 재연산을 하는 것이 성능적으로 우월할 때가 대다수입니다. 따라서 메모이징이 도움이 되는 경우는 예외적인 케이스라는 것입니다. 그러므로 정말 무거운 작업임이 확실한 상황에서만 도입을 고려해야 합니다.

  • 모바일에서의 모달 닫기 UX에 대해
    컴포넌트
    2달 전

    모바일에서의 모달 닫기 UX에 대해

    UI를 개발하면 항상 마주하는 녀석이 있습니다. 모달과 팝업 화면의 레이어를 무시하면서 나타나는 이 UI들은 디자인적 요소 외에도 UX적으로 고려해야 할 아주 중요한 요소가 있습니다. 바로 어떻게 닫을 것인가에 대한 처리입니다. 모달을 닫는 방법 모달 및 팝업(이하 모달로 통일하여 칭합니다)의 닫기 처리에 대해서 안드로이드/iOS 유저별로 대중적으로 학습되어 있는 상태가 다르기 때문인데요. 그 전에 우선 모바일에서 모달을 닫는 방법을 보면 흔히 아래 3개 정도가 있습니다. DIM(반투명 배경) 클릭 X버튼 클릭 OS에서 제공하는 뒤로가기 기능 사용 안드로이드: 하드백키 iOS: 스와이프백 1과 2는 애플리케이션에서 제공하는 기능이기에 OS환경이 다르더라도 웹뷰에서 동일한 로직으로 처리 가능하여 크게 신경쓸 필요는 없지만 문제가 되는 것이 3입니다. OS별 '뒤로가기' 기능에 대해 관점이 다르기 때문인데요. OS별 뒤로가기 기능에 대한 인식 차이 뒤로가기에 해당하는 기능인 안드로이드의 하드백키와 iOS의 스와이프백이 대중적으로 녹아있는 모습을 보면 안드로이드 유저는 하드백키를 누르면 모달은 닫고 페이지는 유지하는 것이 학습되어 있고, iOS 유저는 스와이프백을 하면 모달도 닫고 페이지까지 같이 뒤로가는 것이 학습되어 있습니다. 💡 물론 네이티브단에서 모달이 떠 있을 때에는 스와이프백 동작 자체를 허락하지 않게 하는 케이스도 있으나, 여기서는 순전히 웹뷰의 입장에서만 고려합니다. 즉, 우리는 웹뷰에서 모달을 구현할 때 유저의 경험을 존중하여 두 OS를 분기하여 로직을 작성해주어야 할 필요가 있습니다. 안드로이드는 하드백키 클릭 시 모달만 닫아야 하므로 모달을 상태로 관리하되, 브라우저 히스토리 스택도 추가적으로 쌓기 iOS는 스와이프백 시 모달을 닫으면서 페이지까지 같이 뒤로 가는 것으로 동작해야 하므로 모달을 상태로 관리 모달을 띄울 때 ‘상태’로 처리하는 것은 iOS/안드로이드 모두 똑같지만 안드로이드의 경우에는 히스토리를 하나 쌓아두어야 하는 셈이죠. 안드로이드에서의 추가 처리 따라서 이 로직을 공통적으로 처리하기 위해서 전역상태를 통해 모달 리스트를 관리하되, 안드로이드인 경우에만 pushState를 통해 보이지 않는 히스토리를 하나 추가해줍니다. 그리고 브라우저 뒤로가기 버튼이나 history api의 back으로 인해 pushState되었던 히스토리가 스택에서 제거되고 동시에 이로 인해 트리거될 popstate 이벤트를 두어 전역상태 모달 리스트에서 가장 마지막에 떠있는 모달을 제거하는 방식으로 로직을 구성합니다.
 마무리 이미 구현해본 사람들에게 크게 어려운 내용은 아닐 수 있으나, 웹뷰를 다뤄본 경험이 없는 프론트엔드 엔지니어라면 놓치고 지나갈 수 있는 부분인데요. 설계 초반에 반드시 공통 로직화하여 잡고가야 프로젝트가 진전되었을 때 피곤하지 않은 UX 기능이 되므로 꼭 참고하시길 바랍니다.
 저또한 현업에서 최근 모달UX 개선에 대한 필요성을 느껴 위 로직을 적용 시도하고 있는데 파일체인지가 상당하여 검증에 신중함을 기하고 있습니다. 최초에 설계를 잘해두었으면 어땠을까? 하는 생각이 들곤하네요 ㅎㅎ 이만 마무리하겠습니다.

  • SEO 순위에 대해 아시나요?
    SEO
    2026.02.19

    SEO 순위에 대해 아시나요?

    SEO는 왜 중요할까 내 사이트가 검색엔진에 잘 노출되기 위해서 라고 간단히 얘기할 수도 있습니다. 다만 현업에서 SEO 업무를 진행하면서 느낀 2가지 관점에서의 장점을 얘기해보자면 1) 기술적 관점 구글이나 네이버같은 SEO 프로바이더들은 막강한 영향력을 가지고 있습니다. 이 프로바이더들은 본인들의 검색엔진에서 내 사이트의 페이지가 SERP에 더 잘 노출될 수 있는 기술적인 가이드를 제공해주고 우리는 페이지를 구성할 때 이 내용을 표준으로서 참고하여 만드는 것이 권장됩니다. 기술적 영향력이 강한 프로바이더들이 제공하는 가이드이므로 신뢰도가 높기 때문에 시키는 것만 잘해도 내 웹사이트가 성능 좋게 잘 만들어질 수 있는 틀을 갖출 수 있게 됩니다. 예로 SEO를 위한 작업 중 흔히들 아는 서버측 렌더링(SSR)/정적 렌더링(SSG, ISR)을 이용한다던가, TTFB 개선을 위해 CDN을 이용한다던가 모두 가이드에 나와있는 내용인데요. 크롤러 봇은 유저입장에서 사이트를 평가하기에 SEO를 위한 것 = 유저를 위한 것이 된다고 볼 수 있습니다. 2) 마케팅적 관점 다른 마케팅 방법과 비교했을 때 돈들이지 않으면서 불특정 다수에게 서비스를 알릴 수 있는 가장 확실한 마케팅입니다. 구글, 네이버같은 사람들이 가장 자주 방문하는 서비스에서 제 사이트를 비용없이 노출할 수 있게 되는데 회사에서 보았지만.. 마케팅에 쓰이는 비용은 정말 장난이 아니었습니다... 지하철 광고 집행비, 강남 한복판 옥외 전광판 집행비, 광고 모델 섭외비 등 마케팅팀에 슬쩍 들은 비용만 해도 억소리나는 금액이었거든요. 돈을 그렇게 투자하더라도 성공이 보장되지 않는 시대이다보니 비용은 낮추고 접근성은 올릴 수 있는 SEO는 적어도 지금 제 현업 도메인에서는 최고의 마케팅이라고 느끼고 있습니다. SEO 순위를 높이는 방법 크롤링 색인 순위 로 나누어 볼 때 검색엔진에 노출 가능 조건인 색인까지는 어느 사이트든 대부분 문제없이 이루어집니다. 즉 '노출' 하는 것 자체만으로는 경쟁력이 크지 않으므로, 내 페이지가 그들보다 상단에 위치하도록 '순위'를 높이고 또 높은 순위를 바탕으로 내 사이트에 유입될 수 있게 CTR 지표를 개선하는 것이 SEO의 최종적인 목표라고 볼 수 있는데요. 구글링을 해봤을 때 흔히 나오는 대표적인 SEO 작업들 몇가지를 아래에 나열해보면 순위에 직접적으로 관여되는 것은 그리 많지 않습니다. 사이트맵 JSONLD 메타데이터 유저 친화적 URL 구성 몇 레퍼런스들은 이러한 작업들을 검색엔진 최적화 작업이라며 한번에 묶어서 얘기하고, 필수 작업인 것처럼 얘기하곤 합니다. 순위적인 관점에서 이걸 한다고 해서 구글에서 더 좋은 점수를 주거나 하지 않고, 노출적인 관점에서도 이 것들이 필수 요소는 아닙니다. 다만 대부분의 항목이 유저입장에서 SERP에 나온 결과물 중 내 페이지를 클릭할 확률을 높여주는 요소들이므로 (필수라기보다는) 반강제되는 옵션이라고 봐야합니다. sitemap을 예로 들어봅시다. 새로 만들어진 페이지를 크롤러에게 알려주는 행위가 sitemap인 것인데 만약 크롤러가 내 사이트의 새로운 페이지를 잘 찾아갈 환경이 링크 등으로 잘 구성되어 있다면? sitemap이 없더라도 크롤러는 찾아올 수 있습니다. 물론 내 사이트가 유명하지 않아서 크롤러가 접근할 진입점이 많지 않다던가 하는 이유로 페이지가 존재한다는 것을 선제적으로 알려주는 것이 좋은 옵션이라서 sitemap 작업을 할 뿐이죠. 즉 순위와는 연관이 없는 것을 언급하지 않고 SEO 작업이라고 묶어서 얘기한 것 + 노출과 관련해서도 필수인 작업은 아니므로 반은 잘못되었다고 생각하고, 그럼에도 해야하는 건 맞는 작업이라는 데에서 반은 맞다고 생각합니다. 그럼 이제 잘 알려진 작업들 중 실제로 순위와 연관이 있는 작업을 구분해보려고 합니다. 구글이 말하는 SERP 순위와 연관있는 최적화 기법 ✅ 1) 페이지 렌더링 방식 메이저 검색엔진들은 크롤러 봇이 페이지를 잘 인식하게 하기 위해서 웬만하면 서버측 렌더링, 정적 렌더링을 이용하라는 스탠스이지만 이러한 렌더링 방식 자체가 SEO 순위와 직접적으로 연결되어 있다는 언급은 없습니다. 다만 아래 3)에서 언급할 코어 웹바이탈 지표는 확실한 순위 반영 요소인데, 이 중 LCP나 INP가 이러한 렌더링 방식과 직결되는 지표이므로 '사실상' 직접적으로 기여된다고 판단하여 추가했습니다. 3)에서 묶어서 언급하지 않은 이유는 이 페이지 렌더링 방식 하나만으로도 하나의 주제가 나온다고 생각했기 때문인데요. 저는 보통 아래의 기준으로 페이지의 렌더링 방식을 결정합니다. 콘텐츠가 텍스트 위주라고 하더라도 질문/답변식으로 답변이 수시로 달리는 등 페이지 콘텐츠가 자주 변경되는지 일방적인 정보 제공인 블로그나 기사처럼 페이지 콘텐츠가 변경될 확률이 현저히 낮은지 등 콘텐츠의 성질에 따라 페이지 렌더링 방식을 선택합니다. 2) EEAT와 링크 링크가 과거보다는 엄청난 순위 결정 요소는 아니라는 언급이 보이긴 하지만 여전히 공식문서에서 언급되어 있는바 관련 논문에서 다른 많은 사이트에서 내 페이지가 링크되거나, 다른 유명한 사이트에서 내 페이지의 링크가 걸리게 되는 것은 SEO에서의 이점이라고 언급합니다. 콘텐츠의 질을 높이는 것도 순위 지정 요소이며, 내 페이지가 외부 사이트에 링크로 걸리는 것도 순위 지정 요소이기에 EEAT의 일환으로 내 페이지 콘텐츠 질을 높여 다른 거물급 사이트에서 내 페이지가 레퍼런스로서 언급되게 하는 것은 SEO상 큰 이점이 됩니다. 그리고 문서에서 내부링크냐 외부링크냐를 구분하지 않은 것으로 보아 제가 컨트롤할 수 없는 외부링크는 별론으로 하고 제 사이트의 페이지간 연결하는 내부링크도 적절히 활용하는 것이 좋아보이네요. 외부링크: 백링크와 동일어로 다른 사이트에서 내 페이지를 링크 내부링크: 내 사이트의 페이지에서 또 다른 내 페이지를 링크 💡 EEAT: 경험, 전문성, 권위, 신뢰성 3) 코어 웹바이탈 코어 웹바이탈로는 LCP, INP, CLS라는 3개의 주요 항목과 또 이 것들과 엮인 FCP, TTFB, TTI 같은 지표도 존재합니다. 위에서 SEO를 위한 것 = 유저를 위한 것라고 했는데요. 이 코어 웹바이탈을 위한 작업이 유저 경험을 개선할 수 있는 작업과 일치하기 때문입니다. 1)에서 언급했던 적절한 페이지 렌더링 방식 사용, CDN 캐싱, 레이지 로딩 등 웹 개발에 있어서의 많은 개념이 여기에 포함됩니다. 세부적인 작업에 대한 글은 아니니 개선 관련 공식문서만 남기고 넘어가겠습니다. 4) 시멘틱 태그와 title 모든 태그에 대해 시멘틱하게 작성해야 한다는 내용은 없지만 적어도 heading 태그에 대한 언급은 존재합니다. 문서의 아이덴티티를 나타낼 수 있는 정보인 title 태그와 heading 태그는 SERP 순위와 연관된 직접적인 요소로 작용하는 것으로 볼 수 있습니다. 구글이 말하는 SERP 순위와 연관없는 최적화 기법 🤔 1) sitemap 사이트맵은 위에서도 말했듯 검색엔진 크롤러에게 내 새로운 페이지의 링크를 알려주어 크롤링을 하라는 용도로 제공합니다. 이 작업을 했다고 해서 검색엔진에 색인까지 일사천리로 되어 SERP에 떡하니 노출되고 그런 것은 절대 아니고 '이런 페이지가 있으니 와서 얼른 크롤링해주세요' 느낌으로 내 페이지에 대해 진입점만 제공할 뿐입니다. 즉, 선제적으로 검색엔진에 내 페이지를 알려서 더 빨리 색인이 될 수 있게 돕는다는데에서 간접적인 이점이 있는 작업입니다. 페이지가 실제로 색인이 된다던가, 높은 순위를 받는다던가 하는 것은 별개의 이야기란 뜻입니다. 아직 규모가 작은 사이트에서는 검색엔진 콘솔에서 일일이 URL을 제공하여 요청을 하는 등으로 커버가 가능하지만 기업급의 규모이거나 UGC 사이트인 경우에는 어드민이 일일이 할 수 없으므로 사이트맵을 제공함으로서 보충하게 됩니다. 예시로 제 현업 도메인은 UGC 성격이라 하루에 몇천, 몇만개의 페이지가 생성이 됩니다. 사이트맵에 동적으로 이 새로운 콘텐츠의 링크를 추가하기 위해서 DB에 해당 데이터가 저장된 테이블의 로우를 전부 액세스하는 상당한 부하가 걸리는 작업이 진행됩니다. 실제 서비스되는 웹서버에서 이 작업을 하는 것은 서버가 터질 수 있으니 적절치 않고 그렇다고 별도의 서버를 띄우자니 비용적인 단점이 생기므로 AWS 람다를 통해 수행하고, 람다로 실행시킬 코드는 step function으로 event bridge에서 지정한 스케줄러로 새벽 3시반에 실행시키도록 하고 있습니다. 아직 여러 사정으로 테이블의 모든 로우를 조회하고 있지만 만약 비슷한 상황에 처한 분이 계시다면 콘텐츠가 오늘 날짜 이후로 update된 콘텐츠만 가져오는 식으로 DB 액세스를 하는 것을 권장드립니다.. 2) 리치스니펫(JSON-LD) 리치스니펫은 검색엔진에서 내 콘텐츠가 풍성(?)하게 보일 수 있게 합니다. 콘텐츠의 성격마다 적합한 리치스니펫이 있는데 질문/답변 위주의 글을 예시로 들자면 이렇게 답변을 부각한다던가 일반 검색결과와 다른 느낌을 제공합니다. 똑같은 주제의 글이 위 캡쳐처럼 SERP에 노출된다고 했을 때 유저입장에서 어느 글을 더 클릭하고 싶을까요? 당연히 윗 글일 겁니다. 대부분의 리치스니펫 UI가 구글에서 인증한 느낌이 들고 콘텐츠의 대한 정보를 미리 일부 제공하는 등 호기심을 유발할 수 있거든요. 리치스니펫 문법인 JSONLD를 적용한다고 해서 곧바로 내 페이지가 리치스니펫이 적용된 채로 보이게 되는 것도 아니고, 순위에 직접적으로 좋은 영향이 생기는 것도 아닙니다만 리치스니펫이 적용되었을 때 유저가 내 페이지를 선택할 확률을 높일 수 있으므로(= CTR 증가) 간접적인 이점이 됩니다. 💡 CTR(Click-Through Rate): 콘텐츠가 노출된 횟수 대비 사용자가 실제로 클릭하여 유입된 비율 3) title을 제외한 메타 데이터 메타 데이터는 해당 페이지에 대한 정보를 정의하는 데이터입니다. 대표적으로 검색엔진이나 SNS 공유 시 노출될 페이지의 정보를 담을 수 있습니다. 역시 이를 설정한다고 하여 직접적으로 순위에 영향이 생기는 것은 아니고 유저에게 내 페이지의 정보를 요약하여 제공함으로써 CTR을 올릴 수 있는 작업이므로 간접적으로 기여합니다. 4) 유저가 인지하기 쉬운 URI 사용 URI를 구성할 때 단순 id보다는 유저가 알아보기 좋은 slug형태로 구성하는 것을 권장합니다. ❌ /post/1 ✅ /post/how-to-make-pizza 구글의 순위 알고리즘 측면에서 도움이 되는건 아니지만 유저입장에서는 path가 slug형태로 보이는게 더 선택할 확률이 높으므로 간접적인 이점이 있다는 뜻으로 볼 수 있습니다. 개인적으로도 검색결과에 나타나는 브레드크럼이 페이지의 정보를 담고 있는 느낌이 들 때 더욱 클릭하고 싶은 마음이 들곤 했습니다. 정리 지금까지 SEO하면 떠오르는 작업들을 '순위'의 관점에서 공식문서를 통해 팩트체크해보면서 돌아보았는데요. 결론은 '순위'와 관련이 되었든 안되었든 공식문서에서 언급된 것들이면 다 하는 것이 좋습니다. 순위와 직접적인 연관은 없더라도 유저의 선택을 유발할 수 있는 요소들이라면 내 사이트에 유입되는데에 도움이 되니까요. 저는 SEO가 중요한 현업에서 해왔던 작업들이 실제로 얼마나 도움이 되는 걸까 싶어 호기심차 알아봤을 뿐이고 우리가 알던 작업들이 사실은 조금 도움이 되는 것이든, 많은 도움이 되는 것이든 해야하는 것은 변함없는 사실입니다. 다만 제가 공유하고 싶었던 것은 흔히 알려진 JSONLD, 메타데이터같은 작업들보단 비교적 덜 언급되는 코어 웹바이탈 개선을 위한 CDN같은 작업이 우리가 목표로 하는 최종적인 SEO에 더 도움이 된다는 것이죠. 시작에서도 말했듯 색인을 거쳐 '노출'시키는 것까지는 어느 사이트든 쉽게 할 수 있습니다. 다만 우리의 목표는 내 페이지가 유저의 시야에 들어 유입되게 하는 것이니까요. 그러기 위해선 SERP 상단에 노출되게 하는 '순위'라는 개념에 대해서 강조를 하며 글을 마무리하겠습니다 :)

  • 블로그 제작기 - 캐시 적용기
    블로그
    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)의 힘을 체감해보았고 실제 적용했던 과정은 다음 글에서 적어볼까 합니다. ...