Skip to content

Instantly share code, notes, and snippets.

@leedc0101
Created April 15, 2026 02:56
Show Gist options
  • Select an option

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

Select an option

Save leedc0101/bc4ebcc9f7a939005f0bbc4e7de3118d to your computer and use it in GitHub Desktop.
frontend-briefing-2026-04-15-KST

원문 제목: Moving Railway's Frontend Off Next.js


Railway의 프로덕션 프론트엔드는 이제 더 이상 Next.js 위에서 돌지 않는다. 대시보드, 캔버스, marketing 사이트까지 전체를 Vite + TanStack Router로 옮겼고, 이 마이그레이션은 두 개의 PR과 무중단 배포로 끝냈다.

왜 Next.js를 떠났나

Next.js는 Railway가 초기에 빠르게 제품을 만들고 수백만 월간 사용자에게 서비스하는 데 큰 역할을 했다. 하지만 시간이 지나면서 더 이상 제품 특성과 맞지 않게 됐다.

가장 큰 문제는 빌드 시간이다. 프론트엔드 빌드가 10분을 넘기기 시작했고, 그중 약 6분은 Next.js 자체에서 소비됐다. 특히 finalizing page optimization 단계에 시간이 많이 묶였다. 팀이 하루에도 여러 번 배포하는 환경에서 이 정도 빌드 시간은 단순한 불편이 아니라, 매 반복마다 붙는 비싼 세금에 가깝다.

Railway의 앱은 본질적으로 클라이언트 중심이다. 대시보드는 상태가 풍부하고, 캔버스는 실시간이며, 웹소켓 사용도 많다. 하지만 Next.js의 강점인 서버 우선 프리미티브는 거의 활용하지 않았다. 오히려 Pages Router 위에 레이아웃과 라우팅 문제를 해결하려고 별도 추상화를 덧붙여야 했다.

Pages Router도 불편했다. 공통 레이아웃을 구성할 때 모든 패턴이 일종의 우회책이었다. App Router가 일부 문제를 해결해주긴 하지만, 그쪽은 서버 우선 패러다임에 더 강하게 기대고 있다. Railway 제품은 의도적으로 클라이언트 주도 구조를 택하고 있기 때문에, App Router로 가는 일은 필요하지 않은 패러다임에 맞춰 다시 짜는 일에 가까웠다.

왜 TanStack Start + Vite였나

팀이 원한 스택은 실제 개발 방식과 맞는 것이었다. 명시적이고, 클라이언트 중심이고, 반복 속도가 빨라야 했다. 그리고 개발자들이 실제로 이 스택으로 작업하는 걸 좋아해야 했다.

TanStack 쪽이 특히 마음에 들었던 이유는 다음과 같다.

  • 타입 안전한 라우팅을 기본 제공한다.
  • 경로 파라미터와 search 파라미터 추론이 잘 된다.
  • 파일 시스템 기반 라우트 생성이 된다.
  • pathless layout route로 레이아웃을 우아하게 조합할 수 있다.
  • dev loop가 매우 빨라서, 변경 후 결과 확인까지의 지연을 거의 의식하지 않게 된다.
  • 필요한 곳에만 SSR을 쓸 수 있다. 마케팅 페이지, 채용, 체인지로그 같은 곳은 SSR을 쓰고, 나머지는 순수 클라이언트 중심으로 남길 수 있다.
  • 프레임워크 마법이 상대적으로 적고, 내부 동작을 더 명시적으로 제어할 수 있다.

팀원 몇 명이 휴가 기간에 TanStack Start를 시험해봤고 반응은 거의 만장일치였다고 한다. Railway 같은 제품 성격에서는 개발 경험이 벤치마크만큼 중요하다는 판단이었다.

두 개의 PR로 끝낸 마이그레이션

대규모 프로덕션 프론트엔드 이전은 보통 오래 걸리고, 병행 운영과 점진적 전환을 수반한다. 하지만 Railway는 마감이 있었고, 결국 두 개의 PR로 작업을 끝냈다.

첫 번째 PR에서는 Next.js에 묶인 부분을 제거했다. next/image, next/head, next/router를 각각 브라우저 기본 API나 프레임워크 중립 대안으로 바꿨다. 프레임워크 자체는 건드리지 않고, Next.js 의존성만 걷어냈다.

두 번째 PR에서 프레임워크를 실제로 교체했다. 200개가 넘는 라우트를 옮겼고, 기존 page 파일에서 라우팅과 무관한 부분을 먼저 React 컴포넌트로 추출한 뒤 원래 트리 구조를 바탕으로 라우트를 생성했다.

서버 레이어는 Nitro를 붙였다. next.config.js를 Nitro 설정으로 치환하면서 500개가 넘는 redirect, 보안 헤더, 캐시 규칙을 한곳으로 모았다. Next.js가 암묵적으로 제공하던 Node.js polyfill, 예를 들어 Bufferurl.parse 같은 부분도 브라우저 기본 API로 바꾸면서 코드가 더 깔끔해졌다.

