David Poblador i Garcia의 The Descent — What Happened to the Frontend While You Weren’t Watching를 읽으며 핵심 관점을 내 언어로 정리하고, 실무에서 느낀 점을 덧붙였다. 원문의 전문 번역이나 재게시가 아니다.

프론트엔드 개발을 하다 보면 가끔 단순한 버튼 하나에 왜 이렇게 많은 것이 필요한지 의문이 든다.

버튼은 컴포넌트가 되고, 타입과 디자인 토큰을 갖는다. 상태에 따라 서버 데이터를 다시 가져오고, 접근성 규칙과 테스트 코드가 붙는다. 브라우저에 도착하기 전에는 트랜스파일과 번들링을 거치고, 서버에서 먼저 렌더링됐다면 클라이언트에서 다시 상호작용 가능한 상태가 되어야 한다.

각 선택을 따로 보면 대부분 타당하다. 그런데 이 선택들이 겹치면 전체 구조를 설명하기 어려워진다.

원문에서 가장 인상 깊었던 관점은 이것이었다.

모든 도구는 실제 상처 위에 생긴 흉터다.

프론트엔드 도구는 유행만으로 등장하지 않았다. 당시의 구체적인 문제를 해결하기 위해 생겼고, 그 해결책이 다시 새로운 문제를 만들었다. 이 관점으로 보면 중요한 질문은 “요즘 어떤 도구를 쓰는가”가 아니다.

이 도구가 해결하려던 상처는 무엇이고, 그 상처는 지금도 남아 있는가?

이 질문을 따라 프론트엔드의 여러 층을 다시 살펴봤다.

TL;DR

  • 프론트엔드의 복잡성은 누군가 한 번에 설계한 결과가 아니라, 실제 문제를 차례로 해결하며 쌓인 결과다.
  • React와 선언형 UI는 화면과 상태를 사람이 수동으로 동기화하던 문제를 해결했다.
  • 번들러는 과거의 브라우저와 네트워크 제약을 해결했지만, 브라우저가 발전한 뒤에도 다른 최적화 역할 때문에 남아 있다.
  • SSR과 React Server Components는 과거로 단순히 회귀한 것이 아니라, 실행 위치와 클라이언트 JavaScript의 경계를 다시 나누는 시도다.
  • 중요한 것은 도구를 모두 걷어내는 일이 아니라, 현재도 비용보다 큰 문제를 해결하고 있는지 계속 확인하는 일이다.
  • AI가 코드를 대신 작성해도 이 판단까지 대신해 주지는 않는다.

화면과 데이터를 사람이 동기화하던 시절

초기의 JavaScript는 페이지의 작은 부분에 상호작용을 더하는 데 사용됐다. 메뉴를 열고, 입력값을 검증하고, 전체 페이지를 새로 고치지 않은 채 일부 데이터를 바꾸는 정도였다.

jQuery는 이 시기의 큰 문제를 잘 해결했다. 브라우저마다 달랐던 DOM API와 이벤트 처리 방식을 하나의 인터페이스로 감쌌다. 복잡한 표준 차이를 개발자가 매번 직접 다루지 않아도 됐다.

하지만 웹이 문서에서 애플리케이션으로 커지자 다른 문제가 나타났다. 상품 가격이 바뀌면 목록, 장바구니 합계, 헤더 배지와 구매 버튼을 모두 찾아 수정해야 했다. 데이터의 상태와 화면의 상태를 사람이 직접 일치시켰다.

여기서 선언형 UI가 등장했다.

화면의 특정 노드를 찾아 바꾼다

가 아니라,

현재 상태가 이렇다면 화면은 이렇게 보여야 한다

라고 표현하기 시작했다. React의 가장 큰 가치는 특정 렌더링 알고리즘 하나보다 이 사고방식의 변화에 있었다고 생각한다.

컴포넌트와 상태 모델이 생기면서 화면을 예측하기 쉬워졌다. 대신 런타임, 컴포넌트 트리, 상태 소유권과 렌더링 경계라는 새로운 개념을 배워야 했다. 수동 DOM 동기화의 비용을 줄이는 대신 애플리케이션 구조의 비용을 받아들인 셈이다.

선언형 UI가 모든 화면의 기본값이어야 할까

이 해결책은 지금도 유효하다. 상품, 주문, 인증처럼 상태 변화가 많은 화면에서 선언형 UI를 버리고 직접 DOM을 관리하는 쪽으로 돌아가기는 어렵다.

