Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

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

Select an option

Save leedc0101/1b71b3f7f132bd9ce7bf57af89d590d0 to your computer and use it in GitHub Desktop.
frontend-briefing-2026-03-30-KST

You still have to refactor, even with AI

AI 에이전트가 형편없는 코드를 만든다거나, 반대로 AI를 안 쓰면 뒤처진 엔지니어라는 식의 극단적인 주장들이 인터넷에 넘쳐난다. 하지만 이런 논쟁에는 중요한 착각이 하나 있다. AI가 인간 엔지니어의 역할을 없애준다고 보는 것이다. 실제로는 그 반대에 가깝다. AI는 소프트웨어 엔지니어링을 없애지 않는다. 다만 엔지니어가 코드를 직접 다루는 방식에서, 아키텍처와 변화의 흐름을 다루는 방식으로 관점을 옮기게 만든다.

소프트웨어가 현재 형태로는 더 이상 요구사항을 감당하지 못하는 순간이 오면 우리는 그것을 리팩터링이라고 부른다. 이 기본 사실은 AI 시대에도 바뀌지 않는다.

초기에는 저자도 에이전트를 자신의 기존 워크플로를 보조하는 도구로 썼다. 자동완성보다 조금 더 강한 보조 장치 정도로 본 것이다. 그런데 1년 남짓 지나면서 에이전트가 더 복잡한 프로토타입까지 빠르게 만들 수 있게 되자, 다른 문제가 드러났다. 처음엔 속도가 엄청나지만 시간이 지나면 코드베이스가 자기 무게를 못 이기고 무너진다. 여기저기 중복이 생기고, 구조가 흐트러지고, 점점 유지보수 불가능한 덩어리가 된다.

이 지점에서 흔히 “봐라, 결국 AI가 만든 코드는 엉망이다”라는 결론에 도달하기 쉽다. 저자도 한동안 그렇게 생각했다. 하지만 나중에 보니 문제는 AI 자체보다도, AI를 바라보는 프레임에 있었다. 에이전트를 나를 보조하는 존재로만 보면 성능이 제한된다. 지금의 엔지니어 역할은 에이전트를 보조하는 쪽에 더 가깝다.

내가 세상에 존재했으면 하는 소프트웨어가 있다면, 내 일은 비결정적인 모델이 처음부터 모든 판단을 완벽히 하길 기대하는 게 아니다. 대신 더 나은 결과가 나오도록 시스템을 겹겹이 설계하는 것이다. 에이전트를 속이는 게 아니라, 에이전트가 내 요구사항과 품질 기준을 만족하도록 환경과 피드백 루프를 만드는 일이다. 다시 말해 “기계를 만드는 기계”를 만드는 일이다.

저자는 최근 실제 사례를 든다. DigitalOcean의 S3 호환 오브젝트 스토리지를 백엔드로 쓰기 위해 엔드포인트 설정을 추가해야 했고, Claude에게 구현을 맡겼다. 첫 구현은 겉보기엔 됐지만 실제 테스트를 해보니 아주 취약했다. 이유를 보니 코드베이스 곳곳에 S3 클라이언트를 생성하는 코드가 열 군데나 있었고, 에이전트는 그중 네 군데를 놓쳤다.

그럴 때 해야 하는 일은 단순하다. 에이전트에게 리팩터링을 시키면 된다. 코드가 잘못 분해돼 있는 “이음새(seam)”를 찾아내고, 그 구조를 다시 잡아야 한다. 이 과정 자체는 예전과 다르지 않다. 달라진 점은 속도다. 에이전트가 소프트웨어를 엄청난 속도로 생산하기 때문에, 리팩터링 루프와 아키텍처 수정 루프 역시 훨씬 짧아진다.

문제는 기존의 소프트웨어 개발 프로세스가 보통 “사람이 코드를 충분히 이해하고 있다”는 가정 위에 세워져 있다는 점이다. 코드를 일일이 읽고 수정하면서 느껴지는 마찰이 사라지면, 무엇을 리팩터링해야 하는지 어떻게 감지할 것인가? 변경 속도가 너무 빨라지면 동료 리뷰어들은 어떻게 따라갈 것인가? 예전 프로세스 그대로는 버티기 어렵다.

그래서 저자는 프로세스 자체를 다시 설계해야 한다고 말한다. 아키텍처 가이드나 스킬 문서에 어떤 안티패턴을 막아야 하는지 더 명확히 적어야 할 수도 있다. 매일 자동으로 적대적 코드 리뷰를 돌려서 단순한 DRY 리팩터링 후보를 찾게 할 수도 있다. 그리고 이런 자동 수정이 제품을 망치지 않도록 외부 블랙박스 테스트를 더 강하게 세워야 한다.