배포는 일요일 이른 아침에 병합했고, 팀이 바로 dogfooding에 들어갔다. Discord에서 워룸을 열고 당일에 수정 사항을 연달아 반영했다. 다운타임은 없었다.

무엇을 포기했나

장점만 있었던 건 아니다.

  • 내장 이미지 최적화 기능은 포기했다. 대신 일반 <img>와 Fastly의 edge 이미지 최적화를 사용했다.
  • next-seo, next-sitemap 같은 생태계 일부도 포기했다. 대신 사내의 작은 대체 구현을 만들었다.
  • TanStack Start는 새롭고, 거친 면이 있다. 하지만 방향성이 맞고 유지보수자 대응이 빠르며, Railway는 Vite와 TanStack을 후원할 만큼 이 생태계를 믿고 있다고 밝혔다.

인프라 관점에서 본 변화

Railway는 자신의 프로덕션 프론트엔드를 자사 플랫폼 위에서 그대로 운영한다. PR마다 preview deploy를 만들고, 헬스체크와 무중단 롤아웃도 그대로 사용한다. 프레임워크와 빌드 시스템을 통째로 바꾸는 동안 인프라는 건드리지 않았다.

이제 트래픽 대부분은 Fastly edge가 직접 처리한다. 마케팅 페이지는 캐시되고, 동적 페이지는 필요한 곳에 ISR을 쓴다. 프론트엔드 서버는 대부분 한가한 상태다. Vite의 asset 모델도 이 구조와 잘 맞는다. 모듈 단위 content hash 청크를 쓰기 때문에, billing 관련 코드만 바뀌면 그 청크만 무효화된다. 재방문 사용자는 메가바이트가 아니라 킬로바이트 단위만 다시 받는다.

핵심 메시지

Railway가 전하고 싶은 핵심은 단순하다. 프론트엔드 프레임워크는 반복 속도를 높여야 하고, 인프라는 그 반복을 거의 보이지 않게 만들어야 한다는 것. 이 팀은 Next.js가 나쁜 프레임워크라서 떠난 게 아니라, 자신들의 제품 구조와 속도 요구에 더 잘 맞는 도구가 생겼기 때문에 옮겼다.

빌드는 10분대에서 2분 이하로 줄었고, 개발 서버는 즉시 뜨며, 라우트 경계에서 타입 체크가 강해졌고, 레이아웃은 더 이상 꼼수가 필요 없게 됐다. 결국 이 글은 “무슨 프레임워크가 최고인가”보다, “우리 제품에 맞는 제약과 피드백 루프를 택했는가”에 대한 사례로 읽는 게 맞다.

원문 제목: Under the hood of MDN's new frontend


MDN은 작년에 새로운 프론트엔드를 공개했다. 겉으로 보이는 변화는 디자인 정리 정도였지만, 실제로 더 큰 변화는 내부 프론트엔드 아키텍처를 통째로 갈아엎은 데 있었다. 이 글은 MDN이 왜 기존 React 기반 프런트엔드를 버리고, 웹 컴포넌트와 서버 컴포넌트 중심 구조로 갔는지 설명한다.

기존 MDN 프런트엔드의 문제

기존 프런트엔드는 yari라는 React 앱이었다. 출발은 Create React App이었지만, 실제 요구사항이 커지면서 기본 설정으로는 감당이 안 됐고, 결국 eject 이후 복잡한 Webpack 설정과 해키한 빌드 스크립트가 계속 쌓였다.

CSS도 마찬가지였다. Sass를 많이 쓰다가 CSS 변수 같은 최신 기능을 섞으면서 스타일링 체계가 뒤엉켰다. 스코프가 약하거나 없는 경우도 많아서, 한 컴포넌트 스타일을 수정하면 다른 곳이 깨지는 일이 흔했다. 결국 사용자가 실제로 보지 않는 컴포넌트 스타일까지 포함한 거대한 render-blocking CSS 덩어리를 내려보내야 했다.

하지만 가장 큰 문제는 React 앱이 정적 콘텐츠를 감싸는 껍데기에 가까웠다는 점이다. MDN 문서는 Markdown에서 HTML과 JSON 메타데이터로 생성되는데, React가 이 콘텐츠 내부까지 자연스럽게 이해하지 못했다. 결국 dangerouslySetInnerHTML로 삽입하는 구조가 되었고, 문서 내부에 넣어야 하는 인터랙션은 React가 아니라 일반 DOM API로 따로 붙여야 했다.

이 구조는 유지보수 비용이 컸다. 어떤 인터랙션은 React 버전과 DOM API 버전을 둘 다 유지해야 했고, JSX의 장점도 누리기 어려웠다.

웹 컴포넌트와 Lit로 방향 전환

MDN은 2024년부터 Lit과 웹 컴포넌트를 시험하기 시작했다. 정적인 문서 안의 작은 인터랙션을 React 밖에서 자연스럽게 넣을 수 있는 방법이 필요했기 때문이다.