다만 모든 페이지가 이런 복잡성을 갖는 것은 아니다. 읽기 중심 콘텐츠나 상호작용이 거의 없는 화면까지 같은 런타임과 상태 구조를 가져야 하는지는 별개의 질문이다. 프레임워크의 편의 때문에 모든 페이지를 애플리케이션으로 취급하면, 해결할 문제가 없는 곳에도 해결책의 비용을 지불하게 된다.

빌드 단계는 왜 생겼고 왜 아직 남아 있을까

JavaScript 애플리케이션이 커지면서 코드를 파일 단위로 나누고 다시 조합할 방법이 필요해졌다. 현대적인 문법과 JSX를 오래된 브라우저에서도 실행해야 했고, 많은 파일을 효율적으로 전달해야 했다.

Babel과 webpack 같은 도구는 이 문제를 해결했다.

  • 새로운 문법을 대상 브라우저가 이해할 코드로 변환한다.
  • 여러 모듈을 배포 가능한 묶음으로 만든다.
  • 사용하지 않는 코드를 제거한다.
  • 코드를 분할하고 압축한다.
  • 변환된 코드에서도 원본을 추적할 수 있도록 소스 맵을 만든다.

이 과정이 우리가 말하는 빌드 단계가 됐다.

지금은 상황이 달라졌다. 모든 현대 브라우저가 ES Modules를 기본 지원한다. CSS에도 변수, 중첩, 컨테이너 쿼리와 :has()처럼 과거에는 전처리기나 JavaScript가 맡던 기능이 들어왔다.

그렇다고 번들러가 바로 불필요해진 것은 아니다. 브라우저가 모듈을 이해하는 것과 프로덕션 애플리케이션을 효율적으로 전달하는 것은 같은 문제가 아니다. 코드 분할, 압축, 사용하지 않는 코드 제거, 자산 처리와 프레임워크 변환은 여전히 필요할 수 있다.

빌드 유무보다 입력과 출력을 보자

“브라우저가 지원하니 빌드를 없애자”와 “예전부터 있었으니 그대로 두자”는 둘 다 너무 빠른 결론이다.

빌드 도구를 평가할 때는 기능 목록보다 실제 입력과 출력을 봐야 한다.

  • 어떤 문법을 무엇으로 변환하는가?
  • 지원 대상 브라우저 때문에 남아 있는 변환은 무엇인가?
  • 번들링으로 줄어든 요청과 추가된 실행 비용은 각각 얼마인가?
  • 사용하지 않는 플러그인과 로더가 남아 있지는 않은가?
  • 개발 빌드와 운영 빌드에서 필요한 역할이 같은가?

도구를 제거할 수 있는지는 도구 이름이 아니라 이 질문들의 답으로 결정해야 한다.

더 빠른 도구는 구조적 복잡성을 없애지 않는다

webpack과 Babel 중심의 빌드가 느려지면서 esbuild, SWC, Vite, Turbopack과 같은 도구가 등장했다. 구현 언어가 JavaScript에서 Go와 Rust로 옮겨가고 병렬 처리가 적극적으로 사용됐다.

이 변화는 체감이 크다. 개발 서버가 빠르게 시작되고 저장한 결과를 곧바로 확인할 수 있다. 긴 빌드 때문에 사고 흐름이 끊기는 일도 줄어든다.

하지만 속도가 빨라졌다고 구조가 단순해진 것은 아니다.

컴파일러가 훨씬 빨라져도 클라이언트에 보내야 할 코드가 과도하다면 사용자가 받는 비용은 남는다. 개발 서버의 메모리 사용량이 줄어도 서버와 클라이언트의 데이터 경계가 불명확하면 애플리케이션의 이해 비용은 그대로다.

도구 성능과 제품 구조는 다른 성과다

도구 성능과 제품 구조를 구분해서 봐야 한다.

빠른 컴파일러는 개발 경험을 개선한다. 작은 클라이언트 번들과 명확한 데이터 경계는 사용자 경험과 유지보수성을 개선한다. 둘은 서로 도움을 줄 수 있지만 같은 성과는 아니다.

새 도구를 검토할 때 “빌드가 몇 배 빨라졌다”는 수치만 보면 도입하기 쉽다. 그와 함께 다음 질문도 필요하다.

이 도구가 빨라지면서 우리가 계속 미뤄도 된다고 착각하게 된 구조적 문제는 없는가?

서버로 돌아왔지만 예전과 같은 서버는 아니다

클라이언트 중심 애플리케이션은 풍부한 상호작용을 만들기 쉬웠지만 초기 화면을 표시하려면 JavaScript를 내려받고 실행해야 했다. 검색과 첫 화면 성능이 중요한 서비스에는 부담이 됐다.

그래서 서버에서 HTML을 먼저 만드는 방식이 다시 중요해졌다. SSR, SSG와 ISR 같은 전략이 등장했고, Next.js를 비롯한 메타 프레임워크가 데이터 로딩과 라우팅, 렌더링 방식을 함께 다루기 시작했다.

여기까지만 보면 프론트엔드가 결국 과거의 서버 렌더링으로 돌아온 것처럼 보인다.

하지만 완전히 같은 자리로 돌아온 것은 아니다.

오늘의 서버 렌더링은 초기 HTML을 빠르게 보내면서도 이후의 클라이언트 탐색과 상호작용을 유지하려 한다. 이 과정에서 hydration 비용이 생겼다. 서버가 만든 HTML을 브라우저가 보여준 뒤, 클라이언트 JavaScript가 같은 컴포넌트 구조를 따라가며 이벤트와 상태를 연결한다.

React Server Components는 여기서 한 단계 더 나아간다. 컴포넌트 일부를 브라우저 번들에 포함하지 않고 클라이언트 앱과 분리된 환경에서 실행할 수 있게 한다. 공식 문서의 설명처럼 Server Component는 빌드 시점이나 요청 시점에 실행될 수 있고, 필요한 부분만 Client Component와 조합한다.

핵심은 서버 회귀가 아니라 경계 재설계다

“서버로 돌아왔다”는 표현은 방향을 이해하는 데는 좋지만, 설계를 결정하기에는 조금 거칠다.

RSC의 핵심은 모든 것을 서버로 보내는 것이 아니다. 어디까지 서버에서 끝내고 어디부터 브라우저의 상호작용으로 남길지 정하는 것이다.

서버에서 처리할 것
- 데이터 접근
- 비밀 값과 권한이 필요한 로직
- 브라우저에 보낼 필요가 없는 라이브러리
- 초기 화면에 필요한 콘텐츠

클라이언트에 남길 것
- 즉각적인 사용자 입력
- 브라우저 API
- 로컬 상태
- 연속적인 상호작용

문제는 이 경계가 자동으로 좋은 위치에 생기지 않는다는 점이다. 편의를 위해 상위 컴포넌트에 "use client"를 붙이면 예상보다 넓은 코드가 클라이언트 경계로 들어갈 수 있다. 반대로 모든 것을 서버로 옮기면 작은 상호작용도 서버 왕복과 캐시 무효화 문제로 바뀔 수 있다.

과거에는 화면과 데이터의 동기화가 주요 문제였다면, 지금은 실행 위치와 데이터 생명주기의 경계를 설명하는 일이 새로운 문제다.

성숙한 웹 플랫폼은 어떤 도구를 지울 수 있을까

원문은 프론트엔드의 가장 아래층에서 다시 HTML과 웹 플랫폼을 만난다고 말한다. 브라우저가 성장하면서 프레임워크와 라이브러리가 대신하던 기능 일부를 직접 제공하기 시작했기 때문이다.

이 흐름에는 동의한다.

  • ES Modules는 브라우저 자체의 모듈 시스템을 제공한다.
  • CSS 변수와 중첩은 스타일 전처리기의 역할 일부를 가져왔다.
  • 컨테이너 쿼리는 뷰포트가 아니라 컴포넌트가 놓인 공간에 따라 스타일을 바꿀 수 있게 한다.
  • popover<dialog> 같은 기능은 접근성 의미와 기본 상호작용 일부를 플랫폼으로 옮긴다.

하지만 “브라우저 기능이 생겼다”와 “기존 구현을 바로 지울 수 있다” 사이에는 간격이 있다.

지원해야 할 브라우저와 WebView 버전이 다르고, 제품이 요구하는 동작이 표준 기능의 범위를 넘을 수 있다. 이미 쌓인 컴포넌트 API와 디자인 시스템을 교체하는 비용도 있다.

웹 플랫폼 우선은 라이브러리 금지 선언이 아니다

웹 플랫폼을 우선하는 태도는 특정 라이브러리를 쓰지 말자는 선언이 아니다. 새 요구사항이 생겼을 때 다음 순서로 확인하자는 의미에 가깝다.

  1. 브라우저가 이미 제공하는가?
  2. 제품의 지원 범위에서 사용할 수 있는가?
  3. 부족한 부분만 얇게 보완할 수 있는가?
  4. 그래도 해결되지 않을 때 라이브러리가 주는 비용보다 가치가 큰가?

기존 도구를 한꺼번에 걷어내기보다, 새 코드에서 불필요한 층을 더 만들지 않는 쪽이 현실적이다.

AI는 복잡성을 숨기지만 없애지는 않는다

원문의 마지막 층에는 코딩 에이전트가 등장한다. 이제 자연어로 화면을 설명하면 그럴듯한 React 코드가 만들어진다. 프론트엔드를 전문적으로 다루지 않은 개발자도 짧은 시간에 동작하는 화면을 만들 수 있다.

이 변화로 코드를 작성하는 비용은 분명히 낮아졌다.

그러나 생성된 코드는 지난 20년 동안 쌓인 선택을 암묵적으로 포함한다. 상태 관리, 서버와 클라이언트 경계, 번들 크기, 접근성, 캐시와 오류 처리 같은 판단이 사라지는 것은 아니다. 생성 과정에서 보이지 않을 뿐이다.

AI는 버튼 컴포넌트를 빠르게 만들 수 있다. 하지만 다음 질문의 답은 여전히 개발자가 책임져야 한다.

  • 이 버튼에 정말 클라이언트 상태가 필요한가?
  • 클릭 뒤 발생하는 요청은 중복 실행되어도 안전한가?
  • 서버 응답을 기다리는 동안 어떤 상태를 보여줘야 하는가?
  • 접근성과 키보드 상호작용은 지켜지는가?
  • 이 컴포넌트 때문에 어느 코드까지 브라우저에 포함되는가?

생성 비용이 낮아질수록 설명 책임은 커진다

AI 시대에는 도구의 사용법을 모두 외우는 능력보다, 생성된 구조가 어떤 문제를 해결하려는지 설명하는 능력이 더 중요해질 수 있다.

설명할 수 없다면 제거해도 되는지 판단할 수 없다. 장애가 났을 때 어느 층을 확인해야 하는지도 알기 어렵다. AI가 복잡한 코드를 더 빠르게 생산할수록, 복잡성을 받아들일 기준은 오히려 더 엄격해야 한다.

모든 흉터를 지울 필요는 없다

프론트엔드가 복잡해졌다는 사실만으로 과거의 단순한 HTML로 돌아가야 하는 것은 아니다.

오늘의 버튼은 예전보다 많은 것을 갖고 있다. 키보드로 조작할 수 있고, 화면 읽기 도구에 의미를 전달하며, 디자인 시스템 안에서 일관된 모습을 유지한다. 타입과 테스트 덕분에 변경의 영향을 더 일찍 확인할 수도 있다.

복잡성 중에는 제품의 품질을 위해 필요한 것도 있다.

문제는 모든 층을 당연하게 받아들이는 데서 시작한다. 한때 실제 상처를 해결했던 도구도 환경이 바뀌면 관성이 될 수 있다. 반대로 겉으로 복잡해 보이는 도구가 지금도 큰 사고와 반복 작업을 막고 있을 수도 있다.

그래서 내가 이 글을 읽고 남긴 기준은 단순하다.

도구를 추가하기 전
1. 해결하려는 문제를 한 문장으로 적는다.
2. 브라우저와 현재 스택이 이미 해결하는지 확인한다.
3. 도입 뒤 생길 새 경계와 운영 비용을 적는다.

도구를 유지할 때
1. 처음의 문제가 아직 존재하는지 확인한다.
2. 실제로 사용 중인 기능을 확인한다.
3. 제거 비용과 유지 비용을 비교한다.

AI가 코드를 만들었을 때
1. 추가된 각 층의 이유를 설명한다.
2. 이유를 설명할 수 없는 의존성과 추상화를 의심한다.
3. 빠르게 생성한 만큼 검증 범위를 명시한다.

프론트엔드의 역사를 배우는 이유는 도구 이름을 더 많이 기억하기 위해서가 아니다. 지금 눈앞에 있는 복잡성이 필수인지, 과거의 상처가 남긴 흔적인지 구분하기 위해서다.

그리고 그 구분이 가능해야 새 도구를 도입할 때도 같은 역사를 반복하지 않을 수 있다.

읽을 거리

원문은 CC BY-SA 4.0으로 공개되어 있다. 이 글은 원문의 전체 구성을 옮긴 번역·재게시가 아니라, 핵심 관점을 선택해 다시 설명하고 개인적인 해석을 덧붙인 독서 기록이다.