[Next.js] App Router 시대의 스타일링과 API 전략: RSC와 RCC

들어가며

이번에 새로운 프로젝트에 들어가면서, Next.js에 Tailwind와 Emotion을 조합을 마주하게 되었습니다. 저는 이미 기획과 아키텍처 구성이 끝난 후에 프로젝트에 참여하게 되었기에 제가 현재의 구성에 맞춰야한다고 생각했습니다! 그럼에도 저 조합의 선택 이유가 궁금하여 프론트엔드 팀원분께 이것 저것 집요하게 질문했던 것 같습니다.
 
사실 저는 단순히 '새로운 기술스택을 사용해봤다~'는 경험보다 중요한 것은 '왜 이 라이브러리를 선택했으며, 그로 인해 발생하는 이슈나 장점은 무엇인가'를 알아야하는게 더 중요하다고 생각했습니다. 그렇기에 Next.js 조합에 런타임에서 동적으로 css를 생성하는 emotion의 도입이 굳이 필요한가?라는 생각이 들었고, 만약 반드시 emotion을 사용하고 싶다면 두명 모두 경험이 없고 학습이 되지 않은 Next.js는 이번 프로젝트에서 제외하는게 맞다는 생각을 했습니다.
 
무튼 결론은 이런 저런 이유로 초기 결정 그대로 가게 되었고, 이를 지금 정리해두어야만 나중에 편할 것 같아서 블로그 글을 작성해봅니다! 무엇보다 의논하는 과정에거 얻었던 나름의 인사이트들을 정리해보려합니다. 백엔드 시점에서도 이 프론트의 렌더링 전략이 API 형태과 직결된다는 점을 알 수 있었습니다.
 

1. 런타임 CSS-in-JS(Emotion)의 트레이드 오프

팀 내에서 제안된 "정적 구조는 Tailwind, 동적 스타일은 Emotion" 전략은 언뜻 보면 합리적인 방식입니다. 하지만 Next.js App Router 환경에서는 명확한 비용이 발생한다고 생각합니다. 무엇보다 Next.js공식 홈페이지에서도 '페이지의 대부분을 서버 컴포넌트로 구성할 것을 권장'한다고 까지 써있습니다. CSS-in-JS를 사용할 경우 다음과 같은 문제가 생깁니다.

  • 서버 컴포넌트(RSC)의 제약: RSC는 서버에서 실행되고 결과물만 브라우저로 보냅니다. 하지만 Emotion은 브라우저 런타임에 스타일을 생성하므로 반드시 'use client' 선언이 필요합니다.
  • 하이드레이션 비용: 모든 컴포넌트에 Emotion을 도입하면 페이지 전체가 클라이언트 컴포넌트화되어 자바스크립트 번들 사이즈가 커지고, Next.js가 제공하는 'Zero JS'의 이점이 희석됩니다.

결론: 스타일의 편의성을 위해 'use client'를 남발할 것인가, 아니면 성능을 위해 Tailwind와 cva같은 도구로 동적 스타일을 정복할 것인가에 대한 선택이 필요했습니다. 그러나 우선 첫째로 다른 팀원분께서 Tailwind 사용 경험이 적어 선호하시지 않는다고 느꼈으며, 둘째로 emotion과 NextJS사용 경험이 없던 저에게 스킬을 연습할 수 있는 좋은 기회였고, 애초에 프로젝트 목적이 '공부하고 싶은 것들 다해보기' 였기에 더이상 성능과 합리적인 조합을 운운하며 이미 기본으로 셋팅된 환경을 엎어버릴 필요가 전혀 없다고 생각했습니다. 
 

2. 기술적 이상과 현실적 협의: '스타일 경계선'을 통한 최적화

사실 기술적으로만 본다면 앞서 말했듯이 Next.js의 서버 컴포넌트 이점 극대화를 위해 Zero-runtime CSS를 지향하는 것이 정답이라고 생각했습니다. 하지만 이번 프로젝트는 '팀원 모두의 기술적 성장'이라는 특수한 목표가 있었고, 팀 내에서 각각의 기술 스택에 대한 숙련도 차이가 있었습니다. 저는 여기서 두 가지 관점으로 합의점을 찾았습니다.
 
첫째, 개발자 경험(DX)과 생산성 확보입니다.
팀원분께 이미 익숙한 Emotion의 Props기반 동적 스타일링을 유지함으로써 개발 속도와 코드 가독성을 확보하되, '스타일 경계선'을 명확히 그어야만 합니다. 꼼꼼한 코드 리뷰를 통해 과하게 클라이언트 컴포넌트를 사용하는 지점은 없는지 살펴보며 개발을 하기로 결정했습니다. 레이아웃과 정적 구조는 Tailwind로 짜서 서버 컴포넌트 비중을 최대한 높이고, 상호작용이 필수적인 '말단 컴포넌트'에만 Emotion을 제한적으로 사용하는 가이드가 필요합니다.
 
둘째, 트러블슈팅을 통한 학습입니다.
비효율이 예상되는 조합임을 알았기에, 오히려 이를 역이용해 '런타임 CSS-in-JS가 하이드레이션에 미치는 영향'을 직접 분석해보고 싶었습니다. 실제로 프로젝트를 진행하며 'use client'의 경계를 어디에 두느냐에 따라 번들 사이즈가 어떻게 변하는지 모니터링하며 개발하겠다는 나름의 목표를 세웠습니다.
 
이렇게 한다면 결과적으로 프로젝트 수행 목표도 달성하며, 그 과정에서 최적의 성능을 끌어내기 위한 경험도 챙길 수 있을 것 같습니다!
 

3. 프론트엔드의 렌더링 전략과 백엔드 API 설계와의 상관관계

의논 과정에서 얻은 또다른 인사이트는 프론트엔드의 렌더링 전략(RSC/RCC)이 백엔드 API의 형태와 직결된다는 점이었습니다.
사실 이 부분은 생각을 안하고 있던게 바보같네요 ㄷㄷ 이 부분은 또 백엔드 팀원분께서 글도 정리해주셔서 시각적으로도 더 명확하게 이해할 수 있었습니다. 최고! (아래 해당 팀원분의 포스팅도 첨부합니다)

  • API 통합의 비효율: 만약 백엔드 API가 정적 데이터와 실시간 데이터를 한 번에 주는 '통합형'이라면, 서버 컴포넌트에서 초기 HTML을 그릴 때 가져온 데이터를 클라이언트 컴포넌트가 재사용하기 위해 다시 무거운 데이터를 요청해야 하는 낭비가 발생합니다.
  • API 분리와 TTFB 최적화: 이를 해결하기 위해 정적 정보(서버 컴포넌트용)와 실시간 정보(클라이언트 컴포넌트용)를 분리하는 Granular API 전략이 필요함을 깨달았습니다. 이렇게 하면 서버 컴포넌트는 캐싱된 데이터를 통해 빠르게 HTML을 내려주고(TTFB 개선), 상호작용이 필요한 부분만 가볍게 클라이언트에서 업데이트할 수 있습니다.

결국 프론트엔드 컴포넌트의 성격(RSC vs RCC)에 맞춰 백엔드 API도 파편화(Granularity)되어야 최상의 성능이 나옵니다! 그렇기에 구현 과정에서 백엔드 팀원과의 소통도 굉장히 중요하겠네요.
https://leestana01.tistory.com/26

프론트의 CSR과 SSR에 따른 백엔드의 대응 방식 연구

배경최근 프론트 팀원들의 컴포넌트 CSR과 SSR에 대한 열띤 논의가 있었다.그런데, CSR과 SSR은 프론트 측의 단순 UI 렌더링만 관련있을까?백엔드 api 설계는 이와 완전 무관할까? 이 게시글을 통해

leestana01.tistory.com

 
 

마치며: 기술 선택의 근거를 찾아서

이번 프로젝트에서는 '하고 싶은 거 다 공부해보기'라는 특수한 프로젝트 목표와 팀의 생산성을 위해 Tailwind와 Emotion을 병행하되, 최대한 서버 컴포넌트 비중을 높이고 API 호출을 최적화하는 방향으로 성능의 균형을 잡아보려 합니다. 프론트엔드와 백엔드가 함께 고민할 때, 비소로 진정한 의미의 '풀스택 최적화'가 가능해짐을 배운 소중한 경험이었습니다~