첫 사례는 Scrimba의 Scrim 임베드였다. 사용자가 실제로 상호작용하기 전까지는 외부 iframe을 로드하지 않고, dialog를 이용해 풀스크린도 지원해야 했다. Lit 기반 커스텀 엘리먼트로 구현하니 상태와 이벤트 관리가 간단했고, 문서 HTML 안에 바로 <scrim-inline> 같은 식으로 삽입할 수 있었다.

MDN 팀은 여기서 중요한 깨달음을 얻었다. 이 정도 규모의 인터랙션은 React보다 Lit 기반 웹 컴포넌트가 더 단순하고 직관적일 수 있다는 것이다.

인터랙티브 예제를 웹 컴포넌트로 재구성

MDN의 “Try it” 예제는 기존에 여러 저장소와 별도 빌드 파이프라인에 흩어져 있었다. 예제를 작성하거나 디버깅하려면 여러 repo를 동시에 맞춰야 했고, 문서와 예제를 함께 미리 보기하는 것도 불편했다.

새 구조에서는 PlayGround의 주요 기능을 잘게 나눈 웹 컴포넌트로 쪼갰다. 예를 들어 CodeMirror 기반 에디터, 콘솔 렌더러, 현재 상태를 렌더링하는 컴포넌트, 상태 전달 컨트롤러를 따로 만들었다. 이 조각들을 조합해 <interactive-example> 웹 컴포넌트를 구성하고, Markdown 문서 안에서 직접 사용할 수 있게 만들었다.

즉, 문서 작성자는 매크로와 코드 블록만 넣으면 되고, 나머지 UI와 상태 연결은 프런트엔드 쪽에서 처리하게 바뀌었다.

React SPA 대신 “섬” 구조로 재설계

MDN 팀은 여기서 더 근본적인 질문을 던졌다. “MDN이 정말 React SPA여야 하나?”

답은 아니었다. MDN 페이지의 대부분은 본질적으로 정적 HTML과 CSS다. 페이지 전체를 클라이언트 자바스크립트로 다시 렌더링할 이유가 크지 않다. 실제로 필요한 것은 페이지 곳곳에 박혀 있는 작은 인터랙션 섬들이다.

그래서 MDN은 페이지 전체를 하나의 SPA로 보는 대신, 정적 HTML을 기본으로 하고 필요한 인터랙션만 웹 컴포넌트로 얹는 구조를 택했다. 이렇게 하면 다음 세 가지 문제가 한 번에 풀린다.

  1. 정적 콘텐츠를 위해 거대한 클라이언트 번들을 보낼 필요가 없다.
  2. “React wrapper”가 문서 내부를 제대로 이해하지 못하는 문제가 사라진다.
  3. 인터랙션은 각각 자기 책임을 갖는 작은 웹 컴포넌트가 된다.

자체 서버 컴포넌트 구조

MDN은 HTML 템플릿을 조립할 때도 컴포넌트 기반 사고를 유지하고 싶어 했다. 그래서 Lit의 HTML 템플릿 리터럴을 이용해 자체적인 서버 컴포넌트 개념을 만들었다. 이 컴포넌트는 Node.js에서 한 번 렌더링되고 끝나므로, 클라이언트 상태 관리나 생명주기 훅이 필요 없다.

중요한 건 이 서버 컴포넌트가 웹 컴포넌트를 포함할 수 있다는 점이다. 예를 들어 상단 네비게이션 서버 컴포넌트는 <mdn-search-button> 같은 웹 컴포넌트를 그냥 HTML 태그처럼 배치할 수 있다.

또 Lit의 SSR 기능을 활용해, 지원 브라우저에서는 Declarative Shadow DOM까지 서버에서 렌더링한다. 즉, 자바스크립트가 로드되기 전에도 구조와 스타일이 먼저 들어와서 레이아웃이 안정적이다.

필요한 CSS와 JS만 로드

MDN의 새 구조에서 특히 인상적인 부분은 “페이지에 실제로 있는 컴포넌트만 로드”하도록 설계했다는 점이다.

각 컴포넌트는 components/<name>/ 폴더 아래에 element.js, element.css, server.js, server.css, global.css 같은 명명 규칙을 따른다. 이 구조 덕분에 페이지 렌더링 후 DOM에 어떤 mdn-* 태그가 있는지 검사해서, 해당 웹 컴포넌트의 JS만 비동기 import로 병렬 로드할 수 있다.

즉, 엔지니어가 “이걸 import해야 하나?”를 신경 쓰지 않아도 된다. 페이지에 그 컴포넌트가 없으면 그 코드도 안 내려간다.

CSS도 비슷하다. 서버 컴포넌트가 실제로 렌더링된 경우만 추적해서, 그 컴포넌트의 CSS 파일만 <head>에 링크로 넣는다. 예전처럼 모든 페이지에 거대한 공통 CSS를 얹는 방식이 아니다.

성능에 대한 관점

작은 CSS, 작은 JS 파일을 여러 개 내려보내면 오히려 비효율적이라고 생각할 수 있다. 하지만 MDN은 HTTP/2, HTTP/3 시대에는 반드시 그렇지 않다고 본다. 병렬 다운로드와 캐시 효율을 생각하면, 작은 파일 여러 개가 오히려 유리할 수 있다.