인간이 결국 마주하는 문제는 예나 지금이나 같다. 소프트웨어가 바꾸기 어렵고, 지나치게 취약하고, 요구사항 변화에 충분히 유연하지 않다는 것이다. 이것을 “AI는 좋은 코드를 못 쓴다”는 증거로 볼 수도 있지만, 저자는 시선을 바꾸자고 제안한다. 인간의 일은 점점 더 에이전트가 더 나은 결과를 내도록 이끄는 일, 즉 시스템과 루프를 개선하는 일로 이동하고 있다는 것이다.

AI 에이전트가 코드를 썼다고 해서 좋은 소프트웨어 엔지니어링의 기본기가 사라지는 건 아니다. 오히려 더 빨리, 더 자주, 더 체계적으로 수행해야 한다. 아키텍처를 더 많이 생각해야 하고, 리팩터링에 더 집요해야 하며, 입력과 출력의 품질을 보장하는 적대적 검증 루프를 더 많이 만들어야 한다. 기본은 그대로다. 다만 AI 덕분에 모든 일이 훨씬 빠르게 벌어질 뿐이다.

The Three Pillars of JavaScript Bloat

지난 몇 년 동안 e18e 커뮤니티가 커지면서 성능과 의존성 정리에 대한 논의도 훨씬 활발해졌다. 특히 “cleanup” 흐름 덕분에 더 이상 필요 없거나, 오래됐거나, 관리되지 않는 패키지들을 의존성 트리에서 걷어내려는 움직임이 커졌다. 이 글은 그 과정에서 계속 등장하는 “자바스크립트 비대화” 문제를 세 가지 축으로 정리한다.

1. 오래된 런타임 지원, 과도한 안전장치, 크로스-리얼름 호환성

npm 트리를 들여다보면 “이 정도는 네이티브로 할 수 있지 않나?” 싶은 작은 유틸 하나 밑에 더 작은 유틸들이 줄줄이 매달린 경우가 많다. 예를 들어 is-string, hasown 같은 패키지다. 왜 이런 일이 생길까?

첫째는 아주 오래된 엔진 지원 때문이다. ES3 수준 환경에서는 forEach, reduce, Object.keys, Object.defineProperty 같은 ES5 기능이 없다. 이런 환경을 지원하려면 직접 구현하거나 폴리필을 깔아야 한다.

둘째는 전역 객체 오염을 방어하려는 안전장치 때문이다. Node 내부의 primordials처럼, 어떤 코드가 전역 Map이나 Math를 오염시켜도 런타임이 망가지지 않도록 원본 참조를 따로 잡아두는 식이다. 일부 라이브러리 작성자들도 이 철학을 패키지 레벨까지 끌고 와서, 단순한 Math 함수 접근조차 별도 의존성으로 분리한다.

셋째는 크로스-리얼름 값 처리다. 브라우저 본문과 iframe은 서로 다른 리얼름이기 때문에, iframe에서 만든 RegExp는 부모 창의 RegExp와 같지 않다. 이 경우 instanceof가 깨진다. 테스트 러너나 iframe 환경을 고려해야 하는 라이브러리라면 Object.prototype.toString.call 같은 우회가 필요할 수 있다.

문제는 이런 요구가 필요한 사람은 아주 소수라는 점이다. 대부분의 개발자는 최근 10년 이내 Node 버전이나 evergreen 브라우저 위에서 일한다. 프레임 간 값 비교도 잘 안 하고, 전역 오염을 일으키는 패키지는 보통 제거해버린다. 그런데도 소수의 특수 요구가 “일반적인 핫패스” 안으로 들어와서 모두가 비용을 부담하고 있다는 게 글의 핵심 비판이다.

2. 원자 단위로 쪼갠 아키텍처

일부 개발자는 패키지를 가능한 한 미세한 단위까지 쪼개야 재사용성이 올라간다고 믿는다. 그래서 사실상 한 줄짜리 정규식, 배열 변환, 경로 치환, PATH 키 판별 같은 코드가 각각 별도 패키지로 존재한다.

이론상으로는 이런 작은 블록들을 조합해 더 높은 수준의 도구를 쉽게 만들 수 있다. 하지만 현실에서는 이 패키지들이 재사용 가능한 블록으로 잘 기능하지 못하는 경우가 많다.

첫 번째 문제는 단일 소비자 패키지다. 예를 들어 어떤 패키지가 사실상 같은 작성자의 다른 패키지 하나에서만 쓰인다면, 코드 인라인과 다를 바가 없는데도 npm 요청, tar 압축 해제, 버전 해석, 설치 메타데이터 등 추가 비용은 그대로 발생한다.

