Skip to content

Instantly share code, notes, and snippets.

@leedc0101
Created April 19, 2026 01:29
Show Gist options
  • Select an option

  • Save leedc0101/801fba0ea855a69d07d7496978cab808 to your computer and use it in GitHub Desktop.

Select an option

Save leedc0101/801fba0ea855a69d07d7496978cab808 to your computer and use it in GitHub Desktop.
frontend-briefing-2026-04-19-KST

원문 제목: Under the hood of MDN's new frontend 원문 링크: https://developer.mozilla.org/en-US/blog/mdn-front-end-deep-dive/ 번역일: 2026-04-19 KST

한국어 번역

MDN은 작년에 새 프론트엔드를 공개했지만, 눈에 띄는 변화는 디자인 통일 정도였고 진짜 큰 변화는 내부 코드 구조였다. 이 글은 왜 MDN이 프론트엔드를 다시 만들었는지, 어떤 기술을 골랐는지, 그리고 그 선택이 어떤 문제를 해결했는지를 설명한다.

기존 MDN 구조를 단순화하면 이렇다. 문서는 여러 Git 저장소의 Markdown으로 관리되고, 빌드 도구가 이를 HTML과 메타데이터가 포함된 JSON으로 바꾼다. 이후 프론트엔드가 이 JSON을 읽어 브라우저 호환성 표, 다국어 처리, 네비게이션 등을 포함한 완성 페이지로 조립하고, 최종 HTML/CSS/JS를 클라우드에 올려 전 세계 사용자에게 배포한다.

문제는 예전 프론트엔드인 yari가 React 앱으로 시작했지만 시간이 지나며 기술 부채가 크게 쌓였다는 점이다. Create React App 기반으로 시작했지만 기본 설정이 맞지 않아 점점 우회 코드가 추가됐고, 결국 eject 이후 매우 복잡한 Webpack 설정과 해키한 빌드 스크립트가 남았다. CSS 쪽도 Sass와 CSS 변수, 전역적으로 얽힌 스타일, 빈약한 스코프 관리 때문에 한 컴포넌트를 수정하면 다른 곳이 깨지는 일이 잦았다. 그 결과 사용자는 자신이 보지 않을 컴포넌트 스타일까지 포함된 큰 렌더 차단 CSS를 받아야 했다.

가장 큰 문제는 React 앱이 실제 문서 콘텐츠를 이해하지 못하는 ‘껍데기’였다는 점이다. 빌드된 HTML 콘텐츠를 React가 다루게 하려면 다시 파싱하고 많은 로직을 클라이언트 번들에 실어야 했다. MDN은 이를 피하기 위해 dangerouslySetInnerHTML로 문서 HTML을 주입했다. 그러다 보니 코드 블록 복사 버튼 같은 상호작용은 React 바깥의 순수 DOM API로 구현해야 했고, 경우에 따라 React 버전과 DOM API 버전을 둘 다 유지해야 했다.

이 문제를 풀기 위해 MDN 팀은 2024년부터 Lit과 Web Components를 실험했다. 첫 성공 사례는 Scrimba 학습 콘텐츠를 삽입하는 ‘Scrim’ 컴포넌트였다. 사용자가 실제로 열기 전까지 iframe을 로드하지 않고, dialog 요소로 풀스크린 동작도 제어해야 했는데, Lit 기반 커스텀 엘리먼트로 구현하니 JSX에 가까운 템플릿 문법과 상태 기반 갱신을 쓰면서도 문서 콘텐츠 안에 바로 삽입할 수 있었다.

이후 더 복잡한 인터랙티브 예제 시스템도 같은 방향으로 옮겼다. 기존에는 예제 코드와 렌더링 로직이 네 개의 저장소에 흩어져 있었고, 작성자 입장에서도 수정과 디버깅이 번거로웠다. 새 구조에서는 CodeMirror 기반 에디터, 콘솔 출력, 미리보기 렌더러, 상태 전달 컨트롤러를 각각 커스텀 엘리먼트로 쪼갰다. 덕분에 문서 안의 코드 블록을 직접 읽어 인터랙티브 예제로 연결할 수 있게 됐고, 작성 경험도 훨씬 단순해졌다.