특히 웹 컴포넌트를 비동기 병렬로 로드하면, 전체 번들을 기다리지 않고 필요한 컴포넌트부터 빨리 인터랙티브해질 수 있다. 변경이 생겨도 해당 컴포넌트 청크만 다시 받으면 되므로 캐시 효율도 높다.

Baseline과 최신 웹 플랫폼 활용

MDN은 자체적으로 참여해온 Baseline 프로젝트를 실제 기술 선택 기준으로 활용했다. Widely Available이면 적극 사용하고, Newly Available이면 팀 내부 논의를 거쳐 progressive enhancement나 polyfill 전략을 정했다.

예를 들어 Custom Elements, Shadow DOM은 널리 지원되므로 기본 전제로 사용했다. Declarative Shadow DOM은 아직 최신 범주이므로, 지원 브라우저에서는 활용하고 비지원 브라우저에서는 global.css 같은 fallback을 둔다.

개발 환경 개선

새 프런트엔드의 가장 큰 내부 개선점 중 하나는 개발 환경이다. 예전에는 로컬 개발 서버를 띄우는 데 2분 정도 걸렸고, SSR을 보려면 별도 명령이 필요했으며, 사소한 변경도 재시작이 필요할 때가 많았다.

새 구조에서는 npm run start 하나로 거의 모든 개발 흐름이 해결되고, 실행까지 2초 정도면 충분하다. 이 속도 향상에는 Rspack이 크게 기여했다. Webpack 호환 API를 제공하면서도 Rust로 작성되어 훨씬 빠르기 때문이다.

핵심 메시지

MDN의 사례는 “웹 플랫폼 자체를 더 신뢰하자”는 방향으로 읽힌다. 무조건 대형 SPA를 쓰는 대신, 정적 콘텐츠는 정적으로 두고, 필요한 인터랙션만 섬처럼 얹고, CSS와 JS도 필요한 만큼만 싣는 구조가 실제로 더 단순하고 빠를 수 있다는 이야기다.

실무적으로 보면 이 글은 세 가지 힌트를 준다.

  • 콘텐츠 중심 서비스라면 SPA가 기본 전제일 필요는 없다.
  • 웹 컴포넌트는 작은 인터랙션 섬에 꽤 현실적인 선택지가 될 수 있다.
  • 번들 최소화는 “개발자가 신경 쓰는 문화”보다 “구조적으로 과잉 로딩이 어렵게 만드는 설계”가 더 강력하다.

원문 제목: Announcing TypeScript 6.0


TypeScript 6.0이 정식 공개됐다. 이번 버전은 단순한 기능 추가 릴리스라기보다, 현재의 TypeScript 5.x 계열과 앞으로 나올 TypeScript 7.0 사이를 이어주는 “브리지 릴리스”에 가깝다. 특히 7.0이 Go 기반 네이티브 구현으로 넘어가기 때문에, 6.0은 그 전환을 준비하는 성격이 강하다.

이번 릴리스의 큰 의미

TypeScript 팀은 6.0을 현재 JavaScript 코드베이스 기반 마지막 메이저 버전으로 설명한다. 이후 7.0은 Go로 작성된 새 컴파일러와 언어 서비스 위에서 동작할 예정이며, 더 빠른 속도와 shared-memory 멀티스레딩을 활용할 수 있다.

즉, 6.0은 “지금 당장 실무에서 쓸 변화”도 있지만, 더 중요한 목적은 7.0으로의 마이그레이션 노이즈를 줄이는 데 있다.

눈에 띄는 기능 변화

1) this를 쓰지 않는 함수의 문맥 민감도 완화

기존 TypeScript는 제네릭 호출에서 메서드 문법으로 작성한 함수의 타입 추론이 예상보다 약하게 동작하는 경우가 있었다. 특히 this를 쓰지 않는데도, 메서드라는 이유만으로 문맥 민감 함수로 취급되어 추론 우선순위가 밀리곤 했다.

6.0에서는 함수 안에서 this를 실제로 사용하지 않으면, 이런 함수를 덜 문맥 민감한 것으로 간주한다. 결과적으로 객체 리터럴 내부 메서드의 타입 추론이 더 자연스럽게 되고, 일부 기존 코드의 애매한 오류가 사라진다.

2) #/ 서브패스 import 지원

Node.js 최신 버전은 imports 필드에서 #/로 시작하는 서브패스 import를 지원한다. TypeScript도 moduleResolution: nodenext 또는 bundler 환경에서 이 패턴을 이해하게 됐다.

이 변화는 경로 별칭을 상대 경로 지옥 없이 더 짧고 명확하게 유지하려는 프로젝트에 꽤 실용적이다.

3) --stableTypeOrdering

7.0은 병렬 타입 체크를 지원할 예정인데, 그러려면 타입과 심볼 정렬이 비결정적으로 흔들리면 안 된다. 이를 위해 7.0에서는 내부 정렬 방식을 더 안정적으로 바꾼다.