두 번째 문제는 중복이다. 예를 들어 대형 프로젝트의 의존성 트리를 보면 is-docker, is-wsl, path-key 같은 아주 작은 도구들이 여러 버전으로 중복 설치돼 있다. 이런 로직을 코드에 직접 포함하면 중복 자체는 남을 수 있어도, 설치 비용과 버전 충돌 비용은 훨씬 줄어든다. 인라인 중복은 싸지만, 패키지화된 중복은 비싸다.

세 번째 문제는 공급망 공격면 확대다. 패키지가 많을수록 유지보수 실패, 보안 사고, 계정 탈취 같은 리스크 지점이 늘어난다. 실제로 과거 일부 유명 유지보수자의 계정이 침해되면서 수백 개의 소형 패키지가 함께 오염된 적이 있었다. 단순한 Array.isArray(val) ? val : [val] 같은 로직은 굳이 별도 패키지일 필요가 없다는 주장이다.

3. 역할을 다하고도 남아버린 포니필(ponyfill)

앱 개발에서는 아직 엔진이 지원하지 않는 미래 기능을 쓰기 위해 폴리필이 필요할 수 있다. 하지만 라이브러리는 소비자 환경을 임의로 오염시키면 안 되기 때문에, 대신 import해서 쓰는 “포니필”을 선택하는 경우가 있다.

포니필은 한때 유용했다. 네이티브 지원이 없을 수도 있는 기능을 안전하게 도입할 수 있었기 때문이다. 문제는 네이티브 지원이 충분히 보편화된 뒤에도 그 포니필이 제거되지 않는다는 점이다.

예를 들어 globalThis, Array.prototype.indexOf, Object.entries 같은 기능은 이미 오래전부터 널리 지원되는데도, 이를 대체하는 포니필 패키지들이 여전히 수백만~수천만 다운로드를 기록한다. 일부는 1번의 오래된 환경 지원 때문에 남아 있을 수 있지만, 많은 경우는 그냥 “생각 없이 계속 남아 있는” 상태다.

글의 제안은 단순하다. 우리가 실제로 지원하는 모든 LTS 엔진이 이미 기능을 제공한다면, 포니필은 제거해야 한다.

그럼 무엇을 할 수 있을까?

이 비대화는 이미 의존성 트리 깊숙이 박혀 있어서 하루아침에 해결되진 않는다. 그래도 시작은 할 수 있다.

  • “왜 이 패키지를 쓰고 있지?”를 의식적으로 묻기
  • 필요 없어 보이면 유지보수자에게 제거 가능 여부를 이슈로 제안하기
  • 더 가벼운 대안이 있는지 살펴보기
  • 직접 의존성부터 먼저 정리하기

글은 구체적 도구도 소개한다.

knip

사용하지 않는 의존성, 죽은 코드 등을 찾아내는 데 유용하다. 모든 문제를 해결하진 못하지만 의존성 청소의 출발점으로 좋다.

e18e CLI

replace 가능한 의존성을 찾아내고, 일부는 자동 마이그레이션도 지원한다. 예를 들어 chalk를 더 가벼운 대안이나 네이티브 기능으로 바꾸는 식이다. 앞으로는 실행 환경에 따라 네이티브 API를 추천하는 방향까지 기대할 수 있다고 한다.

npmgraph

의존성 트리를 시각화해서 어디서 비대화가 생기는지 파악하기 좋다. 어떤 작은 기능 하나를 위해 굳이 5~6개의 패키지가 딸려오는지 확인하고, 더 단순한 대안을 찾는 데 도움이 된다.

module-replacements

네이티브 기능으로 대체 가능하거나, 더 나은 대안이 있는 패키지를 커뮤니티가 정리해두는 데이터셋이다. codemod 프로젝트와 함께 쓰면 실제 교체도 빨라진다.

마무리

이 글의 핵심은 “소수의 예외적 요구를 위해 모두가 계속 비용을 부담하고 있다”는 문제의식이다. 과거에는 이런 패키지 구조가 합리적이었을 수 있다. 당시 플랫폼은 지금보다 훨씬 빈약했고, 호환성 문제도 컸다. 하지만 이제는 환경이 달라졌는데도 습관과 역사 때문에 비대화된 트리를 계속 끌고 가고 있다.

저자는 이를 뒤집자고 말한다. 특수한 호환성과 구조가 정말 필요한 소수만 그 비용을 지고, 대부분의 개발자는 더 현대적이고 가볍고 널리 지원되는 코드 경로를 기본값으로 써야 한다는 것이다. 그리고 그 변화는 결국 각 개발자가 자기 의존성에 “왜?”라고 묻는 데서 시작된다.

When All You Can Do Is All or Nothing, Do Nothing

