현대 프런트엔드 복잡성이 어디서 비롯되었는지, 무엇이 본질적이고 무엇이 우발적인지 살펴보고, HTMX와 HTML Web Components, 서버 사이드 렌더링을 활용한 더 단순한 대안을 제안합니다.
I0I
0
2026-04-18
이 복잡성 의 뿌리는 무엇일까요? 우리는 어떻게 여기까지 오게 되었을까요?
옛날 웹의 새벽 무렵, 브라우저와 웹사이트는 단순했습니다. 사실상 앱이라고 할 만한 것은 거의 없었고, 대부분은 좀 더 보기 좋게 만들기 위한 약간의 CSS가 곁들여진 정적 페이지, 즉 .html 파일들의 모음이었습니다. 이런 웹사이트들은 대체로 텍스트 기반이었고, World Wide Web 상에서 이용 가능한 다른 유사한 문서들로 연결되어 있었습니다. 모든 것은 평범하고 단순했습니다. 서로를 참조하는 정적 문서들이었죠.
그러다 천천히, 한 걸음씩, 점점 더 많은 상호작용성이 추가되었습니다. 먼저 폼과 입력 요소가 등장했고, 그리 오래 지나지 않아 JavaScript 프로그래밍 언어도 등장했습니다(둘 다 1995년).
이 단계에서 복잡성 은 여전히 낮았습니다. 당시 개발되던 웹 시스템은 대부분 다음으로 이루어져 있었습니다.
.html 문서와 템플릿.css 파일 하나 또는 여러 개.js 스크립트중요한 점은, 이 초기 웹사이트와 앱의 UI 소스 코드는 대부분 브라우저가 해석하고 실행하는 출력 파일, 즉 런타임 대상과 거의 같았다는 것입니다. PHP 와 템플릿 언어/시스템(Mustache 같은)을 사용하더라도, 브라우저가 표시하는 최종 HTML 파일과 매우 비슷해 보였습니다.
<h1>{{page.title}}</h1>
<div>
<p>{{name.label}}: {{user.name}}</p>
<p>{{email.label}}: {{user.email}}</p>
<p>{{language.label}}: {{user.language}}</p>
</div>
<a href="/sign-out">{{sign-out}}</a>
템플릿 엔진은 서버 런타임/환경에서 사용할 수 있는 하나의 라이브러리일 뿐이며, 이것을 특정 HTML 페이지로 바꿉니다.
<h1>User Account</h1>
<div>
<p>Name: Igor</p>
<p>Email: [email protected]</p>
<p>Language: EN</p>
</div>
<a href="/sign-out">Sign Out</a>
정적인 .html 문서 모음보다는 조금 더 복잡하지만, 여전히 상당히 직관적입니다. 그다음에는 무슨 일이 일어났을까요?
그다음 AJAX가 등장했습니다 - Asynchronous JavaScript and XML 의 이상한 약자입니다. 이것은 전체 페이지를 다시 로드하지 않고도 비동기적으로 백그라운드에서 HTML 문서 내용을 업데이트할 수 있는 완전히 새로운 가능성을 가져왔습니다. 이 시점부터 점점 더 많은 웹사이트 기능이 점점 더 복잡해지는 JavaScript에 위임되기 시작했습니다. 특히 부분 업데이트를 위해서였고, 이는 대체로 더 정교한 사용자 상호작용에 의해 트리거되어 전체 페이지 새로고침을 피하게 해주었습니다. 그로부터 얼마 지나지 않아 Single Page Application (SPA) 개념 과 최초의 프레임워크들이 등장했습니다. Backbone.js, Knockout.js 그리고 AngularJS (2010) 가 그것입니다. 이 모델에서는 우리가 작업하는 소스 코드와 최종적으로 브라우저 환경에 도달하는 결과물 사이의 거리가 매우 멉니다. 더 정교한 추상화도 함께 등장했고, 그 결과 필요한 도구 체인은 더욱 복잡해졌습니다.
이렇게 해서 대략 오늘날의 복잡성 에 도달하게 되었습니다. 오늘날에는 대부분의 앱이 React, Vue, Angular 또는 Svelte로 만들어지며, 빌드와 개발을 위해 Vite 나 Webpack 같은 전체 도구 체인이 필요합니다. 이들이 동작하는 방식은 브라우저가 원래 설계된 방식과 본질적으로 다릅니다.
소스 코드 형식과 브라우저 런타임 사이의 간극이, 새롭게 발견되고 채택된 이러한 추상화들 때문에, 점점 더 커지면서 웹 애플리케이션을 개발, 빌드, 배포하기 위해 더 많은 도구와 점점 더 복잡한 도구가 필수가 되었습니다.
React로 작성되고, TypeScript를 사용하며, 개발 및 빌드에 Vite를 사용하는 전형적인 현대 SPA를 생각해봅시다. 브라우저가 이해할 수 있게 만들기 위해서는 다음이 필요합니다.
.jsx 파일을 어떻게 처리해야 하는지 전혀 모릅니다.index.html HTML 파일만 가진 SPA이고, 잠재적으로 수십, 수백, 심지어 수천 개의 작은 .js 파일을 가질 수 있습니다. 성능상의 이유로, 이 파일들은 하나의.js번들 (또는 몇 개의 번들)로 패키징되어야 합니다. 최소한 React는 의존성으로서 사용 가능해야 하므로, 최종 번들에 함께 추가되어야 합니다..js 파일을 가능한 한 작게 만듭니다.여기에 추가 단계가 더 있을 수도 있습니다.
분명히 알 수 있듯이, 이건 정말 많습니다! 물론 이러한 모든 변환을 수행하는 스크립트를 직접 작성하는 것은 매우 비실용적일 것입니다. 그래서 Webpack, Turbopack, Vite 같은 빌드 도구가 존재합니다. 물론 이것들은 또 하나의 의존성을 도입합니다. 새롭게 배워야 하고 숙달해야 할 무언가죠. 하지만 우리는 브라우저가 실제 런타임에서 다루는 것과 너무 멀리 떨어진 곳까지 와버렸기 때문에, 이런 도구들은 오히려 필수적입니다. 과거 브라우저의 한계 때문에 순전히 역사적인 이유로 이런 방식으로 발전했다는 매우 설득력 있는 주장을 할 수도 있습니다. 예를 들어 오랫동안 native modules 가 없었습니다.
현재 생태계의 복잡성은 Tower of Babel 에 맞먹습니다. 그래서 저는 이렇게 묻고 싶습니다.
최근 몇 년간 브라우저가 발전해온 모습을 고려할 때, 완전히 처음부터 다시 시작해서 훨씬 더 단순한 접근법을 찾아볼 수는 없을까요?
대부분의 웹 앱에서 오늘날 사용자들이 당연하게 여기는 것들:
프로그래머가 원하는 것들:
여기 하나의 아이디어가 있습니다.
완전히 동작하는 예제는 이 저장소 에서 볼 수 있습니다. 가장 중요하고 흥미로운 부분들을 살펴보겠습니다.
예제에서는 Java와 Spring Boot 프레임워크를 사용해 서버를 작성했습니다. 하지만 웹 개발에 적합한 다른 어떤 프로그래밍 언어나 프레임워크로도 작성할 수 있었을 것입니다. 저는 이것을 서버라고 부르는데, 이 접근법에서는 사실 프런트엔드/백엔드 구분이 거의 없기 때문입니다. 서버가 대부분의 뷰를 렌더링하고, 여기저기에 약간의 클라이언트 측 JS가 뿌려진 하나의 앱이 있을 뿐입니다.
여러 엔드포인트에서 렌더링된 HTML 페이지 또는 조각이 다음과 같이 반환됩니다:
@GetMapping("/devices")
String devices(Model model, Locale locale,
@RequestParam(required = false) String search) {
translations.enrich(model, locale, Map.of("devices-page.title", "title"),
"devices-page.title",
"devices-page.search-input-placeholder",
"devices-page.search-indicator",
"devices-page.trigger-error-button");
enrichWithDevicesSearchResultsTranslations(model, locale);
var devices = deviceRepository.devices(search);
return templatesResolver.resolve("devices-page",
devicesModel(model, devices));
}
이는 맥락에 따라 다음과 같이 동작합니다.
/devices url로 이동한 경우에는 HTML 조각을 반환합니다전체 HTML 페이지를 반환할지 조각을 반환할지 어떻게 알 수 있을까요?
다행히도 HTMX는 자신이 보내는 각 HTTP 요청에 hx-request 헤더를 추가합니다. 따라서 HTTP 요청에 hx-request 헤더가 없다면, 우리의 응답은 전체 HTML 페이지입니다.
<!DOCTYPE HTML>
<html lang="en">
...
<body>
{{ page-specific-html }}
</body>
</html>
그리고 이것이 후속 요청, 즉 전체 페이지 새로고침 없이 한 페이지에서 다음 페이지로 클릭해 이동하는 경우라면 hx-request 헤더가 존재하고, 우리는 HTML 조각을 반환합니다.
{{ page-specific-html }}:
<div class="space-y-2 flex flex-col">
...
<div class="cursor-pointer rounded border-2 p-0 flex">
<span class="px-4 py-2 flex-1">9b0d5f33-6f9e-4aef-bb81-a57a045fb1aa: iPhone 13</span>
<drop-down class="relative">
<div data-drop-down-anchor class="absolute right-2 text-3xl">...</div>
<div data-drop-down-options class="rounded border-2 whitespace-nowrap absolute mt-2 right-0 top-6 bg-white border rounded hidden z-99">
<div class="p-2" hx-get="/devices/9b0d5f33-6f9e-4aef-bb81-a57a045fb1aa" hx-push-url="true" hx-target="#app">Details</div>
<div class="p-2" hx-get="/buy-device/9b0d5f33-6f9e-4aef-bb81-a57a045fb1aa" hx-push-url="true" hx-target="#app">Buy</div>
</div>
</drop-down>
</div>
...
</div>
이것은 다음과 같이 보입니다.

Devices page
여기서 주목할 만한 흥미로운 점이 몇 가지 있습니다.
hx- 속성들(HTMX): hx-get, hx-push-url, hx-target<drop-down> 요소(Web Component)우선 hx- 동작 방식부터 시작해보겠습니다.
Details 또는 Buy 옵션을 클릭하면, 브라우저 url은 HTMX에 의해 표준 History API 를 사용해 변경됩니다. 동시에 HTMX는 각각 /devices/9b0d5f33-6f9e-4aef-bb81-a57a045fb1aa 또는 /buy-device/9b0d5f33-6f9e-4aef-bb81-a57a045fb1aa 로 GET 요청을 보냅니다. app id로 식별되는 HTML 요소의 내용은 서버에서 받은 HTML 조각으로 교체됩니다. 그 결과 전체 페이지 새로고침 없이 새로운 HTML 페이지를 보게 됩니다. 동작 방식은 전통적인, 클라이언트 중심 & JSON 지향 SPA 와 정확히 같습니다.

Details option

Buy option
이것은 Web Components를 개발하는 또 다른 전략으로, 구조는 HTML에서 전부 또는 대부분 정의되고, 컴포넌트는 JavaScript를 통해 거기에 동작만 추가합니다.
<drop-down> 을 예로 들면(Mustache 템플릿):
<drop-down class="relative">
<div data-drop-down-anchor class="absolute right-2 text-3xl">...</div>
<div data-drop-down-options class="rounded border-2 whitespace-nowrap absolute mt-2 right-0 top-6 bg-white border rounded hidden z-99">
<div class="p-2" hx-get="/devices/{{id}}" hx-push-url="true" hx-target="#app">{{devices-search-results.details-option}}</div>
<div class="p-2" hx-get="/buy-device/{{id}}" hx-push-url="true" hx-target="#app">{{devices-search-results.buy-option}}</div>
</div>
</drop-down>
보시다시피 anchor 와 options 요소는 각각 data-drop-down-anchor 와 data-drop-down-options 로 표시되어 있습니다. 그렇다면 <drop-down> 은 무엇을 할까요?
class DropDown extends HTMLElement {
#hideOnOutsideClick = undefined;
connectedCallback() {
const anchor = this.querySelector("[data-drop-down-anchor]");
const options = this.querySelector("[data-drop-down-options]");
anchor.onclick = () => options.classList.toggle("hidden");
this.#hideOnOutsideClick = (e) => {
if (e.target != anchor) {
options.classList.add("hidden");
}
};
window.addEventListener("click", this.#hideOnOutsideClick);
}
disconnectedCallback() {
window.removeEventListener("click", this.#hideOnOutsideClick);
}
}
이것은 HTML 구조를 변경하지 않습니다. 대신 특정 요소들에 동적인 드롭다운 동작을 부여합니다.
비슷한 방식으로 구현된 컴포넌트가 몇 가지 더 있습니다.
이 접근법 덕분에 UI는 대부분 서버 측에서 렌더링되며, 이는 SEO와 성능상의 이점을 줍니다. 대부분이 HTML에서 처리되기 때문에 작성해야 하는 JavaScript 양도 줄어들고, 주로 서버가 처리하므로 테스트하고 올바름을 검증하기도 더 쉽습니다. 단점은 무엇일까요? 작성해야 할 HTML이 더 많고, 때로는 몇 가지 스타일링 의존성을 알아야 할 수도 있다는 점입니다. <drop-down> 예시처럼 말이죠.
<drop-down class="relative">
<div data-drop-down-anchor class="absolute">...</div>
<div data-drop-down-options class="absolute mt-2 right-0 top-6 hidden z-99">
...
</div>
</drop-down>
예상한 대로 <drop-down> 이 표시되려면 부모에는 relative 디스플레이가, 자식에는 absolute 가 설정되어야 하므로, 누군가는 이것이 행동이 캡슐화된 진정한 독립 컴포넌트는 아니라고 주장할 수 있습니다. 하지만 이 철학 덕분에 우리는 많은 유연성을 얻습니다. 여기서는 구조와 스타일링이 아니라 동작만 제공되기 때문에, 거의 모든 것이 설정 가능합니다. 범용적이고 재사용 가능한 컴포넌트를 만드는 맥락에서, 이것은 분명 감수할 만한 트레이드오프입니다. 특히 스타일링과 설정의 일부 패턴이 반복된다면, 아무도 그러한 경우를 위한 전용의 더 구체적인 래퍼를 만드는 것을 막지 않기 때문입니다.
오류가 사용자가 지원되지 않거나 문제가 있는 url을 입력했기 때문에 발생한 경우(전체 페이지 로드), 번역된 예외 메시지를 담은 전용 오류 페이지가 표시됩니다.

Error page
하지만 대부분의 경우에는 HTMX가 우리를 대신해 데이터를 가져오고 변경을 트리거합니다. 이때 실패해서 non-2xx 코드를 받으면, 우리는 다음과 같이 처리합니다.
<error-modal>
...
</error-modal>
...
<script>
document.addEventListener("htmx:afterRequest", e => {
if (e.detail.failed) {
const errorModal = document.querySelector("error-modal");
const error = e.detail.xhr.response;
const [title, message] = error.split("#");
errorModal.dispatchEvent(new CustomEvent("error-modal-show",
{ detail: { title: title, content: message }}));
}
});
</script>
오류가 발생하면, 번역된 오류 제목과 메시지가 # 기호로 구분되어 전달됩니다. 우리가 해야 할 일은 <error-modal> 이 수신하는 사용자 정의 이벤트를 발행하는 것뿐입니다.

ErrorModal
인라인 검증의 경우에는 불필요하게 백엔드를 치고 싶지 않을 것입니다. 이를 위해 표준 <input> 요소를 감싸는 <validateable-input> 컴포넌트가 있습니다. 설정된 validator 가 true 또는 false 를 반환하는지에 따라 validation error 를 숨기거나 보여줄 수 있게 해줍니다.

ValidateableInput
언급했듯이, 이러한 모든 메시지는 사용자 언어로 번역됩니다. 이것은 어떻게 동작할까요?
모든 번역은 UI가 서버에서 렌더링되기 때문에 서버에 존재하며, 우리는 단순한 message_{locale}.properties 파일을 사용합니다.
devices-page.title=Devices
devices-page.search-input-placeholder=Search devices...
devices-page.search-indicator=Searching devices...
devices-page.trigger-error-button=Trigger some error
사용자 언어는 표준 Accept-Language header 를 기반으로 결정되지만, 쿠키, 쿼리 파라미터, 또는 서버에 저장된 사용자별 설정/상태를 통해서도 결정할 수 있습니다.
이 전략의 또 다른 장점은 테스트 가능성이 향상된다는 점입니다. 왜 그럴까요?
HTML 페이지와 조각은 거의 전부 서버 측에서 생성됩니다. 때때로 Web Components나 인라인 스크립트를 통해 JavaScript가 추가되어, 순수하게 클라이언트 측 동작을 컴포넌트에 부여합니다. 이 패턴은 때때로 Islands Architecture 라고 불립니다. 이 대부분을 테스트하고 검증하기 위해 우리가 해야 할 일은 이런 종류의 서버 통합 테스트를 작성하는 것 뿐입니다.
@Test
void rendersFullDevicesPage() {
var allDevices = deviceRepository.allDevices();
var response = testRestClient.get()
.uri("/devices")
.retrieve()
.toEntity(String.class);
assertThat(response.getStatusCode())
.isEqualTo(HttpStatus.OK);
var document = Jsoup.parse(response.getBody());
assertThat(document.select("html"))
.isNotEmpty();
var devicesElement = document.select("#devices");
allDevices.forEach(device -> {
assertThat(devicesElement.text())
.contains(device.id().toString())
.contains(device.name());
var devicePageAttribute = "[hx-get=/devices/%s]".formatted(device.id());
var buyDevicePageAttribute = "[hx-get=/buy-device/%s]".formatted(device.id());
assertThat(devicesElement.select(devicePageAttribute))
.isNotEmpty();
assertThat(devicesElement.select(buyDevicePageAttribute))
.isNotEmpty();
});
}
이 방식으로 우리는 사실상 애플리케이션의 거의 모든 레이어를 한 번에 테스트합니다.
물론 이것이 실제 최종 사용자 환경에서 렌더링되는 것은 아니지만, jsoup (또는 유사한 도구)를 사용하면 브라우저에서도 잘 렌더링될 것이라는 높은 수준의 확신을 가질 수 있습니다.
그렇다면 JavaScript를 사용해 관련 기능과 동작을 제공하는 UI 상태와 컴포넌트는 어떻게 할까요? 그런 경우라면 저는 Playwright 같은 도구를 사용해 E2E 테스트 를 작성하고, 실제 브라우저에서 통합 테스트만으로는 신뢰성 있고 철저하게 검증할 수 없는 특정 페이지와 UI 상태를 테스트할 것입니다. 다행히 여기서 취한 접근법에서는 이런 경우가 오히려 드뭅니다. UI의 대다수는 서버에서 렌더링된 HTML 페이지와 조각으로 이루어져 있기 때문입니다.
로컬 개발용으로는, 서버를 단순히 시작하면 됩니다.
./mvnw spring-boot:run
우리의 경우에는 Spring Boot Developer Tools 를 다음과 같이 설정해 사용합니다.
spring:
devtools:
restart:
enabled: true
poll-interval: 500ms
quiet-period: 250ms
요약하면, 서버 코드가 수정될 때마다 거의 즉시 다시 컴파일되며, 그 덕분에 hot/live reloading 을 얻게 됩니다. 우리가 해야 할 일은 브라우저에서 페이지를 새로고침하고 방금 변경한 내용을 확인하는 것뿐입니다.
데이터베이스를 사용한다면, 추가로 그것을 Docker/Podman 컨테이너로 실행합니다.
여기에는 TailwindCSS도 적용되므로, 로컬 개발을 위해 다음 스크립트도 실행 중이어야 합니다.
npm ci
cd ops
./live-css-gen.sh
≈ tailwindcss v4.2.2
Done in 93ms
Done in 169µs
Done in 4ms
이렇게 하면 UI 관련 파일을 수정할 때 CSS가 지속적으로 다시 생성됩니다.
프로덕션에서는, 이상적으로 다음이 필요합니다.
이를 지원하기 위해 저는 두 개의 스크립트를 준비했습니다. package_components.py 는 모든 JS 컴포넌트를 가져와 하나의 components_{hash}.js 파일로 만듭니다. build_and_package.bash 는 @tailwindcss/cli 도구의 도움을 받아 CSS를 생성하고, 컴포넌트를 패키징하고 해시를 붙이기 위해 package_components.py 를 호출하며, Docker에서 서버를 빌드하고, 실행에 필요한 모든 프런트엔드 자산과 백엔드/서버 코드를 포함한 배포 준비 완료 상태의 자급식 Docker 이미지를 생성합니다. 그런 다음 우리는 dist/load_and_run_app.bash 및 dist/run_app.bash 스크립트와 modern-frontend-complexity-alternative.tar.gz 로 압축된 Docker 이미지를 프로덕션 환경에 복사하고 다음을 실행하기만 하면 됩니다.
bash load_and_run_app.bash
Loading modern-frontend-complexity-alternative:latest image, this can take a while...
Loaded image: modern-frontend-complexity-alternative:latest
Image loaded, running it...
Stopping previous modern-frontend-complexity-alternative version...
modern-frontend-complexity-alternative
Removing previous container....
modern-frontend-complexity-alternative
Starting new modern-frontend-complexity-alternative version...
e12ff5c2d81e933560e2a8a974b79654cfe219c43b5a47995c576ab1a562ccf8
docker logs modern-frontend-complexity-alternative
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.0.3)
2026-04-11T05:55:52.297Z INFO 1 --- [ main] c.ModernFrontendComplexityAlternativeApp : Starting ModernFrontendComplexityAlternativeApp v0.0.1-SNAPSHOT using Java 25.0.2 with PID 1 (/modern-frontend-complexity-alternative.jar started by root in /)
2026-04-11T05:55:52.301Z INFO 1 --- [ main] c.ModernFrontendComplexityAlternativeApp : No active profile set, falling back to 1 default profile: "default"
2026-04-11T05:55:53.003Z INFO 1 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2026-04-11T05:55:53.012Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-04-11T05:55:53.012Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.18]
2026-04-11T05:55:53.033Z INFO 1 --- [ main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 683 ms
2026-04-11T05:55:53.298Z INFO 1 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-04-11T05:55:53.305Z INFO 1 --- [ main] c.ModernFrontendComplexityAlternativeApp : Started ModernFrontendComplexityAlternativeApp in 1.372 seconds (process running for 1.736)
단점은 무엇일까요?
우리가 전통적인 SPA 사고방식 을 가지고 있다면, 이것은 꽤 큰 사고 전환입니다. 응답을 받은 뒤 클라이언트 측에서 다시 변환해야 하는 REST API 엔드포인트를 설계하고 소비하는 대신, 대부분의 데이터는 서버에서 렌더링되고 브라우저는 즉시 표시 가능한 형식으로 이를 받습니다. 또한 우리는 대체로 프레임워크 고유 방식에 의존하는 대신 브라우저의 네이티브 API를 사용합니다. 이것은 큰 장점인데, 네이티브 API는 현재 버전의 React, Vue, Angular 또는 Svelte보다 훨씬 더 긴 수명을 가지기 때문입니다.
트랜스파일 및 polyfillation 단계가 없습니다. 매우 제한된 JavaScript만 작성합니다. Web Components와 몇몇 이벤트 리스너를 위해서만 작성하며, UI를 좀 더 상호작용적으로 만드는 용도입니다. 그래서 큰 문제는 아닙니다. 하지만 이것은 사실입니다. 도구 체인을 크게 단순화해주는 이 단계가 없기 때문에, 우리가 사용하는 JS 기능들이 모든 대상 환경에서 실행될 수 있도록 더 의식적으로 선택해야 합니다.
일부 도구는 직접 만들어야 합니다. 이 아키텍처에서는 소스 코드가 나중에 브라우저 런타임에서 실행되는 것과 대부분 닮아 있기 때문에, 필요한 도구는 최소한입니다. 사실 Web Components가 몇 개뿐이라면 굳이 번들링할 필요도 없습니다. 정적 파일에 대한 HTTP 요청 몇 번은 문제가 아니고, 성능도 꽤 좋을 것입니다. 하지만 수가 늘어난다면 하나의 파일로 번들링하는 편이 낫습니다. 모든 정적 자산에 해시 접미사를 붙여 더 효율적으로 캐시하게 하는 것도 마찬가지입니다. 반면 이런 저수준 전략은 장점으로 볼 수도 있습니다. 우리는 브라우저가 파일을 어떻게 처리하는지 더 잘 인식하게 되고, 그것을 완전히 통제할 수 있습니다. 또한 이 접근법은 브라우저가 HTML 문서를 어떻게 동작시키고, 처리하고, 조작하는지에 더 잘 부합하기 때문에, 전체적으로 필요한 변환 수가 훨씬 적습니다.
가능한 개선점 으로는, 브라우저 탭이 자동으로 새로고침되어 우리가 수동으로 새로고침할 필요가 없는 hot/live reloading 이 있으면 더 좋을 것입니다. 아직 그런 도구는 없지만, 분명 구현 가능해 보입니다. 재사용 가능한 HTML Web Components와 서버 사이드 템플릿 라이브러리도 큰 도움이 될 것입니다. 현재로서는(제가 알기로) 여기서 제시한 방식으로 만들어진 즉시 사용 가능한 컴포넌트가 없기 때문에, 지금은 우리가 직접 만들어야 합니다. 간단히 말해, 생태계가 아직 거기까지 오지는 않았습니다.
우리가 배운 것처럼, 단순하지만 전 세계적인 정적 문서 공유(웹)로 시작했던 것은 결국 매우 복잡한 런타임(브라우저)으로 발전했고, 이제는 거의 모든 애플리케이션을 만들 수 있을 정도가 되었으며 네이티브 환경의 가능성과도 경쟁하고 있습니다.
그 과정에서 우리는 점점 더 상호작용적인 웹사이트와 애플리케이션을 구축하기 위한 몇 가지 서로 다른 단계와 접근법을 거쳤습니다. 처음에는 여기저기에 약간의 JavaScript를 뿌려 더 상호작용적으로 만든 Multi Page Applications (MPAs) 가 있었습니다. 그다음 사람들은 전체 페이지 새로고침이 없고, 거의 모든 데이터 변환과 UI 상태 전환이 전적으로 클라이언트 측에서 실행되는 두껍고 복잡한 JavaScript 레이어에서 처리되는 Single Page Applications (SPAs) 를 실험하기 시작했습니다.
현재 우리는 JavaScript 중심의 현실 속에 살고 있으며, 브라우저 런타임은 우리가 작업하는 소스 코드 파일과 완전히 다르게 보입니다. 이것은 이러한 애플리케이션을 개발하고 빌드하는 데 필요한 도구의 복잡성을 엄청나게 증가시켰습니다. 이런 접근법으로 만들어진 앱이 브라우저에서 동작하려면 많은 변환이 이루어져야 합니다. 런타임이 이해하는 것과 우리가 작업하는 것, 즉 소스 코드 파일은 종종 완전히 다릅니다. 게다가 이러한 도구를 능숙하게 다루기 위해 이해하고 숙달해야 할 새로운 개념도 많습니다. 여기서 복잡성 은 임계점을 넘어섰습니다. 비록 점점 더 정교해지는 도구 체인 속에 더 잘 숨겨지고 있기는 하지만요.
더 단순한 방법이 있습니다.
우리는 HTMX, HTML Web Components 그리고 템플릿 언어 를 활용해, 사용자 경험이나 복잡한 기능, 개발자 경험을 희생하지 않으면서도 브라우저의 동작 방식에 훨씬 더 잘 부합하는 방식으로 웹사이트와 앱을 만들 수 있습니다.
그러니 저는 여러분을 이 더 단순한 대안을 실험해보자고 초대합니다: Tower of Babel Complexity 를 무너뜨리고, 웹 개발을 다시 단순하고 생산적으로 만들어봅시다!
관련 역사적 맥락: From Multi to Single Page Applications
논의된 소스 코드가 있는 저장소: https://github.com/BinaryIgor/code-examples/tree/master/modern-frontend-complexity-alternative
주의 깊은 독자라면 제 예시에는 이미지가 없었다는 점을 알아챌 수도 있으므로, 실제로는 프로덕션 스크립트가 더 복잡해질 수 있습니다. 다만, 실제로 그렇게 어렵지는 않습니다. 우리는 다음처럼 단순하게 할 수 있습니다.
1. 전용 이미지 디렉터리를 둡니다
2. 각 이미지에 대해 랜덤 해시를 생성해, JavaScript 파일에서 하는 것과 같은 방식으로 가능한 한 오래 캐시되게 합니다 - 예를 들어 image.jpg 가 image_a46v98bc.jpg 가 됩니다
3. UI 소스 코드 파일 안의 images/image.jpg 의 모든 등장 부분을 images/image_a46v98bc.jpg 로 교체합니다
현대 프런트엔드 스택의 복잡성: 1. https://eduardo-ottaviani.medium.com/the-unnecessary-complexity-on-the-front-end-1632e101dc84 2. https://blog.logrocket.com/the-increasing-nature-of-frontend-complexity-b73c784c09ae/ 3. https://news.ycombinator.com/item?id=34218003
복잡성: 우발적인가, 본질적인가? https://www.iankduncan.com/engineering/2025-05-26-when-is-complexity-accidental/
HTML Web Components: 1. https://blog.jim-nielsen.com/2023/html-web-components/ 2. https://blog.jim-nielsen.com/2023/html-web-components-an-example/ 3. https://www.oddbird.net/2023/11/17/components/ 4. https://www.youtube.com/watch?v=bIInG91BuhE 5. https://htmlwithsuperpowers.netlify.app/
현대 CSS가 얼마나 강력한가: https://modern-css.com
Polyfills: https://javascript.info/polyfills, Babel: https://babeljs.io 그리고 PostCSS: https://postcss.org
네이티브 웹 기술을 선호하라: https://blog.jim-nielsen.com/2020/cheating-entropy-with-native-web-tech/
가치 있는 피드백, 질문, 코멘트가 있거나 그냥 연락하고 싶다면 [email protected] 로 이메일을 보내주세요.
그곳에서 뵙겠습니다!
생각해볼 또 다른 것:
©Igor Roztropiński