6.0의 --stableTypeOrdering 플래그는 이 변경을 미리 체험하게 해준다. 선언 파일 차이 비교나 6.0→7.0 전환 리허설 때 유용하다. 다만 최대 25% 정도 성능 저하가 있을 수 있어서 상시 사용보다는 마이그레이션 진단용에 가깝다.

4) es2025 타깃과 라이브러리 지원

targetlibes2025를 쓸 수 있게 됐다. 덕분에 RegExp.escape, Promise.try, Iterator 관련 기능 등 최신 내장 API 타입을 더 자연스럽게 사용할 수 있다.

5) Temporal, Map upsert 등 최신 표준 타입 반영

Temporal API 타입이 내장되고, Map.prototype.getOrInsert, getOrInsertComputed 같은 upsert 관련 제안도 타입 레벨에서 바로 쓸 수 있게 됐다. 브라우저와 런타임이 따라오고 있는 최신 표준을 TS가 더 빨리 품기 시작한 셈이다.

6) lib.dom에 iterable 관련 타입 통합

이전에는 DOM 컬렉션을 for...of로 돌리려면 dom.iterable을 따로 lib에 넣어야 하는 경우가 많았다. 6.0에서는 dom.iterable, dom.asynciterable 내용이 dom 안으로 사실상 흡수됐다. 자잘하지만 체감 좋은 변화다.

실무에서 더 중요한 건 기본값 변화와 deprecated 항목

이번 버전은 새 기능보다 설정 변화가 실무 영향이 더 크다.

strict 기본값이 true

이제 새 프로젝트 기본은 strict 모드다. 기존에 느슨한 타입 검사를 전제로 하던 설정은 명시적으로 strict: false를 넣어야 한다.

module 기본값이 esnext

ESM이 사실상 표준이 된 흐름을 반영했다.

target 기본값이 최신 연도 ES 버전

현재 기준으로 es2025에 해당한다. 대부분의 런타임이 evergreen라는 현실을 반영한 결정이다.

types 기본값이 []

이 변화가 특히 중요하다. 예전에는 node_modules/@types 아래를 광범위하게 자동 탐색했지만, 이제는 기본적으로 아무 것도 포함하지 않는다. 그래서 process, fs, describe 같은 전역 타입을 기대하던 프로젝트는 types: ["node"], types: ["jest"] 등을 직접 지정해야 한다.

대신 성능 개선 효과는 크다. TypeScript 팀은 이 조정만으로도 코드베이스에 따라 20~50% 빌드 시간이 줄어들 수 있다고 본다.

rootDir 기본값이 tsconfig 위치

기존에는 공통 소스 루트를 추론했지만, 이제는 더 예측 가능하게 tsconfig가 있는 디렉터리를 기준으로 본다. src/ 기반 프로젝트는 rootDir: "./src"를 명시하는 쪽이 안전하다.

deprecated 및 제거 방향

6.0은 많은 오래된 옵션을 사실상 퇴출시키기 시작했다.

  • target: es5 deprecated
  • downlevelIteration deprecated
  • moduleResolution: node/node10 deprecated
  • module: amd | umd | systemjs | none deprecated
  • baseUrl deprecated
  • outFile 제거
  • namespace 용도로 쓰던 module Foo {} 문법 deprecated
  • import assertion의 asserts 문법 deprecated, with로 이동

이 변화는 TypeScript가 “과거 호환성”보다 “현대 JS 생태계의 현실”을 더 강하게 기준 삼기 시작했다는 신호다. 번들러, ESM, evergreen 런타임이 기본이라는 전제다.

실무 적용 포인트

이번 업데이트에서 가장 먼저 점검할 부분은 다음이다.

  1. tsconfig.jsontypes를 명시하고 있는가
  2. rootDir를 명시해야 하는 구조인가
  3. 구형 moduleResolution이나 baseUrl에 기대고 있지 않은가
  4. 7.0 전환 전 --stableTypeOrdering으로 노이즈를 미리 확인할 필요가 있는가

핵심 메시지

TypeScript 6.0은 “새 기능이 많다”기보다 “이제는 오래된 가정들을 내려놓자”는 릴리스다. 브라우저와 런타임은 이미 최신화됐고, 번들러와 ESM은 주류이며, 성능은 더 중요한 문제가 됐다. 그래서 TS도 기본값과 옵션 체계를 그 현실에 맞게 조정하고 있다.

프론트엔드 팀 입장에서는 이번 릴리스를 단순 업그레이드보다 설정 정리 작업의 계기로 보는 게 좋다. 특히 monorepo, 대형 FE 앱, 테스트 타입 의존이 많은 프로젝트라면 types, rootDir, 모듈 해석 전략을 지금 한 번 정리해두는 게 7.0 대비까지 같이 되는 셈이다.

원문 제목: Why AI Sucks At Front End


이 글은 다소 거친 표현으로 시작하지만, 요지는 분명하다. 생성형 AI는 프론트엔드에서 “무난하고 흔한 것”은 꽤 잘 만들지만, 진짜 어려운 문제, 즉 맞춤형 인터랙션, 정교한 레이아웃, 접근성, 성능, 복합 상태를 다루는 순간 급격히 약해진다.

AI가 잘하는 것

글쓴이는 AI가 “평범함을 사랑한다”고 말한다. 이미 인터넷에 널린 패턴을 빠르게 조합하는 일에는 상당히 강하다.

예를 들면 이런 일이다.

  • 흔한 UI 스캐폴딩 만들기
  • 디자인 토큰 옮기기
  • 기능 목록 초안 작성하기
  • 반복적이고 지루한 변환 작업 처리하기

즉, 많이 본 패턴을 빠르게 복제하는 데는 꽤 유용하다. 이 부분은 많은 프론트엔드 실무자도 이미 체감하고 있을 것이다.

AI가 특히 못하는 것

문제는 프로젝트가 “평범함”에서 벗어나는 순간이다.

1) 맞춤형 인터랙션과 bespoke UI

스크롤 기반 애니메이션, 섬세한 마이크로 인터랙션, 프로젝트 고유의 상호작용 설계처럼 학습 데이터에 잘 정형화되지 않은 문제는 AI가 쉽게 무너진다. 존재하지 않는 CSS 문법을 만들거나, 예전 브라우저 시대 감각의 코드를 내놓기도 한다.

2) 레이아웃과 간격 계산

프론트엔드는 단순히 “div를 배치하는 일”이 아니라, 콘텐츠 크기, viewport, 브라우저 차이, 입력 장치, 사용자 선호도에 따라 동적으로 달라지는 렌더링 문제다. 글쓴이는 AI가 수학 자체도 약한데, 이런 동적 레이아웃 계산까지 잘할 리 없다고 본다.

3) 복합 상태와 큰 컴포넌트 수정

컴포넌트가 커지고 상태 조합이 복잡해질수록, AI는 어느 부분을 고쳐야 하는지 자주 놓친다. 첫 번째 답변은 그럴듯한데, 이어지는 수정 요청부터 이전 문맥을 깨먹는 경우가 많다는 지적이다.

4) 접근성과 성능

접근성은 특히 큰 약점으로 꼽힌다. aria-hidden="true" 같은 속성을 무비판적으로 붙이는 식의 얕은 대응이 자주 나온다. 성능도 마찬가지다. 명시적으로 요구하지 않으면 무겁고 과한 구현을 내놓기 쉽다.

왜 이런 일이 생기나

글은 네 가지 이유를 든다.

1) 낡고 평균적인 데이터에 많이 학습됐다

모델은 오래된 해결책, 흔한 템플릿, 개성 없는 구현을 대량으로 학습했다. 그래서 가장 가능성 높은 평균값은 자주 맞히지만, 세련되고 최신이며 프로젝트에 맞는 답은 잘 못 낸다.

2) 렌더링 엔진처럼 “실제로 보지” 못한다

LLM은 브라우저가 아니다. 시각 모델이 붙어도 실제 레이아웃 엔진 수준으로 DOM, CSSOM, box model, fallback, line break, intrinsic sizing을 계산하는 존재는 아니다. 결국 상당 부분은 어둠 속에서 찌르는 셈이다.

3) 아키텍처의 “왜”를 이해하지 못한다

AI는 코드 패턴은 흉내 내지만, 왜 그 제약이 필요한지, 왜 이 상태 모델을 택했는지, 왜 특정 구조를 유지해야 하는지까지는 프롬프트에 충분히 넣어주지 않으면 놓친다.

4) 실행 환경을 통제하지 못한다

HTML/CSS는 런타임 환경이 지나치게 다양하다. 브라우저 종류, 브라우저 버전, 화면 크기, 입력 방식, 사용자 접근성 설정, 테마, 언어 방향성까지 다 변수다. Rust, Python, TypeScript처럼 상대적으로 통제된 환경에 비해, 프론트엔드는 변수 폭이 훨씬 넓다.

인간이 여전히 중요한 이유

글쓴이가 말하는 핵심은 “프론트엔드는 인간 문제를 기술로 번역하는 작업”이라는 점이다. 우리는 취향, 맥락, 의도, 기대, 접근성, 심리적 피드백까지 고려한다. 이건 단순히 HTML/CSS 코드를 생성하는 것보다 훨씬 넓은 문제다.

AI는 주류적인 평균 해답에는 도움을 줄 수 있다. 하지만 제품의 개성과 품질을 결정하는 마지막 20%에서는 아직 많이 부족하다. 특히 실무에서 중요한 건 “동작하느냐”보다 “이게 왜 이런 방식이어야 하느냐”인데, 그 부분은 여전히 사람이 책임져야 한다.

실무적으로 읽는 포인트

이 글을 과격한 AI 비판으로만 볼 필요는 없다. 오히려 프론트엔드 팀에 주는 메시지는 꽤 현실적이다.

  • AI는 보일러플레이트와 반복 작업에서 적극 활용하자.
  • 하지만 레이아웃, 접근성, 인터랙션, 성능, 상태 모델링은 검토 비용을 전제로 써야 한다.
  • “한 번에 다 해줘”보다 좁고 명확한 단위로 시키는 게 낫다.
  • 디자인 의도와 제약을 텍스트로 더 구체적으로 전달해야 한다.