저자는 최근 몇 년간 디자인 시스템 관점에서 웹 성능을 설계하는 일을 많이 해왔고, 그 과정에서 한 가지 실무적 조언을 정리한다. 핵심은 아주 단순하다.

할 수 있는 선택지가 “전부 아니면 전무”뿐이라면, 차라리 아무것도 하지 말라.

문맥은 이렇다. 어떤 조직의 디자인 시스템이 매우 자유로운 CMS 위에 올라가 있으면, 같은 컴포넌트가 어떤 페이지에서는 히어로 영역에, 어떤 페이지에서는 폴드 아래쪽에, 또 다른 페이지에서는 반복 리스트 안에 렌더링될 수 있다. 이럴 때 loading=lazy 나 fetchpriority=high 같은 성능 힌트를 컴포넌트 자체에 기본값으로 박아두면 오히려 역효과가 날 수 있다.

힌트는 “마법의 속도 향상 스위치”가 아니다

loading=lazy 는 사용자가 아직 필요로 하지 않는 리소스에 적용될 때만 도움이 된다. 지금 당장 필요한 이미지에 붙이면 페이지가 더 느려질 수 있다.

fetchpriority=high 역시 마찬가지다. 네트워크에서 가장 먼저 챙겨야 할 후보 하나를 명확히 지목할 수 있을 때만 의미가 있다. 상단에 있을지도 모르는 이미지 여러 장 모두에 high를 붙이면, 우선순위를 주는 게 아니라 브라우저에 잡음을 던지는 셈이 된다. 모두가 중요하면, 아무도 중요하지 않다.

즉, 이 속성들은 “넣으면 무조건 빨라지는 옵션”이 아니라 문맥이 분명할 때만 유효한 힌트다.

멍청한 디자인 시스템이 더 안전할 때가 있다

저자는 디자인 시스템이 실제로 알지 못하는 것을 아는 척하면 안 된다고 말한다. 예를 들어 카드 컴포넌트의 이미지에 기본으로 loading=lazy 를 넣어두는 건, 그 카드가 항상 그리드 하단이나 화면 밖에만 나온다는 확신이 있을 때만 안전하다.

하지만 CMS 사용자가 그 카드를 다음과 같이 쓸 수 있다면 어떨까?

  • LCP 후보인 히어로 카드로 사용
  • 상단 첫 컴포넌트로 사용
  • 두 장 중 하나는 폴드 위, 다른 하나는 폴드 아래인 배치에 사용
  • 진짜로 화면 밖에 있는 위치에 사용

이 경우 디자인 시스템은 그 위치를 정확히 모르기 때문에 추측하면 안 된다. 이런 상황에서는 loading=lazy 를 제거하고 브라우저 기본 동작에 맡기는 편이 더 안전하다.

그 결과 폴드 아래 이미지 몇 장이 다소 이르게 로드될 수는 있다. 하지만 LCP 후보를 실수로 lazy-load 해버리는 것보다 훨씬 낫다.

저자는 또 하나 중요한 보충을 한다. loading=lazy 는 단순히 “폴드 아래”를 뜻하지 않는다. 화면 안에 있어도 지금 당장 필요 없으면 lazy가 적절할 수 있다. 예를 들어 캐러셀의 두 번째 이후 슬라이드, 초기화 전까지 의미가 없는 숨은 메뉴 안 이미지, 큰 갤러리의 썸네일 등이 그렇다. 핵심 기준은 위치가 아니라 즉시 필요 여부다.

fetchpriority 역시 확실할 때만 써라

큰 이미지 컴포넌트가 하나 있고, 그게 분명 페이지의 LCP 후보라는 걸 안다면 fetchpriority=high 는 훌륭한 힌트다. 하지만 CMS가 비슷한 이미지 블록 여러 개를 페이지 상단에 놓을 수 있게 허용한다면, 그중 무엇이 진짜 우승 후보인지 시스템은 모른다.

이 상태에서 모든 후보에 high를 붙이면 브라우저가 이미 하고 있던 우선순위 판단을 방해하게 된다. 브라우저는 원래도 어떤 요청이 더 중요한지 최대한 알아서 계산한다. 시스템이 정말 “이게 1순위”라고 말할 자신이 없으면, 차라리 입을 다무는 게 낫다.

브라우저 기본값은 실패가 아니다

저자는 “아무것도 하지 않는 것”을 태만으로 보지 않는다. 몇 년 전만 해도 loading, fetchpriority 같은 도구 자체가 없었다. 이 속성들을 빼는 것은 퇴보가 아니라, 브라우저의 기본 발견/요청/우선순위 알고리즘으로 되돌아가는 것이다. 특히 시스템이 애매한 상황에서는 이것이 가장 솔직하고 해가 적은 기준선이 된다.

세 가지 선택지가 있는데,

  1. 맞는 일을 하기
  2. 틀린 일을 하기
  3. 아무것도 하지 않기

이 중에서 첫 번째를 할 수 없을 때는, 두 번째보다 세 번째가 낫다는 논리다.

놓친 최적화가 잘못된 최적화보다 안전하다

이미지 하나를 lazy-load 하지 않아서 조금의 성능 이득을 놓칠 수는 있다. 하지만 LCP 이미지를 잘못 lazy-load 하면 페이지를 적극적으로 망치게 된다. fetchpriority도 마찬가지다. 진짜 히어로 이미지에 high를 안 주면 약간 늦을 수 있지만, 후보 여러 개에 모두 high를 주면 유용한 힌트를 노이즈로 바꾸게 된다.

즉, 잘못된 최적화는 해롭고, 놓친 최적화는 대개 보수적인 손해에 그친다. 브라우저에는 여전히 안전망이 있다.

확실성이 생길 때만 힌트를 적용하라

이 글은 loading=lazy 나 fetchpriority=high 를 쓰지 말자는 글이 아니다. 다음처럼 시스템이 충분히 알고 있을 때는 과감히 쓰라고 한다.

  • 문단 3 이후 본문 이미지는 항상 폴드 아래다
  • 홈페이지 히어로는 언제나 첫 번째다
  • 캐러셀 첫 슬라이드 뒤는 항상 오프스크린이다

이 정도 문맥이 있다면 정밀하게 힌트를 줄 수 있다. 하지만 그 수준의 확신이 없다면, 함부로 기본값으로 박아 넣지 말라.

결론

디자인 시스템 레벨에서는 때로 가장 덜 똑똑한 선택이 가장 안전하다. 전부 lazy-load 할 수밖에 없거나, 아무것도 lazy-load 하지 않는 선택지만 있다면 후자를 택하라. 여러 후보 모두를 high priority로 찍어야 하는 상황이라면 아무도 찍지 마라.

영원히 아무것도 하지 말라는 뜻은 아니다. 시스템이 더 많은 문맥을 이해하게 되기 전까지는, 보수적으로 브라우저에 맡기는 것이 낫다는 뜻이다. 결국 이 글의 메시지는 하나로 요약된다.

전부 아니면 전무뿐이라면, 의도적으로 아무것도 하지 마라.

Next.js Across Platforms: Adapters, OpenNext, and Our Commitments

Next.js 16.2는 여러 호스팅 플랫폼에서 더 일관되게 배포할 수 있도록 세 가지 중요한 약속을 내놓았다. 안정화된 Adapter API, 공개 테스트 스위트, 그리고 플랫폼 제공자와 유지보수자가 함께 논의하는 워킹그룹이다. 이 글은 왜 이런 변화가 필요했는지, 그리고 Next.js가 앞으로 어떤 방향을 취할지 설명한다.

왜 이런 작업이 필요했나

많은 Next.js 앱은 여전히 단일 Node.js 서버에서 next start 로 배포된다. 이 방식은 지금도 수많은 팀이 문제없이 사용한다. 하지만 앱을 실제 서비스 규모로 키우려면 서버 인스턴스를 여러 대 띄우는 순간이 온다. 그러면 다음과 같은 것들이 “최적화”가 아니라 “정상 동작의 필수 조건”이 된다.

  • 캐시된 콘텐츠가 여러 인스턴스 사이에서 동기화되어야 한다
  • on-demand revalidation 이 전파되어야 한다
  • 스트리밍이 안정적으로 동작해야 한다

여기에 글로벌 CDN, 엣지 컴퓨트, 서버리스로 인한 콜드 스타트 최적화 같은 선택지가 겹치면 플랫폼별 복잡도가 급격히 커진다. 스트리밍, Server Components, Partial Prerendering, 미들웨어, Cache Components, revalidation 같은 기능 조합은 표면적으로는 단일 프레임워크 기능이지만, 실제 배포 플랫폼 입장에서는 아주 많은 구현 포인트를 가진다.

문제는 이 전체 표면적이 오랫동안 공식적이고 안정적인 계약으로 문서화돼 있지 않았다는 점이다. 플랫폼 제공자는 각 릴리스마다 빌드 결과와 내부 동작을 해석해가며 맞춰야 했고, 이게 유지보수를 어렵게 만들었다.

OpenNext가 다리 역할을 했다

이 공백을 메운 것이 OpenNext다. OpenNext는 Next.js 빌드 결과를 여러 플랫폼이 소비할 수 있는 형태로 변환하면서, 프레임워크 의미론을 각 플랫폼의 인프라 primitive에 매핑하는 역할을 했다. 처음에는 호환성 레이어에 가까웠지만, 시간이 지나며 특히 AWS에서 사실상 프로덕션급 어댑터로 성장했고, 이후 Cloudflare와 Netlify도 이 노력에 합류했다.

이 경험을 통해 드러난 핵심 통찰은 하나였다. Next.js 빌드 출력 자체가 안정적이고 정의된 인터페이스가 될 수 있다는 점이다.

이후 Next.js 팀은 OpenNext 유지보수자, Netlify, Cloudflare, AWS Amplify, Google Cloud 엔지니어들과 함께 2025년 4월 Build Adapters RFC를 공개하고, 워킹그룹을 구성해 API 설계와 검증을 공동으로 진행했다.

Adapter API

Next.js 16.2에는 안정적이고 공개된 Adapter API가 포함된다. 이제 빌드 시점에 Next.js는 애플리케이션의 구조를 타입과 버전이 명시된 형태로 출력한다. 이 안에는 다음 같은 정보가 담긴다.

  • routes
  • prerenders
  • static assets
  • runtime targets
  • dependencies
  • caching rules
  • routing decisions

플랫폼 어댑터는 이 출력을 읽어 자기 인프라에 맞게 매핑하면 된다. 어댑터는 modifyConfig 와 onBuildComplete 두 훅을 구현한다. 중요한 점은, 파괴적 변경은 Next.js 메이저 버전 증가를 통해서만 이뤄진다는 약속이다.

또 하나 의미 있는 포인트는 Vercel 어댑터도 같은 공개 계약을 사용한다는 점이다. 즉, Vercel만을 위한 비공개 훅이나 특수 통합 경로가 있는 것이 아니라, 같은 공개 API 위에 서 있다는 뜻이다. 해당 어댑터는 오픈소스로 공개돼 있다.

문서도 함께 정비됐다. Rendering Philosophy, Deploying to Platforms, PPR Platform Guide, How Revalidation Works, CDN Caching 같은 가이드가 추가되어 플랫폼 통합 표면을 더 명확히 설명한다.

테스트 스위트

Next.js는 어댑터 작성자를 위해 테스트 스위트를 공개했다. 이 테스트는 스트리밍 동작, 캐시 상호작용, 클라이언트 내비게이션, 실제 환경에서 발생하는 엣지 케이스를 다룬다. 새 기능이 추가되면 기대 동작이 이 테스트에 코드로 박힌다.

어댑터 작성자는 이 테스트를 자기 구현 위에서 돌려서 pass/fail 결과를 받을 수 있다. 그리고 이건 Vercel이 자기 어댑터 검증에 쓰는 것과 같은 테스트 세트다. 즉, 특정 플랫폼만의 숨은 기준이 아니라 모두에게 동일한 정합성 기준이 적용된다.

Verified adapters

공식 문서에 노출되고 Next.js GitHub 조직 아래 호스팅되는 “verified adapter”가 되려면 두 가지 조건이 필요하다.

  1. 오픈소스일 것
  2. 전체 테스트 스위트를 통과할 것

오픈소스여야 하는 이유는 유지보수와 이슈 트래킹이 가능한 공용 공간이 필요하기 때문이다. 테스트 통과 조건은 호환성을 측정 가능한 상태로 만들기 위해서다.

다만 구현 소유권은 각 플랫폼 팀에 있다. 배포 인프라 구조가 서로 다르기 때문에 Next.js 팀이 구현 세부를 강제하진 않는다. 그리고 비공개 어댑터를 만들고 배포하는 것도 가능하다. 중요한 것은 모두가 같은 공개 API를 기준으로 삼는다는 점이다.

현재 공개된 Adapters API와 함께 다음 어댑터들이 언급된다.

  • Vercel adapter: 오픈소스
  • Bun adapter: 참고용 레퍼런스 어댑터
  • Netlify / Cloudflare / AWS(OpenNext 경유): 올해 내 릴리즈 예정

Ecosystem Working Group

Next.js 팀은 플랫폼 제공자와 어댑터 유지보수자를 위한 상설 워킹그룹도 만든다. 회의록은 공개되고, 릴리스 전에 예정된 변경을 더 일찍 공유하는 것이 목표다. 플랫폼 팀이 릴리스 이후에야 깨닫는 구조 대신, 사전에 함께 검토하는 구조를 만들겠다는 뜻이다.

파괴적 변경은 범위에 비례하는 리드타임을 두고 예고하고, 새 기능은 릴리스 전에 여러 환경에서 테스트한다는 것이 이 그룹의 운영 원칙이다.

글의 결론

Next.js는 이미 수백만 개발자가 쓰고 있고, 그중 많은 팀이 Vercel 이외의 인프라에서 운영한다. 이 사용자들도 같은 수준의 신뢰성과 새 기능 접근성을 가져야 한다는 것이 글의 핵심 메시지다.

Adapter API는 이를 위해 “모든 플랫폼이 구현할 수 있는 공용 계약”을 제공한다. 여기에 공개 테스트 스위트가 붙으면서, 특정 플랫폼만 알고 있는 비밀 규약 대신 측정 가능하고 공유 가능한 호환성 기준이 생겼다. 앞으로 나오는 새 기능 역시 이 어댑터 계약 안에서 문서화될 예정이다.

실무 관점에서 보면 이 변화는 꽤 크다. Next.js를 특정 호스팅 사업자에 강하게 묶는 대신, 프레임워크와 배포 플랫폼 사이의 경계를 더 명시적이고 표준화된 방식으로 재정의하려는 시도이기 때문이다. 멀티 클라우드, 자체 호스팅, 엣지/서버리스 혼합 배포를 고민하는 팀일수록 주목할 만한 변화다.

Vite 8.0 is out!

Vite 8의 핵심 메시지는 분명하다. 지금까지 개발용은 esbuild, 프로덕션 번들은 Rollup이라는 이원 구조 위에서 성장해온 Vite가, 이제 Rolldown이라는 Rust 기반 단일 번들러로 수렴했다는 것이다. 이것은 Vite 2 이후 가장 큰 아키텍처 변화로 소개된다.

Vite 팀에 따르면 Rolldown 기반의 Vite 8은 기존 플러그인 호환성을 유지하면서도 빌드 속도를 최대 10~30배까지 끌어올릴 수 있다. 동시에 registry.vite.dev 라는 새로운 플러그인 디렉터리도 공개해, 커지는 생태계 안에서 플러그인을 더 쉽게 탐색할 수 있게 했다.

왜 바꾸었나

초기부터 Vite는 개발 경험을 위해 esbuild를, 프로덕션 번들링과 청킹/최적화를 위해 Rollup을 사용해왔다. 이 구조 덕분에 빠른 DX를 구현할 수 있었지만, 시간이 지나면서 두 개의 변환 파이프라인을 동시에 유지해야 하는 비용이 커졌다.

  • 플러그인 시스템이 사실상 둘로 나뉘어 있었다
  • 양쪽 동작을 맞추기 위한 glue code가 계속 늘어났다
  • 한쪽에서 맞춘 엣지 케이스가 다른 쪽 차이로 다시 튀어나오는 일이 잦았다

즉, 그동안의 구조는 유용했지만, 장기적으로는 일관성과 유지보수 측면의 발목을 잡기 시작한 것이다.

Rolldown이 제공하는 것

Rolldown은 VoidZero 팀이 만든 Rust 기반 번들러다. 글은 세 가지 목표를 강조한다.

  1. 성능: Rust 기반이라 네이티브 속도로 동작하며, 벤치마크 기준 Rollup보다 10~30배 빠르다고 설명한다.
  2. 호환성: Rollup/Vite와 같은 플러그인 API를 지향해 기존 Vite 플러그인의 상당수가 그대로 동작한다.
  3. 확장성: 단일 번들러 구조 덕분에 Full Bundle Mode, 더 유연한 chunk splitting, 모듈 단위 persistent caching, Module Federation 같은 기능이 쉬워진다.

안정화까지의 과정

Vite 팀은 이 전환을 한 번에 밀어붙이지 않았다. 먼저 rolldown-vite 라는 별도 패키지로 기술 프리뷰를 배포해 초기 사용자들이 실전 코드베이스에서 검증할 수 있게 했다. 이후 주요 프레임워크/플러그인 조합을 돌리는 전용 CI를 마련해 회귀를 조기에 잡았고, 2025년 12월에는 Vite 8 beta를 통해 Rolldown 통합판을 넓게 검증했다.

이 베타 과정에서 커뮤니티 피드백이 큰 역할을 했고, 실제로 여러 회사가 눈에 띄는 빌드 시간 감소를 보고했다.

  • Linear: 46초 → 6초
  • Ramp: 57% 감소
  • Mercedes-Benz.io: 최대 38% 감소
  • Beehiiv: 64% 감소

특히 대형 프로젝트일수록 체감 효과가 크며, Rolldown이 성숙해질수록 더 좋아질 수 있다고 본다.

단일 툴체인으로의 수렴

Vite 8은 단순히 번들러 하나를 바꾼 게 아니라, Vite(빌드 툴) + Rolldown(번들러) + Oxc(컴파일러)라는 협력 구조를 더 선명하게 만들었다. 이렇게 되면 파싱, 해석, 변환, 최소화 전반에서 더 일관된 동작을 기대할 수 있고, 언어 사양 변화에 더 빠르게 대응할 수 있다. 나아가 Oxc의 의미 분석을 Rolldown 최적화에 연결하는 식의 교차 레이어 최적화도 가능해진다.

Vite 8의 추가 기능

Rolldown 통합 외에도 눈에 띄는 변화가 몇 가지 있다.

  • Integrated Devtools: devtools 옵션으로 Vite Devtools를 켜서 dev server 안에서 디버깅/분석을 더 쉽게 할 수 있다.
  • Built-in tsconfig paths support: resolve.tsconfigPaths 옵션으로 TS path alias 해석을 내장 지원한다. 다만 약간의 성능 비용이 있어 기본 활성화는 아니다.
  • emitDecoratorMetadata 지원: TypeScript의 emitDecoratorMetadata를 외부 플러그인 없이 자동 지원한다.
  • Wasm SSR support: .wasm?init import가 SSR 환경에서도 동작한다.
  • Browser console forwarding: 브라우저 콘솔 로그/에러를 dev server 터미널로 전달할 수 있다. 특히 코딩 에이전트와 함께 작업할 때 클라이언트 런타임 에러를 CLI에서 바로 볼 수 있다는 점이 실무적으로 꽤 유용하다.

@vitejs/plugin-react v6

Vite 8과 함께 @vitejs/plugin-react v6도 나온다. 이 버전은 React Refresh 변환에 Oxc를 사용하고, Babel 의존성을 제거해 설치 크기를 줄였다. React Compiler가 필요한 프로젝트는 @rolldown/plugin-babel 과 함께 reactCompilerPreset을 명시적으로 opt-in 하는 경로를 쓸 수 있다.

중요한 점은 plugin-react v5도 Vite 8과 계속 호환된다는 것이다. 즉, Vite 업그레이드와 React 플러그인 업그레이드를 한 번에 강제하지 않는다.

앞으로의 방향

글은 Rolldown 통합이 시작일 뿐이라고 말한다. 앞으로는 다음 같은 영역을 계속 다듬을 계획이다.

  • Full Bundle Mode(실험적): 개발 중에도 모듈을 번들 단위로 다루는 방식. 대형 프로젝트에서 dev server 기동 3배, 전체 reload 40%, 네트워크 요청 10배 감소 같은 초기 결과가 언급된다.
  • Raw AST transfer: Rust에서 만든 AST를 JS 플러그인으로 더 효율적으로 전달
  • Native MagicString transforms: 문자열 조작 계산을 Rust에서 처리하면서 JS에서 로직을 작성하는 구조
  • Environment API 안정화

설치 크기 증가도 솔직하게 공개

Vite 팀은 단점도 숨기지 않는다. Vite 8은 Vite 7보다 설치 크기가 대략 15MB 정도 커졌다.

  • 약 10MB는 lightningcss가 optional peer dependency에서 일반 dependency로 바뀌면서 생긴 증가
  • 약 5MB는 Rolldown 바이너리 크기 증가

즉, 성능을 위해 바이너리 크기 일부를 받아들인 셈이다. 앞으로 줄여나가겠다고 하지만, 업그레이드 시 CI 이미지나 캐시 전략을 신경 쓰는 팀이라면 알아둘 부분이다.

마이그레이션 전략

대부분의 프로젝트는 비교적 부드럽게 업그레이드할 수 있도록, 기존 esbuild/rollupOptions 설정을 Rolldown/Oxc 쪽으로 자동 변환해주는 호환 레이어를 제공한다. 그래도 복잡한 프로젝트라면 두 단계 접근을 추천한다.

  1. Vite 7에서 먼저 rolldown-vite 패키지로 전환
  2. 그 다음 Vite 8로 업그레이드

이렇게 하면 문제의 원인이 번들러 전환인지, Vite 8의 다른 변경인지 분리해서 파악하기 쉽다.

마무리

Vite 8은 단순한 메이저 버전 업이 아니다. 프런트엔드 툴링이 “빠른 개발 서버 + 안정적 프로덕션 번들”의 이원 구조를 넘어, 하나의 고성능 파이프라인으로 수렴하는 방향을 보여주는 신호에 가깝다. 실무적으로는 빌드 시간 단축, 플러그인 정합성, 향후 고급 최적화 가능성까지 함께 열리는 변화다.

대신 일부 설치 크기 증가와, 복잡한 프로젝트의 경우 마이그레이션 검증 비용은 감수해야 한다. 그럼에도 최근 프론트엔드 빌드 툴 변화 중에서는 가장 영향력이 큰 업데이트 중 하나로 볼 만하다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment