현대 FE에 와서의 캐싱이란 feat. nexjts, aws ecs

(곧 퇴사라 그런가.. 적적하긴하다)
FE 에서 캐시? cdn에 올리면 끝 아니야 ?
현대 FE에 있어서, FE는 더이상 html을 서빙하는 분야가 아니게 되었다.
오죽하면 세간에서 우스갯소리로 현대 FE는 풀스택이다! 라는 말도 나돌정도다
그것도 그런게 이제 FE에서도 자신만의 서버를 가지게되었고 서버가 생김에 따라 서버에서 사용하던 기법들을 그대로 FE에 사용가능하게되었다
물론 이전에도 템플릿엔진이 이걸 하던시절이있는데 JAVA + JSP 라던가 더 옛날로 돌아가면 apache + php 라던가 ..
더더더더 구식으로가면 그냥 server + html 를 수동으로서빙한다던가 ..
이 역할이 이제 그대로 node + react가 되었다 생각하면 된다.
react와 nextjs의 발전
22년도부터 nextjs가 화두에 서게되면서 각기업들이 nextjs를 채용하게되고 ..
지금있어서야 FE 개발자라면 필수소양이 되어버린 nextjs...
뭐 이해는 간다. nuxt만큼은 아니지만 어느정도 강제되는 lifecycle과 수없이 제공되는 기능들, 사실 쓰다보면 빠져들수밖에없다
app router와 server component, cache 조금 더 지나면 default가 될 cache component + ppr 까지, 별의 별 기능 다만들어뒀다.
이 기능들을 다 써본 느낌으로는
ㅋㅋ 알아서 선택해서 써 우린 다 만들어 뒀다?
약간 이런느낌이다.
기본적으로 다들 SSR 로직을 따르고 있을거라 믿어 의심치않고. 나처럼 시간만 생기면 최적화에 목메는 사람들은 cache까지 건들이고 ..
배포환경이 듀얼 환경이면.. 여러모로 많이 생각하게되는 프레임 워크이다.
이번글은 캐싱이니 캐싱에대해서 이야기해보자
개인적으로 nextjs에서 캐시라고 하면 가장 많이 쓰이는건 아무래도 UI Level cache (nexts 16와서 방법이 살짝 달라지긴했다)
비슷한방식으로 Data-level caching이 있을텐데 여기서도 좀 갈린다
Fetch API 를 쓰지 않는다면 useCache를 이용한 framework에서 함수결과값을 caching하는 방법,
반대로 fetch api 를 쓴다면 nextjs의 확장된 Fetch API 가 제일 주로 많이 쓰일것이다.
나는 실제로 FetchAPI 와 tanstack query의 prefetchQuery를 통합해서 사용을 하고있는데 매우 적절한 로직이 아닌가싶다.
Hydration도 tanstack query가 알아서 잘 해주니 나는 HydrationBoundary만 잘 넣어주고 데이터만 잘 넣어주면 page단의 caching과 prefetch를 동시에 이루면서..
마치 이 모든게 정적페이지다 ! 라는 의심을 줄 만큼의 UX를 제공한다
그만큼 잘 쓰면 매우 만족되는 기능아닐까 싶다.
에에서 더 나아가 공식에서는 권장하지는 않지만 (근데 쓰지말라는 소리는 안했다) proxy.ts (middleware.ts) 에서의 cache 도 가능해졌다
원래 15.5버전 미만에서는 이게 불가능했는데 (똥고집 nextjs팀에서 그 전까지는 분리된 별도의 server runtime환경으로 운영했었다)
버전업 15.5에서 이걸 버리고 node 런타임으로 넘어오면서 가능해졌다.
그래서 이전에 시도하던 로직들도 다 먹히고 … 실제로 여기서도 한 50ms 깎아내는 기염을 토해냈다
이런식으로 server의 로직을 마음껏 다룰수있다는점은, 그만큼 매우 매력적이다는것이다
캐싱 방법 ?
nextjs의 고유방법 말고, 서버에서 데이터를 캐싱한다면 인메모리와 외부 kv인스턴스를 이용한 캐싱방법 (대표적으로 redis정도)이 있는데
이번에는 인메모리에서 쓸만한 4가지의 방법정도 적어보려한다
여기서 왜 4가지방법이있고 이걸 왜쓰냐 ? 할수도있는데 .. cdn이 아닌 서버환경에서는 당연하게도 제한적인 환경에서의 자원을 활용해가며 사용해야한다
예를들어서 cache 객체가 무제한으로늘어나버리면 꼼짝없이 OOM이 되어버려 서버가 터져버리니 .. 이러한것도 알아가면서 적용해야한다는거다
LRU Cache
LRU는 Least Recently Used의 약자로, 캐시가 가득 차면 가장 오랫동안 사용하지 않은 데이터를 먼저 지우는 방식이다.
예를 들어 캐시에 A, B, C를 넣어두고 순서대로 조회했다고 해보자.
여기서 새로운 D를 넣을 공간이 없다면 가장 오래전에 사용한 A를 지운다.
근데 그 사이에 A를 한번 더 조회했다면 ? 이제는 B가 지워지는 것이다.
이렇게 조회할 때마다 사용 순서를 갱신하고, 공간이 부족해지면 가장 오래 사용하지 않은 데이터부터 제거한다.
다만 한번씩만 조회하는 데이터가 갑자기 많이 들어오면.. 기존에 자주 사용하던 데이터도 밀려날 수 있다.
최근에 사용했다는게 앞으로도 자주 사용한다는 뜻은 아니기때문이다.
LFU Cache
LFU는 Least Frequently Used의 약자로, 캐시가 가득 차면 사용 횟수가 가장 적은 데이터를 먼저 지우는 방식이다.
LRU가 언제 썼냐를 본다면, LFU는 몇 번 썼냐를 본다.
많이 찾는 데이터니까 캐시에 남겨두는게 이득 아니야 ? 라는 생각이다.
예를 들어 A는 10번, B는 3번, C는 1번 조회했다면 새로운 D를 넣을 공간이 없을 때 C를 먼저 지운다.
자주 사용하는 데이터를 남겨두기에는 좋다.
근데 이것도 문제가 없는건 아니다. 예전에만 잘 나가던 데이터가 쌓아둔 사용 횟수 덕분에 계속 자리를 차지할 수 있다는 것이다.
반대로 새로 들어온 데이터는 아직 사용 횟수가 적으니 불리할 수밖에 없다.
그래서 구현에 따라 오래된 사용 기록의 영향력을 줄이는 방법을 같이 사용하기도 한다.
FIFO Cache
이건 더 단순하다.
먼저 들어온 데이터를 먼저 지운다.
이름도 First In, First Out 이름 그대로다.
A → B → C 순서로 캐시에 넣었다면 새로운 D를 넣을 공간이 없을 때 A부터 지운다.
여기서 LRU랑 뭐가 다른데 ? 싶을 수 있는데..
FIFO는 중간에 A를 백번 조회해도 A를 먼저 지운다.
사용 여부와 관계없이 들어온 순서만 보기때문이다.
구현도 단순하고 언제 조회했는지, 몇 번 조회했는지 관리할 필요도 없다.
다만 지금 제일 많이 쓰는 데이터가 A여도 먼저 들어왔다는 이유로 지워진다는 것이다.
단순한만큼 이런 부분은 감수해야한다.
TTL Cache
이건 캐시에 유통기한을 붙이는 방식이다.
TTL은 Time To Live의 약자인데, 이 데이터를 얼마나 오래 유효하게 볼거냐 ? 를 정한다고 생각하면 된다.
예를 들어 상품 정보를 한번 가져온 뒤 5분 동안 캐싱한다고 해보자.
그동안은 요청이 들어와도 캐시에서 꺼내주고, 5분이 지난 뒤 다시 요청이 들어오면 원본을 조회해서 새로 저장하는 것이다.
매번 서버에 물어볼 필요도 없고..
한 번 저장한 정보를 평생 들고있을 필요도 없어진다.
물론 그 5분 사이에 원본이 바뀌었다면 예전 정보를 보여줄 수는 있다.
그래서 유효 시간도 데이터의 성격에 맞게 정해야한다.
근데 여기서 주의할 점이 있다.
유효 시간이 끝났다고 메모리에서도 바로 사라지는건 아니라는 것이다.
구현에 따라 다음 조회 때 지울 수도 있고, 일정 주기로 만료된 데이터를 정리할 수도 있다.
그러니 TTL을 걸었으니 메모리도 알아서 관리되겠지 ? 하고 끝내면 안된다.
여하튼 OOM을 막으려고 시작한 이야기니 다시 돌아오면..
결국 캐시를 최대 몇 개까지, 또는 어느 정도 용량까지 보관할 것인지도 정해야한다.
LRU, LFU, FIFO는 무엇부터 지울지를 정하는 방식이고, TTL은 언제까지 유효한지를 정하는 방식이다.
TTL을 5분으로 설정했어도 그 5분 안에 key가 수십만개 들어오면 메모리는 그대로 커지는 것이다.
이 중에 무조건 하나만 골라야하는건 아니다.
최대 크기를 정해두고, 가득 차면 LRU로 지우고, 오래된 데이터는 TTL로 만료시키는 식으로 같이 쓰면 된다.
캐싱도 결국 데이터를 메모리에 올려두는 일이다.
넣는 로직을 만들었으면 언제 어떻게 지울지도 같이 생각해야한다.
또 다른 적, 서버 이중화
이렇게 캐싱을하게되면 필연적으로 따라오는 문제
원천 데이터는 같은데 서버가 각자 다른 캐싱을 해버리면 ??
이제 여기서 머리를 탁 치게되고 왜 수많은 선조들이 Redis같은 KV인스턴스를 따로 두는지 이해하게된다.
방법은 간단하다. 캐싱로직을 인메모리로 넣는게아닌 외부 KV인스턴스로 돌리면 끝나는 방식.
심지어 위에 언급한 캐싱방법들도 신경안 쓰고 용량도 넉넉하겠다, 그리고 서버가 재시작되어도 인스턴트 데이터는 유실되지않는다
nexjts에서도 이미 이것을 경험했는지 헐레벌떡 cache에관한 외부인스턴스 지원을 이미 해둔 상태이다
Custom Cache Handler
물론 proxy.ts 에서 구현한 자체 caching에대해서는 해당 로직을 직접 짜서 넣어야하지만 나머지 확장된 Fetch API 로직, data, ui level cache등의 cache로직은
nextjs에서 알아서 cache hanlder를 감지하고 외부 캐싱을 쓸 수 있게해준다
그래서 무조건 좋은건가 ?
어느정도 풍문으로라도 BE로직을 다룰줄 안다면 금상첨화라 개인적으로 생각한다
더욱더 사견을 첨부하자면 무조건 좋은거고.. 개발이란 무릇 경력이 쌓이면서 자연스레 풀스택엔지니어가 된다는.. 아니 되어야한다는 의견이기도하기에
이러한 변화는 매우 좋다고 생각한다
하지만 반대로말하면 이러한 개념들을 실제로 배울때는 매우 혼란이 된다는것이다
일반적으로 FE라고가르친건 jsx문법에따라른 로직, 그리고 컴포넌트 최적화 등 단위단위의 내용을 가르치다가
갑자기 실무들어오니 이러한 서버까지 고려하면서 개발해라 ! 하면 주니어들은 다 죽어나가는건 확정이기때문이다
그래도 그 영겁의 고통을 버티고나면.. 어느정도 머리가 넓어지기 때문에 개인적으로는 매우 좋다 생각한다
마치면서
주말에 심심하게 뭐하지 하다가 IDE를 열었다가 ...
작년의 캐싱과 구현등에 머리빠져가면서 구현한 솔루션들을 보면서 글을 적게되었다
물론 이번달까지만하고 팀 공중분해인지라 정감가는 솔루션을 손에서 떠보내야하는 슬픔은 살짝있지만 ..
흑흑
다음 회사에서는 조금 더 나은, 좋은 아키텍쳐로 설계하자는 마음을 가져본다
댓글