하지만 MDN의 재구축은 인터랙티브 조각만의 문제가 아니었다. React SPA 모델은 정적인 콘텐츠도 결국 클라이언트에서 다시 렌더링하기 위해 큰 JS 번들을 내려보내야 한다. MDN 팀은 React Server Components가 이 문제를 푸는 방향이라고 인정하면서도, 이를 제대로 쓰려면 현재 구조와 맞지 않는 프레임워크로 크게 이주해야 한다고 봤다. 대신 MDN은 사이트 성격 자체를 다시 정의했다. MDN 대부분은 정적인 문서와 코드 예제이고, 상호작용은 ‘섬’처럼 일부에만 존재한다. 그렇다면 페이지 전체를 하나의 SPA로 둘 이유가 없고, 필요한 부분만 웹 컴포넌트로 두면 된다.

그래서 MDN은 서버 템플릿도 자체적인 ‘서버 컴포넌트’ 개념으로 정리했다. Lit의 HTML 템플릿 리터럴을 서버에서도 활용해 네비게이션, 메뉴, 검색 UI 같은 정적 구조를 컴포넌트 단위로 조립했다. 이 방식은 클라이언트에 불필요한 로직을 보내지 않으면서도, 컴포넌트 단위 스타일링과 조립의 장점을 유지할 수 있었다.

결국 새 구조는 세 가지 문제를 한 번에 해결했다. 정적인 콘텐츠를 위해 거대한 SPA 번들을 보내지 않아도 되고, 문서 HTML을 이해하지 못하는 ‘래퍼 앱’ 문제도 사라졌으며, 각 인터랙션은 필요한 곳에서만 로드되는 독립적인 웹 컴포넌트가 되었다. MDN이 내린 결론은 분명하다. 콘텐츠 중심 사이트라면 앱처럼 다 만들 필요가 없고, 정적 렌더링과 작은 상호작용 섬의 조합이 더 단순하고 더 빠를 수 있다.

원문 제목: React Server Components Your Way 원문 링크: https://tanstack.com/blog/react-server-components 번역일: 2026-04-19 KST

한국어 번역

TanStack는 React Server Components(RSC)를 ‘서버가 전체 트리를 소유하는 특별한 프레임워크 규칙’이 아니라, 필요할 때 가져오고 캐시하고 렌더할 수 있는 하나의 데이터 스트림으로 다루고 싶었다. 이 글은 TanStack Start가 왜 그런 방향을 택했는지, 그리고 그게 실제 프론트엔드 구조에 어떤 의미를 갖는지를 설명한다.

글의 출발점은 간단하다. RSC는 무거운 렌더링 작업이나 잘 변하지 않는 콘텐츠를 클라이언트에서 서버로 옮길 수 있는 강력한 기본 요소다. Markdown 파싱, 문법 하이라이팅, 날짜 포맷팅, 검색 인덱싱, 콘텐츠 변환처럼 굳이 브라우저에서 할 필요 없는 일에 특히 잘 맞는다. 하지만 현재 많은 프레임워크는 RSC를 “서버가 트리를 소유하고, use client로 상호작용 영역을 표시하며, 프레임워크가 전체 구조를 정한다”는 방식으로 제공한다. TanStack는 여기서 불편함을 느꼈다. RSC를 쓰기 위해 앱 전체가 그 패러다임을 중심으로 돌아가야 하기 때문이다.