핵심 메시지

프론트엔드는 단순 코드 생성 문제가 아니라, 브라우저라는 혼란스러운 실행 환경 속에서 인간 경험을 설계하는 일이다. 그래서 AI는 평균적인 답은 잘 주지만, 좋은 제품 경험을 만드는 데 필요한 정밀함과 의도를 아직 충분히 갖고 있지 않다.

결국 AI는 프론트엔드에서 “대체자”보다 “가속기”에 가깝다. 빠르게 시작하게 해주지만, 마지막 품질과 방향성은 여전히 사람이 잡아야 한다.

원문 제목: The Business Case for Vanilla JS


이 글은 프론트엔드 프레임워크를 무조건 쓰는 관성을 다시 생각해보자는 주장이다. 글쓴이는 이제 생산 환경에서 몇 달 이상 유지될 웹 앱이라면, 오히려 Vanilla JS와 브라우저 API를 직접 쓰는 편이 더 실용적일 수 있다고 본다.

왜 Vanilla JS인가

글쓴이의 핵심 논리는 유지보수성이다. 브라우저 API는 계속 좋아지고 있고, 비교적 하위 호환성도 잘 지켜진다. 반면 특정 JS 프레임워크에 크게 의존하면, 빠르게 변하는 생태계 변화에 애플리케이션 전체가 흔들릴 수 있다.

Vanilla JS는 문서화도 잘 되어 있다. 사람에게도 읽기 쉽고, LLM에게 코드 생성을 시킬 때도 오히려 더 안정적일 수 있다. 빌드 도구, source map, transpiler, 프레임워크 특유의 개념을 계속 추적하지 않아도 된다는 점도 장점으로 든다.

글쓴이가 생각을 바꾼 계기

글쓴이는 예전에는 일반적인 프론트엔드 “정석”을 따르며 React 같은 도구를 썼다. 하지만 촉박한 일정 속에서 특정 기능을 React 세계관에 우겨넣을 시간이 없었고, 그냥 직접 DOM을 만지는 JS를 작성해봤다. 결과는 의외로 괜찮았고, 심지어 더 쉬웠다고 말한다.

나중에 다시 HTML 리포트를 만들어야 할 때도 처음엔 Preact를 썼지만, hydration, component tree 같은 개념에서 피로감을 느꼈고, 다시 브라우저 플랫폼 자체로 돌아갔다. 그리고 또 한 번 더 “이게 더 낫다”는 결론에 도달했다.

React를 어떻게 보나

글쓴이는 React를 DOM 위의 더 높은 추상화라고 보기 어렵다고 말한다. 기술적으로는 추상화가 맞지만, 실제 체감은 “더 높은 수준”이라기보다 “다른 방식의 추상화”에 가깝다는 것이다.

React에는 자체 디버거, 컴파일러, 린터, 상태 모델, 생명주기적 사고방식이 붙는다. 그런데 이 추상화는 자주 새기 쉽다. declarative라고 말하지만, 현실에서는 hook 상태 추적, useRef, 렌더 타이밍, 재렌더 원인 추적 등으로 곧장 구현 세부사항이 드러난다.

즉, 학습해야 할 개념 수가 줄어드는 게 아니라 바뀌는 셈이다.

왜 큰 앱에서 오히려 플랫폼이 나을 수 있나

글쓴이는 복잡한 웹 앱일수록 React가 항상 더 적합하다는 주장에도 회의적이다. 규모가 커질수록 추상화의 누수가 더 많이 드러나고, 결과적으로 팀이 이해해야 할 정신 모델이 늘어난다고 본다.

물론 어떤 기술이든 복잡성은 생길 수 있다. 하지만 그렇다면 왜 더 느리고 덜 안정적이며, 플랫폼 자체보다 수명이 짧을 수 있는 추상화에 더 강하게 의존해야 하느냐는 질문이다.

이 글을 실무적으로 읽는 방법

이 글은 “모든 프레임워크를 버려라”라기보다, 프레임워크 채택의 기본값을 다시 점검하자는 제안으로 읽는 편이 좋다.

특히 다음 같은 경우에는 설득력이 있다.

  • 인터랙션이 복잡하지 않은 내부 툴
  • 문서, 리포트, 관리 화면 같은 콘텐츠 중심 UI
  • 긴 수명과 낮은 의존성 리스크가 중요한 프로젝트
  • 빠른 부트업과 단순한 디버깅이 중요한 팀

반대로 상태 복잡도, 팀 규모, 생태계 통합, 설계 시스템, 라우팅/데이터 패칭 규칙 통일 같은 요구가 크다면 프레임워크가 여전히 유효할 수 있다.

핵심 메시지

이 글의 핵심은 “브라우저는 생각보다 이미 충분히 강력하다”는 점이다. 프레임워크는 많은 경우 도움이 되지만, 그 자체가 목적이 되면 유지보수 부담과 추상화 비용이 쌓인다.

프론트엔드 실무자 입장에서는 기술 선택 시 이렇게 자문해볼 만하다.

  • 이 문제를 브라우저 기본 기능으로 풀 수 없는가?
  • 이 프레임워크가 줄여주는 복잡성이, 새로 도입하는 복잡성보다 큰가?
  • 몇 년 뒤 유지보수 비용까지 생각했을 때도 이 선택이 유리한가?

결국 좋은 선택은 “가장 현대적인 도구”가 아니라, 제품과 팀의 제약에 가장 잘 맞는 도구다. 이 글은 그 기준을 다시 플랫폼 쪽으로 기울여보자는 주장이다.

원문 제목: '"one" | "two" | string' autocomplete TypeScript trick


이 글은 짧지만 실무적으로 꽤 유용한 TypeScript 팁을 소개한다. 목표는 간단하다. “특정 문자열 리터럴을 추천해주되, 결국은 아무 문자열이나 허용하고 싶다”는 요구를 타입으로 표현하는 방법이다.

예를 들어 HTTP 메서드를 받는 함수에서 다음처럼 쓰고 싶을 수 있다.

type HTTPMethod = "GET" | "POST" | string

직관적으로는 GET, POST 자동완성과 함께 다른 문자열도 허용될 것 같지만, 실제로는 그렇지 않다.

왜 그냥 안 되나

TypeScript 입장에서는 "GET" | "POST" | string은 결국 string과 같다. 더 넓은 타입인 string이 유니온을 흡수해버리기 때문이다. 그래서 IDE 자동완성 시점에는 리터럴 정보가 사라진다.

즉, 아래 코드는 타입상 그냥 문자열 하나를 받는 것과 차이가 없다.

type HTTPMethod = "GET" | "POST" | string

해결법: (string & {})

트릭은 string을 완전히 동일한 string처럼 보이지 않게 만드는 것이다. 그때 쓰는 패턴이 바로 (string & {})다.

type HTTPMethod = "GET" | "POST" | (string & {})

이렇게 쓰면 타입 체커는 리터럴 유니온과 “약간 다른 string”이 같이 있는 것으로 인식한다. 결과적으로 IDE 자동완성에서는 GET, POST 같은 추천이 살아 있고, 동시에 사용자는 다른 임의의 문자열도 넣을 수 있다.

doHTTP("GET")
doHTTP("POST")
doHTTP("not-an-http-method")

세 경우 모두 허용된다.

이미 널리 쓰이는 패턴

이건 완전히 비공식 꼼수만은 아니다. type-fest 패키지는 아예 이 용도를 위한 LiteralUnion 타입을 제공한다.

import type { LiteralUnion } from 'type-fest'

type Pet = LiteralUnion<'dog' | 'cat', string>

이 타입을 쓰면 dog, cat 자동완성은 살리면서도, 그 외 문자열 입력도 막지 않을 수 있다.

언제 유용한가

프론트엔드 실무에서 이런 패턴은 생각보다 자주 등장한다.

  • HTTP 메서드 문자열
  • 디자인 토큰 key
  • 이벤트 이름
  • variant 이름
  • CSS 유틸리티 값이나 내부 DSL 형태 문자열
  • API 옵션 키 중 “추천 값은 있지만 확장도 허용해야 하는” 경우

예를 들어 design system 컴포넌트에서 기본 variant는 추천하되, downstream 팀의 확장 variant도 허용하고 싶을 때 꽤 잘 맞는다.

주의할 점

이 패턴은 타입의 표현력과 IDE 경험을 좋게 만들지만, 런타임 검증을 대체하지는 않는다. 사용 가능한 값이 실제로 제한되어야 하는 도메인이라면, 타입 추천만으로 끝내지 말고 런타임 validation도 붙이는 편이 맞다.

또 팀원에게 맥락 없이 보여주면 “왜 string & {} 같은 이상한 걸 쓰지?”라는 반응이 나올 수 있다. 실무에 넣는다면 별도 유틸 타입으로 감싸두는 편이 더 읽기 좋다.

type SuggestedString<T extends string> = T | (string & {})

핵심 메시지

이 팁의 본질은 “타입 안전”보다 “개발 경험 개선”에 있다. 완전히 닫힌 유니온은 아니지만, IDE 자동완성을 통해 추천값을 보여주고 싶을 때 매우 유용하다.

작은 팁이지만, 컴포넌트 API나 FE 라이브러리 설계에서 자주 마주치는 문제를 깔끔하게 해결해준다. 프론트엔드에서 TypeScript를 단순 오류 방지 도구가 아니라, 사용성 높은 API 설계 도구로 쓰고 있다면 기억해둘 만한 패턴이다.

frontend-briefing-2026-04-15-KST

2026-04-15 KST 기준 프론트엔드 데일리 브리핑 번역 모음.

포함 아티클:

  1. Moving Railway's Frontend Off Next.js
  2. Under the hood of MDN's new frontend
  3. Announcing TypeScript 6.0
  4. Why AI Sucks At Front End
  5. The Business Case for Vanilla JS
  6. '"one" | "two" | string' autocomplete TypeScript trick
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment