Tag

#react

02 Posts
Latest · 개발 2026.10.01 25 min read

브라우저 밖에서 리액트를 다시 만나다

브라우저 탭 바깥에서 만난 리액트 저는 스타트업에서 정규직 개발자로 일하며 주로 리액트로 웹 서비스를 만들었고, 지금은 이런 저런 제조 대기업을 옮겨 다니며 상주 프리랜서로 일하고 있습니다. 직함과 계약 형태가 바뀌…

Posts최신순
2026.09.28
개발
React에서 벗어나고 싶었던 어느 리액트 개발자 이야기

여러분이 보고 계시는 이 웹 사이트는 워드프레스와 php로 구축되었습니다. 태어나 처음으로 워드프레스와 php를 만져보면서 생각했던 것들을 정리해봅니다. 화면을 위한 라이브러리였던 리액트 저는 회사에서 리액트로 서비스를 개발하던 프런트엔드 개발자였습니다. 물론 지금은 AI의 도움으로 프런트엔드에 국한되지 않고 회사에서 필요한 게 있으면 닥치는 대로 개발하는 잡부 느낌이 강하긴 하지만, 어쨌든 저의 명함에는 꽤 오랫동안 "Frontend Engineer"라는 직함이 박혀 있었습니다. 개발자라는 직업을 얻게 된 이래로 저는 리액트를 사용하며 컴포넌트 단위로 UI를 설계하는 법을 배웠고, 선언형 렌더링이 주는 생산성을 체감할 수 있었습니다. 상태가 바뀌면 화면이 따라 바뀐다는 단순한 모델은 확실히 jQuery로 DOM을 직접 조작하던 시절과 비교하면 분명한 진보였을 것입니다...... 하지만 현업에서 리액트만을 전문으로 수 년을 다뤘던 리액트 개발자였던 제가, 어쩌다 어느새부턴가 "이제는 리액트 좀 그만 쓰고 싶다", 는 생각을 하고 있었습니다. 이번에 새 블로그를 구축하기 위해 생전 처음 다뤄보는 워드프레스 php를 사용해보면서, 저는 이 글에서 웹 개발에서 프런트엔드의 기술 스택으로 리액트를 선택하는 것이 마치 업계 전체의 기본값이 되어버린 것 같은 오늘날의 모습에 대한 지극히 개인적인 저의 생각들을 나눠보려고 합니다. 리액트는 처음에 스스로를 "사용자 인터페이스를 만들기 위한 자바스크립트 라이브러리"라고 소개했습니다. 개발자들 사이에서 리액트가 프레임워크냐 라이브러리냐에 관하여 논쟁도 꽤 있었던 것으로 기억합니다만 분명 리액트의 처음 시작은 "라이브러리"였었고, 이에 따른 리액트의 역할도 명확했습니다. "브라우저에서 가상 DOM을 기반으로 화면을 그리고, 상태 변화에 따라 필요한 부분만 갱신하는 것". 이 때 당시만 해도 라우팅 데이터 요청이나 서버 렌더링 따위는 리액트의 관심사가 아니었습니다. 성능과 SEO가 불러온 서버로의 회귀 문제는 클라이언트 사이드 렌더링(CSR)의 구조적 한계에서 시작됩니다. CSR 방식의 싱글 페이지 애플리케이션은 서버에서 사실상 비어 있는 HTML 문서와 자바스크립트 번들을 내려보냅니다. 실제 콘텐츠는 브라우저가 번들을 내려받고, 파싱하고, 실행한 뒤에야 만들어집니다. 실제 로딩 과정에서 CSR이 문제가 되는 지점은 조금 더 구체적입니다. 브라우저는 HTML을 파싱하면서 스크립트 태그를 발견하면 자바스크립트를 병렬로 내려받기 시작할 수 있고, 브라우저의 preload scanner는 메인 파서보다 앞서 리소스를 미리 찾아 요청하기도 합니다. 하지만 문제는 페이지의 핵심 콘텐츠나 그 콘텐츠를 위한 API 요청이 애플리케이션 자바스크립트의 실행 이후에야 발견되는 경우에 발생합니다. 페이지의 핵심 콘텐츠를 로드하는 것이나 그 콘텐츠를 위한 API 요청이 애플리케이션이 자바스크립트가 실행된 이후에야 시작될 수 있다는 것, 자바스크립트 실행이 완료되어야 컴포넌트 트리가 만들어지고, 그제야 API 요청이 시작되고, 응답이 돌아온 뒤에야 콘텐츠를 화면에 그릴 수 있다는 사실은 자바스크립트 실행 → API 요청 → 데이터 수신 → 렌더링까지의 각 과정이 순차적으로 그 바로 전 단계에 의존하고 있다는 것을 의미합니다. 여기서 LCP(Largest Contentful Paint)가 늦어지는 이유는 단순히 "자바스크립트가 느리다"는 문제만으로는 설명할 수 없습니다. 여기서의 진짜 문제는 핵심 콘텐츠가 자바스크립트 실행이나 클라이언트 API 요청에 의존하는 구조, 그 자체입니다. 자바스크립트는 네트워크로 내려받는 것만으로 끝나지 않습니다. 브라우저는 그것을 파싱하고, 컴파일하고, 실행해야 하며, CPU가 느린 모바일 기기에서는 이 비용이 더욱 커질 수밖에 없고, 가끔 정말 똥컴(...)이나 오래된 모바일 기기를 사용하다 보면 자바스크립트가 메인 스레드를 점유하는 동안에는 화면이 보이는데도 클릭과 입력이 반응하지 않는 상황이 만들어지기도 합니다. 검색 노출에 관한 문제는 조금 더 미묘합니다. 구글봇은 크로미움 기반 렌더러로 자바스크립트를 실행할 수 있습니다. 다만 CSR 페이지에서 핵심 콘텐츠가 초기 HTML에 존재하지 않는다면, Google은 렌더링 단계를 거쳐야 그 콘텐츠를 확인할 수 있습니다. 이 렌더링은 별도의 큐에서 처리되며, 페이지가 그 단계에 도달하기까지 시간이 걸릴 수 있습니다. 서버가 완성된 HTML을 응답한다면 크롤러는 HTTP 응답만으로 콘텐츠를 바로 발견하지만, CSR 구조에서는 자바스크립트를 발견하고, 렌더링하고, 그 결과에서 콘텐츠를 발견하는 단계가 더 얹어집니다. 게다가 모든 크롤러가 구글봇만큼 완전한 렌더러를 가지고 있는 것도 아니어서, 많은 링크 미리보기 크롤러는 자바스크립트 실행을 보장하지 않습니다. 자바스크립트 실행 이후 주입되는 오픈 그래프 메타 태그나 타이틀이 제대로 인식되지 않는 경우가 생기는 이유입니다. 이제 콘텐츠와 검색 유입이 중요한 서비스에서는, 핵심 콘텐츠를 초기 HTML에 포함시키기 위한 SSR이나 SSG, 프리렌더링이 중요한 선택지가 되었습니다. 리액트 생태계는 이 요구에 응답하기 위해 그 컨트롤 범위를 UI 영역에서 점점 서버 영역으로 확장시키게 됩니다. 리액트 18, UI 라이브러리에서 웹 아키텍처로 리액트가 서버로 확장된 흐름을 이해하려면 최근 5~6년 사이의 리액트 버전 변화를 짚고 넘어가는 것이 도움이 될 것 같습니다. 리액트 17은 사실상 기능 추가가 없는 릴리즈였습니다. 리액트 팀이 공식적으로 밝혔듯이 17은 "아무 새 기능도 없는" 버전이었고, 핵심 목적은 리액트를 점진적으로 업그레이드할 수 있도록 기반을 정리하는 것이었습니다. 서로 다른 버전의 리액트 트리를 한 페이지에서 공존시키기 쉽게 만들기 위해 이벤트 위임 위치를 document에서 각 root container로 옮긴 것도 그 맥락이었습니다. 버전 17 자체는 조용했지만, 그 조용함은 버전 18에서의 엄청난 지각변동급 변화를 준비하고 있었습니다. 리액트 18에서 일어난 가장 근본적인 변화는 동시성 렌더링(Concurrent Rendering)의 도입입니다. 기존의 ReactDOM.render를 대체하는 새로운 루트 API createRoot는 리액트 18의 새로운 렌더러와 기능을 사용할 수 있게 하는 기반이 되었습니다. 이전까지 리액트의 렌더링은 동기적이었습니다. 한 번 시작하면 중간에 멈출 수 없었고, 렌더링이 진행되는 동안 브라우저는 다른 작업을 할 수 없었습니다. createRoot 위에서 startTransition이나 Suspense 같은 concurrent feature를 사용하면, 리액트는 렌더링 작업의 우선순위를 나누고 낮은 우선순위 작업을 중단했다가 다시 이어갈 수 있습니다. useTransition과 startTransition이 그 진입점입니다. 급하지 않은 상태 업데이트를 startTransition으로 감싸면, 리액트는 그 렌더링을 후순위로 밀어두고 사용자 인터랙션에 먼저 반응합니다. 자동 배칭(Automatic Batching)도 리액트 18의 주요 변화 중 하나입니다. 리액트 17까지는 이벤트 핸들러 내부의 상태 업데이트만 묶어서 한 번에 처리했습니다. setTimeout, Promise, 네이티브 이벤트 핸들러 안에서의 상태 업데이트는 각각 별도의 렌더링을 유발했습니다. 리액트 18부터는 이런 비동기 상황에서의 업데이트도 자동으로 묶입니다. 사소해 보이지만, 불필요한 렌더링이 줄어드는 실질적인 성능 개선입니다. 그러나 뭐니뭐니해도 리액트 18에서 가장 큰 의미를 가지는 변화는 역시 서버 영역입니다. 리액트 18은 renderToPipeableStream과 renderToReadableStream이라는 새로운 서버 렌더링 API를 도입했습니다. 기존의 renderToString은 페이지 전체 HTML이 완성될 때까지 기다렸다가 한꺼번에 전송하는 방식이었지만, 이제 새 API는 Suspense 경계를 기준으로 준비된 부분부터 스트리밍으로 흘려보낼 수 있게 되었습니다. 서버가 느린 데이터를 기다리는 동안 이미 준비된 헤더와 본문 영역을 먼저 브라우저에 내려보내고, 댓글이나 추천 콘텐츠처럼 시간이 걸리는 부분은 준비되는 대로 이어 붙이는 방식입니다. 이렇게 되면 브라우저는 전체 HTML을 기다리지 않고서도 먼저 수신되는 순서대로 렌더링을 시작할 수 있습니다. 그리고 여기에 더해, 리액트 서버 컴포넌트(RSC)가 있습니다. RSC는 사실 리액트 18보다 먼저 등장했습니다. 리액트 팀은 2020년 말 처음 RSC 연구 결과를 공개했고, React 18이 출시된 2022년에도 이 기능은 여전히 실험 단계였습니다. React 18이 스트리밍 SSR과 Suspense 기반 서버 렌더링의 토대를 정식으로 제공했다면, RSC는 이후 Next.js App Router 같은 프레임워크를 통해 먼저 현실의 애플리케이션에 들어오기 시작했고, React 19에서야 리액트 차원의 안정 기능으로 자리 잡았습니다. 동시성 렌더링이 클라이언트에서의 렌더링 방식을 바꿨다면, RSC는 컴포넌트 자체의 실행 위치를 다시 정의했습니다. 이 두 변화가 합쳐지면서 리액트는 더 이상 브라우저에서 동작하는 "UI 라이브러리"라는 정체성만으로는 설명되지 않게 되었습니다. 리액트는 여전히 스스로를 UI 라이브러리라고 부릅니다. 다만 현재의 공식 문서는 동시에 'full-stack React architecture'라는 방향을 명시하고 있고, 새로운 애플리케이션을 시작할 때는 리액트만 직접 구성하기보다 프레임워크를 사용할 것을 권장합니다. jQuery식 직접 DOM 조작이 일반적이던 시대에 등장한 도구가 십여 년을 거치면서 서버 런타임, 캐시 전략, 번들 경계, 데이터 페칭 패턴까지 의견을 내는 존재가 된 것입니다. 라이브러리라는 이름은 그대로지만, 그 주변에 제안되는 아키텍처의 범위는 초기와 비교하기 어려울 정도로 넓어졌습니다. 그런데 여기서 한 가지 중요한 맥락이 있습니다. 리액트 서버 컴포넌트는 리액트 18보다 먼저 연구 단계에서 공개되었지만, 리액트 자체는 이를 당장 프로덕션에서 쓸 수 있는 완성된 형태로 제공하지 않았습니다. RSC는 서버와 클라이언트를 아우르는 컴포넌트 모델이기 때문에, 실제로 동작시키려면 서버 런타임, 라우팅, 번들링, 스트리밍 처리까지 통합하는 프레임워크가 필요합니다. 그 역할을 가장 먼저, 그리고 가장 적극적으로 맡은 것이 Next.js입니다. Next.js를 만드는 Vercel은 리액트 팀과 긴밀히 협력하며 App Router라는 형태로 RSC를 프로덕션에서 쓸 수 있도록 구현했고, 현재 리액트 공식 문서도 새 프로젝트를 시작할 때 리액트만으로 프로젝트를 구성하기보다는 프레임워크를 사용하도록 권장하며 그 목록 맨 앞에 Next.js를 올려두고 있습니다. 그 결과, 특히 RSC를 중심으로 한 현대 React의 풀스택 아키텍처를 이야기하자면 이제는 Next.js를 빼놓기 어려워졌습니다. 생각해도 너무 복잡하게 느껴지는 Next.js 여기서 이야기하는 "복잡함"이란 제가 주로 실무에서 경험했던 Next.js 13~15 App Router 시기의 모델을 기준으로 합니다. 이후 Next.js 15에서 fetch와 Route Handler의 기본 캐싱 정책이 바뀌었고, Next.js 16에서는 Cache Components와 use cache를 중심으로 캐시를 명시적으로 선택하는 새로운 모델이 도입되었습니다. 흥미롭게도 이러한 변화 자체가, 초기 App Router의 암묵적 캐싱 모델이 개발자들에게 얼마나 큰 혼란을 만들었는지를 보여주기도 합니다. Next.js를 실무에서 다루며 가장 크게 느낀 것은 단순히 '복잡하다'는 감각이 아니었습니다. 더 정확히 말하면, 제가 쓴 코드의 의미를 이해하는 것과 그 코드가 Next.js 시스템 안에서 어떻게 작동하는지를 이해하는 것이 전혀 다른 일이 된다는 데에 있었습니다. 개발자는 자신의 비즈니스 로직뿐 아니라 프레임워크의 렌더링 결정 규칙, 캐시 계층 구조, 컴포넌트의 실행 위치까지 머릿속에 들고 있어야 합니다. App Router는 이름과 달리 단순한 라우터가 아닙니다. 과거에 제가 주로 사용하던 React Router의 선언적 라우팅을 떠올려보면 이는 URL과 컴포넌트를 연결하는 것이 전부였습니다. 하지만 Next.js App Router에서 폴더 구조는 단순히 라우팅 URL만 정하지 않습니다. layout.tsx는 하위 페이지들이 공유하면서 클라이언트 내비게이션 중에도 유지되는 UI를 정의하고, loading.tsx는 해당 route segment에 Suspense 경계를 만들고, error.tsx는 오류 경계를 담당합니다. 여기에 template.tsx, not-found.tsx, route.ts, parallel route, intercepting route까지 더해집니다. 각각의 파일이 단순히 '이 URL에서 보여줄 화면'이 아니라 렌더링 생명주기의 서로 다른 단계를 담당하는 구조로써 결합되고 있습니다. 특히 개발자로서 주의 깊게 신경써야 하는 부분은 렌더링 전략이 결정되는 방식입니다. 겉으로 보기에는 그냥 데이터를 가져오는 서버 컴포넌트가 있다고 해봅시다. 그런데 어느 하위 컴포넌트에서 cookies()를 한 번 호출하면, 그 route 전체가 빌드 시점에 만들어지는 정적 페이지에서 요청마다 실행되는 동적 페이지로 전환됩니다. 코드의 지역적인(local) 의미는 단순합니다. "쿠키를 읽는다.", 하지만 Next.js 아키텍처 안에서의 의미는 다릅니다. 그 한 줄이 이 route의 렌더링 전략을 바꾸고, Full Route Cache의 적용 여부까지 결정합니다. 코드의 지역적인 의미와 시스템 전체에서의 의미가 달라지는 이런 비지역적 효과(non-local effect)가 많아질수록, 코드를 읽는 것만으로는 동작을 온전히 파악하기 어려워집니다. 이와 더불어 Next.js의 캐싱 전략은 개발자들로 하여금 많은 혼란을 야기하였습니다. App Router의 캐시 구조는 '캐시됨/캐시 안 됨'이라는 단순한 이분법으로 이해할 수 없습니다. Next.js 문서 기준으로는 네 가지 계층이 존재합니다. Request Memoization은 한 번의 서버 렌더링 중 동일한 fetch 호출이 여러 컴포넌트에서 반복될 때 중복 요청을 막는 React 레벨의 최적화로, 요청이 끝나면 사라집니다. 요청을 넘어 유지되는 것은 Data Cache, Full Route Cache, 그리고 브라우저의 Router Cache입니다. Data Cache는 서버 요청을 넘어 지속되는 캐시로, 동일한 데이터를 여러 사용자 요청에 걸쳐 재사용합니다. Full Route Cache는 렌더링 결과인 HTML과 RSC Payload 자체를 저장하며, Data Cache와는 별개의 계층이지만 서로 영향을 주는 구조입니다. 브라우저에도 Router Cache가 있어서 이전에 방문했거나 prefetch된 route의 RSC Payload를 메모리에 저장합니다. 그래서 DB를 수정했는데 화면이 그대로일 때, 개발자는 특히 요청을 넘어서 유지되는 이 세 계층이 얽힌 상황에서 어디에 오래된 값이 살아 있는지 하나씩 추적해야 합니다. 데이터를 갱신하는 코드가 DB 수정 한 줄로 끝나지 않고, 어느 캐시를 무효화할 것인지, revalidatePath인지 revalidateTag인지, 클라이언트까지 router.refresh()가 필요한지까지 포함하게 됩니다. App Router 초기에는 성능을 극대화하려는 방향으로 캐싱이 공격적이었습니다. 명시하지 않은 서버 fetch가 force-cache 방향으로 동작했고, GET Route Handler도 조건에 따라 기본 캐시 대상이 됐습니다. 개발자가 데이터를 가져오는 코드를 작성할 때 '지금 데이터를 가져오는 것'인지 'Next.js가 저장해둔 데이터를 쓰는 것'인지 코드만으로는 알 수 없었습니다. Next.js 15에서 이 기본값을 크게 바꾼 것은 Next.js 팀 스스로 성능을 우선한 공격적인 기본값이 실제 개발자의 직관과 충돌했음을 인정한 결과였습니다. 초기의 추상화와 기본값이 개발자 경험 측면에서 충분히 안정적이지 않았음을 보여주는 사례입니다. 호스팅 환경에 대해서도 한 가지 짚어둘 필요가 있습니다. Next.js는 공식적으로 self-hosting을 지원하고, next start를 실행하면 이미지 최적화, Middleware, ISR, Streaming 등 대부분의 기능이 동작합니다. 문제는 규모가 커졌을 때입니다. 여러 인스턴스를 띄우면 각 서버가 자체 로컬 캐시를 가지기 때문에, 한 인스턴스가 갱신한 캐시를 다른 인스턴스는 알 수 없습니다. 캐시 일관성이 필요한 경우에는 Redis나 S3 같은 공유 저장소를 기반으로 한 custom cache handler가 필요하고, Nginx를 앞에 두면 버퍼링 설정을 따로 조정해야 스트리밍이 제대로 동작합니다. 이것이 Next.js가 Vercel에서만 동작한다는 뜻은 아닙니다. 다만 프레임워크가 추상화해주던 인프라의 복잡도가 자체 인프라에 배포하는 순간 다시 개발팀의 책임으로 모습을 드러낸다는 뜻입니다. App Router를 구성하는 각각의 아이디어는 떼어놓고 보면 상당히 논리적입니다. 정적 페이지는 캐시하면 빠르고, 같은 데이터를 여러 컴포넌트에서 중복 요청할 이유가 없고, RSC를 쓰면 클라이언트 번들을 줄일 수 있고, 스트리밍을 쓰면 느린 데이터 하나 때문에 전체 페이지가 늦어지는 것을 막을 수 있습니다. 문제는 그것들이 합쳐지면 개발자가 '이 컴포넌트는 서버인가 클라이언트인가, 이 route는 정적인가 동적인가, 이 fetch는 어느 캐시 계층에 들어가는가, 데이터가 바뀌면 어느 캐시를 무효화해야 하는가, 여러 인스턴스를 띄울 때 캐시 일관성은 어떻게 유지하는가'를 모두 머릿속에 들고 있어야 한다는 것입니다. 그렇게 이해하고 나서 지금 만들고 있는 페이지를 다시 보면 단순한 회사 소개, 블로그, 서비스 소개, 문의 폼...일 수 있습니다....... 먼 길을 돌아 다시 제자리 이러한 복잡함을 겪으면서 제 안에서는 근본적인 질문이 계속되었습니다. 서버에서 완성된 HTML을 내려주는 것이 필요하다면, 원래 그렇게 동작하던 전통적인 웹 개발 방식을 쓰면 되지 않을까? 웹의 기본 모델은 단순합니다. 서버는 요청에 HTML 문서를 응답하고, 브라우저는 그 문서를 렌더링합니다. 그 HTML은 정적 파일일 수도 있고, PHP 같은 서버 프로그램이 요청마다 생성한 결과일 수도 있습니다. 상호작용이 필요한 부분에만 자바스크립트를 더하는 것, 이 구조에서 검색엔진은 아무런 추가 장치 없이 완성된 문서를 읽습니다. 우리는 이 방식을 떠나 클라이언트에서 모든 것을 그리는 방향으로 이동했다가, 그로 인해 생긴 문제를 해결하기 위해 다시 서버로 돌아오고 있습니다. 다만 돌아오는 과정에서 훨씬 복잡한 구조를 짊어지고 왔습니다. 전통적인 React SSR에서 대표적으로 따라오는 비용이 바로 하이드레이션(hydration)입니다. 서버가 HTML을 만들어 보내더라도, 브라우저는 서버에서 만들어진 DOM을 재사용하면서 해당 컴포넌트의 렌더링 로직을 자바스크립트로 다시 실행해 이벤트 핸들러를 연결하고 React 트리와 맞춰야 합니다. SSR은 자바스크립트를 없앤 것이 아니라, 자바스크립트를 실행하기 전에 보여줄 HTML을 먼저 만들어둔 것에 가깝습니다. 화면은 빨리 보이지만 실제로 상호작용이 가능해지기까지는 시간이 걸리며, 서버와 클라이언트의 렌더링 결과가 조금이라도 어긋나면 하이드레이션 불일치 오류가 발생합니다. 그럼에도 이 여정 자체를 부정하고 싶은 것은 아닙니다. 앞서 살펴본 것처럼 리액트가 서버 영역으로 확장된 데에는 실제로 존재하는 많은 문제들이 있었고, 그 해법은 그 문제를 가진 서비스로서는 분명히 가치가 있었을 겁니다. 돌이켜보면 리액트가 걸어온 길에는 흥미로운 왕복 운동이 있습니다. 서버가 HTML을 만들던 전통적인 방식을 떠나 브라우저에서 모든 것을 그리는 방향으로 갔다가, 그로 인해 생긴 성능과 검색 문제를 해결하기 위해 다시 서버로 돌아갔다는 것, 이번에는 서버 컴포넌트, 서버 함수, 캐시 전략, 클라이언트와 서버의 실행 경계까지 함께 결정해야 하는 훨씬 복잡한 구조를 짊어지고서 말입니다. 매우 먼 길을 돌아, 서버가 HTML을 만들어 보내던 웹의 오래된 구조를 훨씬 복잡한 형태로 다시 발견하고 있는 것은 아닐까요. 한두 페이지짜리 사이트에 리액트가 필요할까 회사 소개 페이지, 이벤트 랜딩 페이지처럼 한두 페이지로 끝나는 정적 사이트를 만드는 데에도 리액트가 당연하게 선택되는 경우를 자주 보았습니다. 이 경우 개발자는 번들러를 설정하고, 수백 개의 패키지가 들어간 node_modules를 관리하고, 빌드와 배포 파이프라인을 구성해야 합니다. 물론 node_modules의 크기가 그대로 사용자에게 전달되는 것은 아닙니다. 하지만 단순한 정적 HTML로 충분했던 페이지에 리액트 런타임과 하이드레이션을 위한 자바스크립트까지 전달한다면 그만큼 다운로드와 파싱, 실행 비용이 추가될 수 있습니다. 의존성이 늘어날수록 업데이트와 호환성 관리 부담도 함께 늘어납니다. 해결하려는 문제에 비해 개발 환경과 런타임의 복잡성이 커지는 셈입니다. 보안 측면에서도 마찬가지입니다. 리액트가 아닌 기술을 이야기하면 흔히 보안 취약성이 거론되지만, 리액트 생태계 역시 보안에서 자유롭지 않습니다. npm 생태계는 수많은 전이 의존성 위에 서 있고, 패키지 탈취나 악성 코드 삽입 같은 공급망 공격은 반복적으로 발생해 왔습니다. 리액트가 서버로 영역을 넓히면서 공격 표면도 함께 넓어졌습니다. 2025년에는 React Server Components의 서버 함수 payload 처리 과정에서 인증 없이 원격 코드 실행이 가능한 CVE-2025-55182가 공개되기도 했습니다. 서버에서 코드를 실행하는 순간, 서버 개발의 보안 책임도 함께 지게 되는 것입니다. 어떤 기술이든 보안은 관리해야 할 대상이며, 구조가 단순하고 의존성이 적을수록 관리해야 할 지점은 줄어듭니다. 저는 무엇보다도, 리액트가 아닌 기술을 "레거시"라는 한 마디로 폄하하는 분위기가 답답했던 것 같습니다. 기업이 리액트를 고집하는 데에는 채용 시장의 풍부함, 기존 코드베이스와의 연속성, 팀 내 지식의 공유 같은 현실적인 이유가 있습니다. 그러한 판단 자체는 이해할 수 있습니다. 하지만 조직의 관성이 기술적 판단을 대신하기 시작하면, 우리는 문제를 보는 대신 도구를 보게 됩니다. 웹 개발의 역사는 서버 렌더링에서 SPA로, 다시 서버 중심으로 흘러가며 순환해 왔습니다. 오늘의 표준이 내일의 레거시가 되고, 어제의 방식이 다시 재발견되는 것이 이 분야의 흐름입니다. 최근에는 서버 중심의 렌더링을 다시 적극적으로 활용하려는 흐름도 뚜렷해지고 있습니다. Astro는 페이지 대부분을 정적 HTML로 내보내고 상호작용이 필요한 영역만 독립적으로 하이드레이션하는 아일랜드 아키텍처를 내세웁니다. htmx는 HTML 속성만으로 서버와 통신해 페이지 일부를 교체하는 방식을 제안하며, Rails의 Hotwire와 Laravel의 Livewire도 서버가 HTML을 만들고 클라이언트는 최소한의 역할만 하는 구조를 지향합니다. 모두 "서버가 문서를 만들고, 필요한 곳에만 상호작용을 더한다"는, 웹이 원래 가지고 있던 철학을 다시 꺼내 든 것입니다. AI 시대의 바닐라 자바스크립트 이런 생각 끝에 저는 개인적인 작업에서는 가능한 한 바닐라 자바스크립트를 사용하고, 리액트는 꼭 필요한 경우에만 쓰겠다는 생각을 하게 되었습니다...... 아니, 가능한 한 바닐라 자바스크립트를 사용한다기보다는, 굳이 프레임워크가 필요 없는 간단한 작업에까지 프레임워크라는 거대한 짐을 이고 지고 짊어지진 않겠다는 말이 더 정확할 것 같습니다. 이 선택을 가능하게 한 것은 두 가지의 변화입니다. 하나는 웹 플랫폼 자체의 성숙도가 올라갔다는 점입니다. 과거에 프레임워크나 라이브러리가 필요했던 기능들 상당수가 이제 브라우저 표준으로 제공됩니다. ES 모듈을 통해 빌드 없이 코드를 분리할 수 있고, fetch로 비동기 통신을 처리하며, 템플릿 리터럴로 마크업을 조립할 수 있습니다. <dialog> 요소는 모달을, Web Components는 재사용 가능한 컴포넌트를, View Transitions API는 화면 전환 애니메이션을 표준으로 지원합니다. CSS 역시 :has() 선택자와 컨테이너 쿼리, 네이티브 중첩 문법을 갖추면서 예전에는 자바스크립트로 처리하던 일들을 상당 부분 흡수했습니다. 다른 하나로는, 역시 AI의 발달을 들 수 있겠습니다. 예전에는 바닐라 자바스크립트로 기능을 구현하려면 반복적인 코드를 직접 작성해야 했고, 그 수고를 덜기 위해서라도 프레임워크에 의존했습니다. 이제는 AI의 도움으로 그러한 비용이 크게 줄었습니다. 빌드 과정 없이 브라우저에서 바로 실행되는 코드는 디버깅이 직관적이고, 의존성 업데이트에 시달릴 일도 없습니다. 다만 이 방식에도 한계가 있다는 점은 분명히 해두고 싶습니다. 애플리케이션의 규모가 커지면 상태 관리, 효율적인 DOM 갱신, 컴포넌트 간 통신 같은 문제를 결국 직접 해결해야 하고, 그 과정에서 문서도 규칙도 없는 무근본 자체 프레임워크(프레임워크를 의도하지 않았지만 어찌 저찌 나도 모르게 프레임워크가 되어 버리는)가 만들어지기도 쉽습니다. AI가 생성한 코드가 일관된 구조 없이 쌓이면 유지보수는 오히려 어려워집니다. 팀 단위 개발에서 프레임워크가 주는 공통의 규칙과 어휘는 여전히 큰 가치를 지닙니다. 다만, 그저 저에게 있어서 바닐라 자바스크립트는 작은 규모의 작업에서 불필요한 복잡성을 걷어내는 선택지일 뿐인 것입니다. 처음 다뤄본 워드프레스와 PHP 그리고 지금 저는 개인 블로그를 구축하기 위해 처음으로 워드프레스와 PHP를 다루고 있습니다. 가장 먼저 놀란 것은 생태계의 규모입니다. 워드프레스는 전 세계 웹사이트의 40% 이상을 구동하는 것으로 알려져 있습니다. 블로그를 구축하기 위해 필요한 기능 대부분은 이미 테마나 플러그인으로 구현되어 있고, 관련 문서와 커뮤니티의 축적량도 방대합니다. 구조를 들여다보면 그 설계가 꽤 실용적이라는 것도 알게 됩니다. 워드프레스의 확장성은 훅(hook) 시스템에 기반합니다. 특정 시점에 코드를 실행하는 액션(action)과, 데이터가 출력되기 전에 값을 가공하는 필터(filter)를 통해 코어를 수정하지 않고도 거의 모든 동작을 바꿀 수 있습니다. 템플릿 계층 구조는 요청된 페이지의 종류에 따라 어떤 템플릿 파일을 사용할지를 규칙적으로 결정하므로, 파일 이름만으로도 페이지별 화면을 쉽게 분리할 수 있습니다. WP_Query로 원하는 조건의 글을 조회하고, 내장된 REST API를 통해 콘텐츠를 외부에 제공할 수도 있습니다. 흥미로운 점은 워드프레스의 블록 에디터 일부는 리액트로 만들어졌다는 사실입니다. 글을 작성하는 편집 화면처럼 상호작용이 복잡한 영역에는 리액트를 쓰고, 방문자에게 보여지는 페이지는 서버에서 PHP가 완성된 HTML로 렌더링합니다. 워드프레스에서는 기본적으로 서버에서 HTML을 만들지만, 필요한 블록이나 기능에 따라서는 클라이언트단에서 자바스크립트를 추가할 수 있는데, 저는 이것이 도구를 적재적소에 배치한 좋은 사례라고 생각합니다. 블로그처럼 콘텐츠가 중심인 사이트에서 이 구조는 매우 자연스럽습니다. 글을 발행하면 서버가 완성된 HTML을 응답하고, 검색엔진은 그 문서를 그대로 읽습니다. 검색엔진으로 하여금 본문을 보게 하기 위해 CSR이니 SSR이니 하는 전략을 따로 고민할 필요가 없습니다. 제가 리액트와 Next.js로 먼 길을 돌아 도달하려 했던 결과를, 워드프레스는 처음부터 기본 동작으로 제공하고 있었습니다. PHP는 레거시가 아니다 많은 웹 프런트엔드 개발자들이 PHP를 레거시 기술로 치부하지만, 실제로 지금 이렇게 저렇게 php를 만져보면서, 저는 이러한 평가에 쉽게 동의할 수 없다는 생각이 듭니다. 아마 php가 레거시라는 인식의 상당 부분은 PHP 5 시절, HTML과 SQL과 로직이 한 파일에 뒤섞인 코드와 잦은 보안 사고의 기억에서 비롯된 것이 아닐까 생각합니다. 현재의 PHP는 과거의 PHP와는 많이 다릅니다. PHP 7에서 엔진이 재설계되며 성능이 크게 향상되었고, PHP 8에서는 JIT(just-in-time) 컴파일러, 유니언 타입, match 표현식, 네임드 인수, 어트리뷰트가 도입되었습니다. 이후 버전에서도 열거형(enum), readonly 속성, 파이버(Fiber) 등이 추가되며 언어는 꾸준히 현대화되었습니다. 엄격한 타입 선언을 활용하면 정적 분석 도구와 함께 상당히 견고한 코드를 작성할 수 있고, Composer를 중심으로 한 패키지 생태계와 Laravel, Symfony 같은 프레임워크도 성숙해 있습니다. 무엇보다 전통적인 PHP-FPM 기반 웹 애플리케이션에서는 애플리케이션의 사용자 영역 상태가 기본적으로 요청 단위의 생명주기를 가진다는 장점이 있습니다. 요청 처리가 끝나면 다음 요청은 다시 초기화된 상태에서 시작하므로, 애플리케이션 프로세스에 상태를 오래 보관하는 서버와 비교하면 요청 간 상태 공유에 대해 신경 써야 할 지점이 적습니다. 물론 FPM worker 자체는 여러 요청에 걸쳐 재사용되고, OPcache나 persistent connection처럼 요청을 넘어 유지되는 상태도 있습니다. 최근의 FrankenPHP나 Swoole 같은 long-running PHP 런타임에서는 이 전제가 달라지기도 합니다. 하지만 전통적인 PHP 배포에서는 언어 자체가 필수적인 애플리케이션 빌드 단계를 요구하지 않기 때문에, 단순히 소스 코드를 배포하는 것만으로도 프로덕션 런타임 실행이 가능합니다. 저는 이것을 낡은 방식이 아니라 그저 단순한 방식이라고 부르고 싶습니다. 물론 워드프레스를 쓴다고 모든 것이 해결되는 것은 아닙니다. 워드프레스 생태계에서 공개되는 취약점의 상당수는 코어 자체보다 서드파티 플러그인과 테마에서 발견됩니다. 플러그인을 무분별하게 설치하면 성능 저하와 보안 위험이 함께 커지므로, 필요한 것만 신중하게 고르고 꾸준히 업데이트하는 관리가 필요합니다. 다만 이는 자바스크립트 또는 타입스크립트를 사용하면서 npm 의존성을 꾸준히 관리하는 것과도 비슷한 문제라고 저는 생각합니다. 어떤 생태계든, 외부 코드를 가져다 쓰는 순간 그에 따른 책임은 따라오기 마련입니다. 도구는 문제에 맞춰 고른다 다시 강조하지만, 저는 리액트를 쓰지 말자고 주장하는 것이 아닙니다. 상태가 복잡하게 얽힌 대시보드, 여러 사용자가 실시간으로 협업하는 도구, 화면 전환 없이 끊임없이 상호작용하는 웹 애플리케이션이라면 리액트는 여전히 훌륭한 선택입니다. 워드프레스의 에디터 일부가 리액트로 만들어진 것처럼, 리액트가 빛나는 자리는 분명히 존재합니다. 제가 말하고 싶은 것은, 그저, 도구는 문제에 맞춰 골라야 하며, 도구에 문제를 맞춰서는 안 된다는 것. 콘텐츠 중심의 블로그라면 워드프레스가 가장 합리적인 선택일 수 있습니다. 한두 페이지짜리 정적 사이트라면 HTML과 CSS, 약간의 자바스크립트로 충분합니다. 콘텐츠 사이트에 부분적인 상호작용이 필요하다면 Astro나 htmx 같은 선택지를 고려할 수 있습니다. 복잡한 웹 애플리케이션이라면 그때 리액트를 꺼내면 됩니다. 이 판단을 제대로 내리려면 여러 도구를 알아야 하고, 각각의 도구를 편견 없이 평가할 수 있어야 합니다. 리액트로 실무를 해본 경험이 있었기에 저는 지금 워드프레스와 PHP의 장점을 더 선명하게 볼 수 있습니다. 반대로 워드프레스와 PHP를 경험하면서, 리액트가 정말로 필요한 순간이 언제인지도 더 분명해졌습니다. 어느 한쪽을 버릴 필요는 없습니다. 각자의 자리에 두면 됩니다. "요즘 누가 그걸 써요"라는 말보다 "이 문제에는 무엇이 맞을까요"라는 질문이 더 자주 오가는 개발 문화를 바랍니다. 기술을 고르는 기준은 유행이 아닌 우리가 해결해야 할 문제 그 자체이기를 바라는 마음입니다.