TanStack Start의 핵심 아이디어는 이렇다. RSC는 결국 React Flight 스트림일 뿐이고, 그렇다면 JSON을 가져오듯 RSC도 가져오면 된다. 서버에서 JSX를 스트림으로 만들고, 클라이언트나 SSR 단계에서 이를 다시 React 엘리먼트로 디코딩한다. 즉, RSC를 프레임워크의 신성한 중앙 규칙으로 만들지 않고 일반적인 비동기 데이터처럼 취급한다.

예시 코드에서도 서버 함수인 createServerFn이 renderToReadableStream으로 JSX를 스트림으로 바꿔 반환하고, 클라이언트는 useQuery 안에서 createFromReadableStream으로 이를 읽어 렌더한다. TanStack가 강조하는 건 API 표면이 작고 단순하다는 점이다. 서버에서는 renderToReadableStream, 클라이언트에서는 createFromReadableStream 또는 createFromFetch만 알면 된다.

이 접근의 장점은 캐싱과 조합성이 기존 생태계와 자연스럽게 맞물린다는 데 있다. RSC가 “그냥 데이터”라면 TanStack Query로 명시적인 queryKey와 staleTime을 적용할 수 있고, Router 로더 안에서도 다른 로더 데이터처럼 다룰 수 있다. GET 기반 서버 함수라면 CDN 레벨에서 HTTP 응답 자체를 캐시할 수도 있다. 즉 RSC 때문에 완전히 새로운 캐시 모델을 배울 필요가 없다.

보안 관점도 중요하게 다룬다. 최근 RSC 스택 주변 CVE를 언급하며 TanStack Start는 의도적으로 use server 액션을 지원하지 않는다. 대신 createServerFn 기반의 명시적 RPC만 허용한다. 클라이언트와 서버 경계가 숨겨진 마법이 아니라 드러난 계약이 되도록 하겠다는 뜻이다. 직렬화, 검증, 미들웨어도 그 전제를 기준으로 설계했다.

TanStack는 RSC가 모든 페이지에 만능은 아니라고도 분명히 말한다. 콘텐츠 비중이 높고 의존성이 무거운 블로그, 문서, 마크다운 페이지에서는 효과가 크지만, 대시보드나 장시간 상호작용이 많은 앱 화면은 효과가 작거나 혼합적일 수 있다. 실제로 tanstack.com의 콘텐츠 페이지 일부를 이전해 측정한 결과, 블로그와 문서 페이지는 gzip 기준 클라이언트 JS가 약 153KB 줄었고, Total Blocking Time도 크게 개선됐다. 반면 이미 상호작용 셸 비중이 큰 페이지는 차이가 적었다.

여기서 끝나지 않고 TanStack는 Composite Components라는 새 개념도 소개한다. 서버가 모든 클라이언트 UI를 미리 결정하는 대신, 서버는 슬롯만 남기고 클라이언트가 그 자리를 채우게 한다. 서버는 게시글 본문과 링크를 렌더하고, 클라이언트는 댓글이나 액션 버튼 같은 조각을 children 혹은 render prop 형태로 주입한다. 이 방식은 서버가 트리를 독점하지 않으면서도 서버 렌더링과 클라이언트 상호작용을 함께 쓰게 해 준다.

결국 이 글의 요지는 명확하다. RSC는 반드시 프레임워크가 전체 앱을 소유하는 방식으로만 쓸 필요가 없다. 오히려 스트림 기반 데이터라는 본질에 집중하면, 기존의 Query, Router, CDN, HTTP 캐시와 자연스럽게 연결되고, 필요한 곳에만 점진적으로 도입할 수 있다. TanStack Start는 RSC를 패러다임 강요가 아니라 선택 가능한 성능 도구로 되돌리려 한다.

원문 제목: Frontend Framework Bundle Size Benchmark: React/Vue/Angular vs Fine-Grained Runtimes 원문 링크: https://dev.to/qingkuai/frontend-framework-bundle-size-benchmark-reactvueangular-vs-fine-grained-runtimes-2nk0 번역일: 2026-04-19 KST

한국어 번역

프레임워크 비교는 보통 개발 경험, 생태계, 런타임 성능에 집중되지만, 사용자가 가장 먼저 체감하는 것은 다운로드, 압축 해제, 파싱, 실행 비용이다. 이 글은 그 지점을 보기 위해 같은 기능 범위와 같은 보고 규칙 아래에서 프레임워크 번들 크기를 비교한 벤치마크를 소개한다.

작성자는 여러 프레임워크로 동일한 TodoMVC 기능을 구현한 뒤, 하나의 통합 리포트로 비교했다. 비교 지표는 raw 크기, minified 크기, minified+gzip 크기이며, 런타임, 템플릿, 스크립트, 스타일로도 나눠서 본다. 구현 차이에서 오는 잡음을 줄이기 위해 모든 프레임워크는 동일한 TodoMVC 동작을 구현했고, template/script/style 출력을 추출해 비교했으며, 스타일도 모두 스코프 처리했다. 스타일은 차트에서 기본 선택이 아니지만 통계에는 포함된다.

주류 그룹인 React, Angular, Vue 3를 보면 1개 컴포넌트 기준 minified 번들은 Angular 약 201.69KB, React 약 189.44KB, Vue 3 약 65.64KB였다. 핵심 원인은 런타임 비용이다. 같은 조건에서 Angular 런타임은 약 195.39KB, React는 약 185.66KB, Vue 3는 약 61.83KB였다. 컴포넌트 수가 늘어날수록 Angular와 React는 가파르게 증가하고 Vue 3는 상대적으로 낮은 수준을 유지했다.

글은 여기서 멈추지 않고 Solid, Vue Vapor, Svelte 5, QingKuai 같은 fine-grained 계열도 비교한다. 생태계 전체 흐름은 “범용 virtual DOM diffing을 줄이고, 의존성 단위로 더 정밀하게 갱신하는 방향”이라는 점을 인정하면서도, 같은 방향에 속한 프레임워크끼리도 실제 결과는 꽤 다르다고 말한다.

1개 컴포넌트 기준 minified 수치만 보면 Solid가 약 19.04KB로 가장 작고, QingKuai 25.42KB, Svelte 5 39.25KB, Vue Vapor 47.27KB 순이었다. 하지만 컴포넌트 수가 늘어나는 성장 곡선을 보면 이야기가 달라진다. QingKuai는 더 완만하게 증가해 이번 실험의 높은 컴포넌트 구간에서는 가장 낮은 총량으로 끝났고, Solid는 시작은 가장 작지만 증가 속도가 더 빨랐다. Svelte 5와 Vue Vapor는 큰 규모 구간에서 QingKuai보다 위에 남았다. 결론은 단순히 “fine-grained가 더 좋다”가 아니라, 런타임 구조와 컴포넌트 수에 따른 증가 기울기가 중요하다는 것이다.

Svelte 4는 따로 언급된다. 작은 앱 관점에서는 매우 매력적이다. 1개 컴포넌트 기준 minified 번들이 약 11.08KB이고 런타임은 0.71KB 수준이다. 하지만 규모가 커질수록 곡선이 훨씬 가파르게 올라간다. 중앙 런타임 비용은 낮지만, 컴포넌트 출력 안에 반복되는 구현 조각이 누적되기 때문일 수 있다고 글은 해석한다. 그래서 매우 작은 앱에서는 좋은 선택이 될 수 있지만, 큰 앱에서는 시작 수치보다 성장 기울기를 먼저 보라고 조언한다.

작성자가 얻은 네 가지 교훈은 다음과 같다. 첫째, 번들 크기는 프레임워크 선택에서 1급 지표여야 한다. 둘째, virtual DOM이 없고 fine-grained라는 방향성 자체는 중요하지만, 같은 방향의 구현도 크게 다를 수 있다. 셋째, 1컴포넌트 최저 수치만 보면 오판할 수 있다. 넷째, 중대형 앱에서는 최저 시작점보다 성장 곡선이 더 중요할 때가 많다.

즉 이 벤치마크가 던지는 메시지는 아주 실무적이다. 프레임워크 선택 때 “첫 화면 데모가 얼마나 작냐”만 보면 안 되고, 실제 앱이 커질수록 비용이 어떻게 누적되는지를 같이 봐야 한다. 스타트업의 작은 MVP와 장기 운영 제품은 같은 그래프로 평가하면 안 된다는 뜻이기도 하다.

원문 제목: Six bugs that only appeared after real users installed my React security library 원문 링크: https://dev.to/nedunuri_anurag/six-bugs-that-only-appeared-after-real-users-installed-my-react-security-library-29mk 번역일: 2026-04-19 KST

한국어 번역

작성자는 민감한 폼 입력값을 세션 리코더, 브라우저 확장, AI 화면 판독기로부터 보호하기 위해 FieldShield라는 “제로 트러스트” React 입력 라이브러리를 만들었다. 핵심 아이디어는 DOM에는 항상 xxxxx 같은 마스킹 문자만 존재하게 하고, 실제 값은 Web Worker 안에만 보관하는 것이다. submit 핸들러가 명시적으로 요청할 때만 진짜 값이 Worker 밖으로 나온다.

개발 환경에서는 구조가 완벽하게 동작했다. 하지만 실제 사용자가 설치하자 테스트에서 잡히지 않던 여섯 가지 문제가 튀어나왔다. 이 글은 그 사례를 통해 보안 컴포넌트와 배포 패키징, CSS 격리에서 무엇을 조심해야 하는지 보여준다.

첫 번째 버그는 다른 사람 환경에는 존재하지 않던 Worker 파일이다. 작성자 머신에서는 잘 동작했지만, 첫 사용자 환경에서는 입력창이 비어 보였다. 원인은 Worker를 절대 경로로 생성해 놓았기 때문이다. 작성자의 Vite 개발 저장소에서는 경로가 맞았지만 npm 소비자 환경에서는 파일이 node_modules 안에 있어 dev server가 그 절대 경로를 서빙할 수 없었다. 해결책은 Worker를 빌드 시점에 blob URL로 인라인해 번들 안에 포함시키는 것이었다. 여기서 얻은 교훈은 패키징 실수가 곧 보안 사고로 이어질 수 있으니 publish 전에 npm pack --dry-run으로 실제 배포 산출물을 꼭 확인해야 한다는 점이다.

두 번째 버그는 모노스페이스 폰트가 숨긴 문제였다. FieldShield는 위에 마스크 레이어를 두고, 아래에 투명한 실제 input을 놓는 오버레이 구조다. 커서는 실제 input에 있고, 화면에 보이는 글자는 마스크 레이어에 있다. 개발 중에는 IBM Plex Mono를 썼기 때문에 문자의 폭이 일정해서 x 마스크와 실제 텍스트 폭이 얼추 맞았다. 하지만 첫 소비자가 비례폭 폰트인 Inter를 쓰자 x는 W보다 좁아서 입력 길이가 길어질수록 커서 위치가 눈에 띄게 어긋났다. 해결은 text-align, text-indent, text-transform, font-variant-ligatures, font-kerning, hyphens 같은 상속 가능한 CSS 속성을 명시적으로 리셋하는 것이었다.

세 번째 버그는 ‘유령 placeholder’였다. 소비자는 placeholder가 흐릿하게 겹쳐 보인다고 신고했다. FieldShield는 마스크 레이어의 span과 숨겨진 실제 input의 native placeholder, 두 곳에 placeholder가 존재했다. 작성자는 color: transparent면 native placeholder가 안 보일 거라 생각했지만, 브라우저는 currentColor 위에 자체 opacity를 더해 처리했고, 소비자 폰트가 달라지자 native placeholder와 mask placeholder 위치가 조금씩 달라져 블러처럼 보였다. 해결은 실제 input의 ::placeholder 의사 요소에 color: transparent를 명시하는 것이었다.

네 번째 버그는 세 단계 위에서 내려온 CSS 속성이었다. 어떤 소비자 앱은 #root에 text-align: center가 걸려 있었고, 이 상속 속성이 라이브러리 내부 마스크 레이어까지 전파됐다. 보이는 텍스트는 가운데로 밀렸지만, 실제 input에 묶인 커서는 왼쪽에 남아 정렬이 틀어졌다. 결국 내부 마스크 레이어에서도 상속 가능한 여섯 개 속성을 모두 강제로 리셋해야 했다. 작성자는 이런 오버레이 기반 보안 컴포넌트의 CSS 격리 이슈가 기존 문헌에 거의 정리돼 있지 않다고 지적한다.

다섯 번째 버그는 Vite 4+가 거부한 CSS import였다. README는 소비자에게 dist 경로에서 스타일시트를 직접 import하라고 안내했는데, Vite 4 이상은 package.json의 exports 필드에 명시되지 않은 경로는 실제 파일이 있어도 막아 버린다. 해결은 "./style.css": "./dist/style.css" 같은 CSS export를 package.json에 추가하는 것이었다.

여섯 번째는 Ctrl+Z가 보여 주는 ‘뒤섞인 진실’이다. 사용자가 실행 취소를 누르면 이전 실제 값이 아니라 xxxx만 복원되는 현상이 보고됐다. 작성자는 이건 버그가 아니라 구조적 보안 보장이라고 설명한다. 브라우저의 native undo 히스토리는 DOM 변경만 추적하고, DOM은 애초에 xxxx만 봤기 때문이다. 진짜 값은 Worker 메모리에 있으니 기본 undo 시스템이 접근할 수 없다. 다만 차기 버전에서는 Ctrl+Z를 가로채 Worker 메모리에서 값을 되돌리는 커스텀 undo 스택을 만들 계획이라고 한다.

작성자의 결론은 강하다. 가장 강한 테스트 매트릭스는 단위 테스트가 아니라 실제 사용자 환경이다. CRA, Webpack, Next.js, 각기 다른 폰트와 CSS reset이 들어간 환경에서만 드러나는 문제가 있다. 특히 헬스케어, 핀테크처럼 민감한 데이터를 다루는 React 폼에서는 보안 설계만큼 패키징, 스타일 격리, 실제 소비자 환경 검증이 중요하다.

원문 제목: Container Query Typography Systems 원문 링크: https://mattwaler.com/blog/container-query-typography-systems/ 번역일: 2026-04-19 KST

한국어 번역

이 글은 모든 브라우저에서 container queries를 쓸 수 있게 된 지금, 타이포그래피 시스템도 뷰포트 기준에서 컨테이너 기준으로 옮겨야 한다고 주장한다.

작성자는 그동안 거의 모든 웹 프로젝트에서 h1, h2 같은 제목용 유틸리티 클래스를 만들어 왔다. 예를 들어 Tailwind로 h1은 text-4xl, md:text-5xl, lg:text-6xl, h2는 한 단계 작게 식으로 반응형 스케일을 구성했다. 이런 방식은 전체 사이트 일관성을 유지하기에는 좋지만, 치명적인 약점이 하나 있다. 바로 2단 레이아웃이다. 한쪽에는 이미지, 다른 한쪽에는 본문이 들어가는 흔한 UI에서, 전역 h2 스타일을 그대로 쓰면 그 좁은 칼럼 안에서는 글자가 지나치게 커 보인다.

그렇다고 h2 대신 h3 같은 한 단계 작은 스타일을 쓰면 다른 문제가 생긴다. 데스크톱에서는 괜찮아 보여도, 태블릿 이하에서 2단 레이아웃이 1단으로 쌓일 때는 오히려 글자가 너무 작아지고 위쪽 다른 블록과 스케일 일관성도 무너진다.

작성자가 제안하는 해결책은 container queries와 :has 선택자를 이용해 “타이포그래피가 들어 있는 부모를 컨테이너로 만들고, 제목 크기를 뷰포트가 아니라 그 컨테이너 크기에 반응하게 만드는 것”이다.

예시 코드는 다음 아이디어를 담고 있다.

*:has(.h1),
*:has(.h2),
*:has(.h3),
*:has(.h4),
*:has(.h5),
*:has(.h6) {
  @apply @container;
}

@utility h1 {
  @apply font-extrabold text-4xl @md:text-5xl @lg:text-6xl;
}

@utility h2 {
  @apply font-extrabold text-3xl @md:text-4xl @lg:text-5xl;
}

핵심은 md, lg 같은 스케일 전환 기준이 이제 전체 화면 폭이 아니라 부모 컨테이너 폭이라는 점이다. 그래서 같은 h2 클래스를 써도, 넓은 본문 칼럼에서는 크게 보이고, 좁은 2단 칼럼에서는 자동으로 더 알맞은 크기로 줄어든다. 그리고 레이아웃이 다시 1단으로 쌓이면 컨테이너가 넓어지므로 타이포그래피도 자연스럽게 커진다.

즉 기존 breakpoint 중심 타이포그래피는 “페이지 전체”만 보고 있었지만, container query 기반 접근은 “실제로 텍스트가 놓인 박스의 크기”를 기준으로 본다. 컴포넌트화가 강한 현대 UI에서는 후자가 더 자연스럽다. 카드, 사이드바, 2단 콘텐츠, 재사용 블록처럼 다양한 배치에서 같은 타이포그래피 클래스가 더 일관되게 동작하기 때문이다.

이 글이 던지는 실무 포인트는 단순하다. 반응형 타이포그래피를 여전히 전역 breakpoint에만 묶어두고 있다면, 이제는 컴포넌트 컨텍스트까지 고려해야 한다. container query는 레이아웃뿐 아니라 타입 스케일까지 국소화하는 도구가 될 수 있다.

원문 제목: The Vertical Codebase 원문 링크: https://tkdodo.eu/blog/the-vertical-codebase 번역일: 2026-04-19 KST

한국어 번역

이 글은 components, hooks, types, utils처럼 파일을 ‘종류’별로 나누는 전통적인 수평 구조가 장기적으로는 코드베이스를 망가뜨린다고 주장한다. 대신 무엇을 하는 코드인지, 즉 도메인과 기능 중심으로 묶는 수직 구조를 제안한다.

작성자는 오래전부터 이런 수평 분할을 싫어했다고 말한다. useTheme와 useTodo가 둘 다 hooks라는 이유로 같은 폴더에 있지만, 실제로 더 밀접한 ThemeProvider와는 떨어져 있는 구조는 논리적으로 맞지 않는다. 초기에야 편할 수 있지만, 시간이 지나면 components 폴더에는 수백 개 파일이 쌓이고, 공통점은 단지 “컴포넌트라는 사실”뿐이 된다. Sentry 같은 대형 코드베이스의 최상위 components 디렉터리에 200개 넘는 파일이 쌓인 사례를 들며, 이런 구조는 결국 어디서부터 찾아야 할지조차 모르게 만든다고 지적한다.

작성자는 AI 에이전트 시대에도 이 문제는 여전히 중요하다고 말한다. 오히려 사람과 에이전트 모두 효율적으로 일하려면 경계, 제약, 빠른 피드백 루프가 필요하다. 탐색하기 쉬운 프로젝트 구조, 좋은 lint 규칙과 TypeScript 설정, 빠르고 신뢰할 수 있는 테스트가 있어야 한다. 유기적으로 커진 오래된 코드베이스에서 에이전트가 약한 이유도 바로 이런 경계가 흐리기 때문이라는 주장이다.

핵심 원칙은 인지 부하를 줄이는 것이다. 함께 바뀌는 코드는 함께 살아야 한다. 예를 들어 Widget 컴포넌트의 props 타입을 별도 파일로 빼지 않고 컴포넌트와 같은 파일에 두는 것은 대부분 자연스럽게 받아들인다. 그런데 Widget이 데이터를 가져오기 위해 widgetQueryOptions를 쓰게 되면, 수평 구조에서는 이 함수가 util 취급을 받아 src/utils/widget.ts 같은 곳으로 밀려나기 쉽다. 작성자는 이것이 전혀 확장 가능하지 않다고 본다.

Sentry 예시에서도 analytics 관련 함수가 utils/analytics에 있고, 이를 쓰는 UI는 components/analyticsArea에 있고, profiling 코드는 components, types, utils, views로 갈라져 있다. 이 구조는 응집도를 낮추고 결합도를 높인다. 서로 밀접한 코드가 흩어지고, 아무 컴포넌트나 아무 util을 가져다 쓸 수 있게 되기 때문이다.

대안으로 제시하는 것은 도메인 중심 수직 구조다. widgets와 관련된 것은 컴포넌트든 훅이든 타입이든 상수든 모두 src/widgets 아래에 둔다. 중요한 것은 “기술적으로 무엇인가”가 아니라 “무엇을 하는가”다. 이런 묶음은 실제 조직 구조나 codeowners와도 잘 맞는다. profiling 팀 코드가 src/profiling 아래에 있는 식이다.

물론 문제도 있다. 적절한 vertical을 정하는 일이 쉽지 않다. 라우트나 페이지 단위로 시작하는 것이 좋고, /dashboard 같은 경로가 좋은 출발점이 될 수 있다. 위젯처럼 여러 페이지에서 공통으로 쓰는 개념은 그것 자체를 하나의 vertical로 분리하면 된다. 작성자는 그래서 일부러 ‘feature’보다 더 넓은 ‘vertical’이라는 표현을 쓴다.

또 하나 중요한 것은 경계 설정이다. 관련 코드를 한 폴더로 모은다고 해서 자동으로 결합도가 낮아지진 않는다. 어떤 유틸이 원래 한 기능용으로 만들어졌는데 다른 곳에서 몰래 재사용되기 시작하면 작은 변경도 위험해진다. 그래서 각 vertical에는 외부에 공개할 public interface와 내부에서만 쓰는 private 코드가 필요하다.

작성자는 이상적인 방법으로 모노레포를 제안한다. 각 vertical을 별도 패키지로 두고, package.json의 exports 필드로 공개 인터페이스를 정의하면 된다. 이렇게 하면 vertical 간 의존성이 가능하더라도 명시적이 된다. pnpm workspaces가 여기에 잘 맞고, Nx는 어떤 vertical이 어떤 vertical에 의존할 수 있는지 규칙으로 강제할 수 있다. 모노레포가 아니어도 eslint-plugin-boundaries 같은 도구로 deep import 금지나 내부 모듈 보호 규칙을 만들 수 있다.

물론 공짜 점심은 없다. 어떤 vertical에 넣을지 결정하기 어렵고, private 코드를 두면 팀마다 비슷한 것을 중복 구현할 위험도 있다. 결국 더 많은 팀 간 소통이 필요하다. 하지만 작성자는 그래도 방향은 분명하다고 본다. 코드베이스는 종류별 수평 분할보다 도메인별 수직 분할로 자라야 하며, 그래야 사람도 에이전트도 더 잘 탐색하고 바꿀 수 있다.

frontend-briefing-2026-04-19-KST

한국어 번역 묶음.

포함 파일:

  • 01-mdn-new-frontend.md
  • 02-tanstack-rsc-your-way.md
  • 03-framework-bundle-size-benchmark.md
  • 04-react-security-library-bugs.md
  • 05-container-query-typography-systems.md
  • 06-the-vertical-codebase.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment