Skip to content

프론트엔드 프레임워크의 본질

💡 학습 가이드: 이 글은 근본적인 질문 하나에 답합니다 — 프론트엔드 프레임워크(Vue, React, Svelte 등)는 도대체 무엇을 하는가? HTML, CSS와 약간의 JavaScript만 배웠더라도 전혀 문제없습니다. 처음부터 차근차근 설명합니다.

시작하기 전에, 다음 두 가지 기본 개념을 알고 있는지 확인하세요. 확실하지 않다면 해당 장을 먼저 읽어보세요:

  • HTML: 웹페이지의 뼈대, 페이지에 어떤 요소(제목, 단락, 버튼, 이미지 등)가 있는지 정의합니다. HTML과 CSS 레이아웃을 참고하세요.
  • JavaScript: 웹페이지를 "움직이게" 하는 프로그래밍 언어로, 페이지 내용을 수정하고 사용자 동작에 반응할 수 있습니다. JavaScript 심층 가이드를 참고하세요.

또 하나의 개념이 앞으로 자주 등장할 텐데, 여기서 먼저 완전히 설명하겠습니다.

DOM이란 무엇인가?

DOM의 전체 이름은 Document Object Model이며, 한국어로는 "문서 객체 모델"이라고 합니다.

브라우저에서 웹페이지를 열면, 브라우저가 가장 먼저 하는 일은 HTML 코드를 읽는 것입니다. 읽은 후 브라우저는 HTML 텍스트를 그대로 페이지에 표시하지 않고, 먼저 HTML 코드를 트리 구조로 변환하여 메모리에 저장합니다. 이 트리를 DOM 트리라고 부릅니다.

트리의 각 노드(Node)는 HTML의 태그 하나에 대응합니다. 태그 간의 중첩 관계는 DOM 트리에서 부모 노드와 자식 노드의 관계가 됩니다.

👇 직접 해보세요: 왼쪽의 HTML 코드 위에 마우스를 올리면, 오른쪽 DOM 트리에서 해당 노드가 하이라이트됩니다. 반대로도 마찬가지입니다. HTML 태그의 각 줄은 DOM 트리의 노드 하나에 대응합니다.

HTML → DOM treeHow the browser understands your HTML
HTML code you write
1<html>
2<body>
3<h1>My cart</h1>
4<p>3 products total</p>
5<ul>
6<li>Headphones</li>
7<li>Keyboard</li>
8<li>Mouse</li>
9</ul>
10<button>Checkout</button>
11</body>
12</html>
Browser parses
DOM tree generated by the browser
html
└─body
└─h1"My cart"
└─p"3 products total"
└─ul
└─li"Headphones"
└─li"Keyboard"
└─li"Mouse"
└─button"Checkout"
📄
NodeEvery box in the DOM tree is a node. Each HTML tag, such as <h1> or <p>, maps to one node.
🌳
Parent-child relationshipWhen one tag is nested inside another, the DOM tree represents that as a parent-child relationship.
✏️
DOM operationJavaScript can add, remove, or change nodes in the DOM tree. After a node changes, the browser recalculates layout and paints the page again.
Key concept:The DOM is a tree maintained by the browser in memory. It corresponds to the HTML you wrote. JavaScript changes this DOM tree, and the browser updates the screen from that changed tree.

DOM을 이해해야 하는 이유는 무엇일까요? JavaScript가 페이지를 수정하는 방식이 바로 이 DOM 트리를 조작하는 것이기 때문입니다 — 노드 추가, 노드 삭제, 노드 내용 수정. 그리고 프론트엔드 프레임워크가 하는 핵심 작업이 바로 이러한 DOM 조작을 자동화하는 것입니다. 앞으로 DOM을 자주 언급할 텐데, DOM을 이해하는 것이 프레임워크 원리를 이해하는 기초입니다.


0. 서론: "프론트엔드 프레임워크"란 무엇인가?

먼저 "프레임워크"라는 단어를 설명하겠습니다. 프로그래밍에서 프레임워크(Framework) 는 이미 작성된 코드와 규칙의 모음으로, 코드를 어떻게 구성하고 어떻게 실행할지 규정합니다. 개발자는 프레임워크의 방식에 따라 코드를 작성하고, 프레임워크는 반복적이고 번거로운 하위 작업을 대신 처리해 줍니다.

프론트엔드 프레임워크는 바로 웹페이지 인터페이스를 구축하는 데 특화된 프레임워크입니다. 현재 가장 널리 사용되는 것으로 Vue, React, Svelte, Angular가 있습니다.

그렇다면 이 프레임워크들은 도대체 어떤 문제를 해결해 주는 걸까요? 아래 세 장의 카드가 핵심 로직을 요약합니다:

Problem
  • When data changes, manual DOM updates are easy to miss.
  • The more complex a page becomes, the more places need synchronization.
  • In team work, scattered DOM operations become hard to maintain.
Root cause
  • The browser does not know the relationship between data and UI.
  • Native DOM APIs are low-level operations, not automatic UI synchronization.
  • Developers are forced to act as manual synchronizers.
Framework solution
  • Build a mapping from data to UI: UI = f(State).
  • Detect data changes automatically with reactivity.
  • Compute minimal DOM updates with virtual DOM or compiler optimization.

이제 한 걸음씩 펼쳐서, 가장 기본적인 문제부터 설명하겠습니다.


1. 핵심 문제: 데이터가 바뀌면, 인터페이스는 어떻게?

1.1 먼저 "데이터"와 "인터페이스"가 무엇인지 이해하기

모든 웹 애플리케이션에는 두 가지가 동시에 존재합니다:

  • 데이터(Data / State): 프로그램 내부에 저장된 정보입니다. 예를 들어 "장바구니에 3개의 상품이 있음", "사용자 이름은 홍길동", "현재 두 번째 탭이 선택됨" 등입니다. 이 데이터는 JavaScript 변수에 저장되어 있으며, 사용자는 이를 볼 수 없습니다.
  • 인터페이스(UI): 사용자가 화면에서 보는 것입니다. 예를 들어 페이지에 "장바구니(3)"이 표시되고, "환영합니다, 홍길동"이 표시되며, 두 번째 탭이 하이라이트되는 것 등입니다. 이는 HTML 요소가 렌더링한 시각적 결과입니다.

데이터와 인터페이스 사이에는 대응 관계가 있습니다: 데이터가 "3개의 상품"이면, 인터페이스에도 "3"이 표시되어야 합니다. 데이터가 "4개의 상품"으로 바뀌면, 인터페이스도 "4"로 따라 바뀌어야 합니다.

문제는 바로 이것입니다: 이 "따라 바뀌는" 과정을 누가 책임지는가?

👇 직접 클릭해보세요: "상품 추가" 버튼을 클릭하고 주목해서 보세요: 데이터(왼쪽)는 이미 바뀌었지만, 인터페이스(오른쪽)는 업데이트되지 않았습니다 — 둘 사이가 "단절"된 것입니다. 그런 다음 "인터페이스 동기화"를 클릭해서 수동으로 수정해 보세요.

Data (JavaScript variable)
Product count0
Total price¥0
StatusNormal
✅ Synced
UI (what users see)
Cart0 item(s)
Total price¥0
StatusNormal
Core problem:Without a framework, changing data does not automatically update the UI. You must write code to update the interface yourself. If you forget, users see stale or wrong information.

1.2 JavaScript 변수가 바뀌었는데, 왜 인터페이스는 자동으로 바뀌지 않을까?

이것은 기초부터 시작하는 분들이 가장 헷갈려하는 부분입니다. 하위 원리를 단계별로 설명하겠습니다.

JavaScript에서 변수는 데이터를 저장하는 메모리 공간입니다. count = count + 1을 실행할 때, JavaScript 엔진이 하는 일은 매우 간단합니다: 메모리에서 count 위치의 값을 3에서 4로 바꾸는 것입니다. 이 단계를 마치면 끝입니다. 더 이상 아무 일도 일어나지 않습니다.

그리고 페이지에 표시되는 내용(예: <span>3</span> 이라는 DOM 노드)은 완전히 다른 메모리 공간에 저장되어 있습니다. JavaScript 엔진이 변수를 수정할 때, 페이지에 이 변수의 값을 표시하고 있는 DOM 노드가 있다는 것을 전혀 알지 못하며, 이를 확인할 수 있는 메커니즘도 없습니다.

따라서 본질적인 이유는: JavaScript 변수와 DOM 노드는 서로 독립된 두 개의 메모리이며, 둘 사이에는 어떤 자동 연동 메커니즘도 없습니다. 변수를 수정하면 변수가 위치한 메모리만 바뀔 뿐, DOM 노드가 위치한 메모리는 아무런 영향을 받지 않습니다.

javascript
let count = 3

// 페이지에 count 값을 표시하는 DOM 노드가 있습니다:
// <span id="counter">3</span>

count = 4
// JavaScript 엔진이 한 일은?
//   → 변수 count의 메모리 값을 3에서 4로 변경
//   → 끝. 그게 다입니다.
// 페이지의 <span>에는 여전히 "3"이 표시됩니다

페이지 표시도 "4"로 바꾸고 싶다면, 반드시 추가 코드를 작성해서, 수동으로 해당 DOM 노드를 찾아 내용을 수정해야 합니다:

javascript
count = 4  // 1단계: 변수 변경

// 2단계: 직접 작성해야 함 — DOM 노드를 찾아 텍스트를 새 값으로 변경
document.getElementById('counter').textContent = count

페이지에 count 값을 표시하는 곳이 5곳(장바구니 수량, 상품 목록, 총 가격, 소계, 상태 알림)이라면, 이런 코드를 5개 작성해야 합니다. 하나라도 빠뜨리면, 그 위치에는 여전히 이전 값이 표시되고, 사용자는 잘못된 정보를 보게 됩니다.

1.3 프레임워크는 무엇을 하는가? 두 단계로 자동 연결 구축

프레임워크가 자동으로 동기화할 수 있는 것은 두 단계의 협력 덕분입니다 — 어느 하나도 빠질 수 없습니다.

첫 번째 단계: 템플릿에 "어디에 이 변수를 표시할지" 등록하기

프레임워크의 HTML 템플릿에서, 와 같은 문법으로 "여기에 count 값을 표시한다"고 표시합니다:

html
<!-- Vue 템플릿 -->
<span>장바구니: {{ count }} 개</span>    <!-- 위치 A: count를 표시 -->
<span>총 가격: ¥{{ count * 99 }}</span>   <!-- 위치 B: count를 사용 -->
<span>{{ count > 5 ? '너무 많음' : '정상' }}</span>  <!-- 위치 C: count를 사용 -->

프레임워크가 처음 페이지를 렌더링할 때, 이 "등록 관계"를 기록합니다: 위치 A, B, C가 모두 count에 의존한다.

두 번째 단계: 프레임워크가 변수를 감시하고, 변경되면 등록 테이블을 조회하여 자동 업데이트

프레임워크는 JavaScript 내장 Proxy(프록시)를 사용하여 변수를 "감싸서", "감시 대상 변수"로 만듭니다. 이 변수를 수정하면, Proxy가 할당과 동시에 몰래 한 가지 일을 더 합니다: 프레임워크에 "count가 변경되었습니다"라고 알립니다. 프레임워크는 알림을 받은 후, 첫 번째 단계의 등록 테이블을 조회하여 A, B, C 세 위치를 모두 업데이트합니다.

순수 JS:
  HTML 작성 → <span id="counter">3</span> (변수와 아무 연결 없음)
  변수 변경 → count = 4 → 끝, 인터페이스는 아무 반응 없음
  수동 보완 → document.getElementById('counter').textContent = 4 → 그제야 인터페이스 업데이트

Vue 프레임워크:
  템플릿 작성 → <span>{{ count }}</span> (프레임워크가 기억: 여기는 count에 의존)
  변수 변경 → count = 4 → Proxy 가로챔 → 프레임워크에 알림 → 프레임워크가 등록 테이블 조회 → A/B/C 자동 업데이트

이것이 바로 "프레임워크만 자동 동기화가 가능한" 이유입니다 — 순수 HTML의 <span>과 JS 변수 사이에는 근본적으로 아무 연결도 없으며, 프레임워크의 템플릿 문법()이 이 연결을 구축하는 핵심입니다. 라고 작성해야, 프레임워크가 여기에 count를 표시해야 한다는 것을 알고, count가 변경될 때 정확히 여기를 찾아 업데이트할 수 있습니다.

👇 직접 클릭해보세요: 먼저 "순수 JavaScript"를 선택하고, "실행"을 클릭한 후 주목해서 보세요 — 변수는 변경되었지만 인터페이스는 전혀 움직이지 않으며, 각 위치를 하나씩 수동으로 동기화해야 합니다. 그런 다음 "프레임워크 사용"으로 전환하고, 마찬가지로 "실행"을 클릭하세요 — 변수가 변경되면 프레임워크가 자동으로 모든 단계를 완료하고, 인터페이스가 즉시 따라갑니다.

What happens when a variable changes?Native JavaScript vs framework
Code you write
// Run when the button is clicked
count = count + 1
// You still have to write these lines:
document.getElementById('count')
.textContent = count
document.getElementById('total')
.textContent = count * 99
Execution flow
1
JavaScript changes the variable
count changes from 0 to 1
2
Find DOM nodes
Call document.getElementById() manually
3
Change DOM content
Set .textContent manually
UI result
Cart0 item(s)
Total price¥0
Why is it not automatic?JavaScript variables are not aware of the UI. When you run count = 4, the engine only changes the value in memory. It does not notify anyone, trigger a callback, or check where count is displayed on the page. The UI will not change unless you update the DOM yourself.

1.4 비교: 수동 동기화 vs 자동 동기화의 실제 효과

원리를 이해한 후, 조금 더 복잡한 시나리오에서 수동 동기화와 자동 동기화의 차이가 얼마나 큰지 살펴보겠습니다.

👇 직접 클릭해보세요: 왼쪽은 프레임워크가 없는 "수동 동기화" 방식입니다 — 각 표시 영역마다 "동기화" 버튼을 따로 클릭해서 업데이트해야 합니다. 오른쪽은 프레임워크가 있는 "자동 동기화" 방식입니다 — "상품 추가" 버튼만 클릭하면 모든 표시 영역이 자동으로 업데이트됩니다. 왼쪽에서 일부러 특정 영역을 동기화하지 않으면 어떻게 되는지 시험해 보세요.

Manual sync / jQuery style
🔴Cart countSynced
0 item(s)
📋Product listSynced
(empty)
💰TotalSynced
¥0
⚠️StatusSynced
Normal
Misses:0
VS
Automatic sync / framework style
🔴Cart countSynced
0 item(s)
📋Product listSynced
(empty)
💰TotalSynced
¥0
⚠️StatusSynced
Normal
Misses:0
Core idea:The essential value of frontend frameworks is automatic synchronization: you change data, and the framework updates every UI location that depends on it.

이것이 프론트엔드 프레임워크가 존재하는 근본적인 이유입니다: JavaScript 변수에 "수정될 때 자동으로 인터페이스 업데이트를 알리는" 능력을 부여하여, 수동 동기화로 인한 오류를 없애는 것입니다.


2. 프레임워크의 핵심 사상: 데이터로 인터페이스 설명하기

2.1 두 가지 작성 방식의 차이

"자동 동기화"의 가치를 이해했으니, 이제 프레임워크가 구체적으로 어떻게 구현하는지 살펴보겠습니다.

프레임워크가 없던 시대(jQuery를 사용하던 시절)에는, 코드가 이렇게 작성되었습니다 — 브라우저에 무엇을 해야 할지 한 걸음씩 명령합니다:

javascript
// 1단계: 페이지에서 id가 counter인 요소 찾기
var element = document.getElementById('counter')
// 2단계: 이 요소의 텍스트 내용을 새 값으로 변경
element.textContent = '4'
// 3단계: 다른 요소를 찾아서, 마찬가지로 변경
document.getElementById('total').textContent = '¥396'
// 4단계: 수량이 5보다 크면, 상태 알림도 변경……

이러한 작성 방식을 명령형(Imperative) 이라고 합니다 — 브라우저에 단계별로 작업을 "명령"하는 것입니다.

프레임워크가 등장한 후, 코드는 이렇게 바뀌었습니다 — "인터페이스가 어떻게 생겨야 하는지"만 설명합니다:

html
<!-- 이 값이 어떻게 페이지에 업데이트되는지는 신경 쓰지 않음 -->
<!-- 오직 말한다: 여기에 count 값을 표시해야 한다 -->
<span>{{ count }}</span>
<span>총 가격: ¥{{ count * 99 }}</span>
<span v-if="count > 5">상품이 너무 많습니다!</span>

이러한 작성 방식을 선언형(Declarative) 이라고 합니다 — 인터페이스의 최종 상태를 "선언"하며, 그 상태에 도달하는 방법은 프레임워크가 스스로 처리합니다.

2.2 핵심 공식: UI = f(State)

모든 현대 프론트엔드 프레임워크 — Vue, React, Svelte를 막론하고 — 는 동일한 핵심 사상을 따르며, 이는 하나의 공식으로 표현할 수 있습니다:

UI = f(State)

이 공식의 의미는 다음과 같습니다:

  • State(상태): 애플리케이션 데이터입니다. JavaScript의 변수들: 장바구니에 몇 개의 상품이 있는지, 사용자가 로그인했는지, 현재 페이지는 무엇인지……
  • f(함수): 프레임워크의 렌더링 메커니즘입니다. 데이터를 어떻게 인터페이스로 바꾸는지 알고 있습니다.
  • UI(인터페이스): 사용자가 화면에서 보는 최종 결과입니다.

의미: 주어진 데이터(State)가 프레임워크의 처리(f)를 거치면, 결정론적으로 해당하는 인터페이스(UI)를 얻을 수 있습니다. 데이터가 바뀌면 인터페이스도 따라 바뀝니다. 개발자는 데이터만 신경 쓰면 되고, 인터페이스가 어떻게 업데이트되는지는 신경 쓸 필요가 없습니다.

👇 직접 클릭해보세요: 왼쪽에서 데이터(State)를 수정하고, 오른쪽의 인터페이스(UI)가 어떻게 자동으로 따라 바뀌는지 관찰하세요. 이것이 UI = f(State)의 직관적인 표현입니다.

State (data)
→ f →
UI
Change State
2
Rendered result (UI)
Hello, guest!
Cart: 2 item(s)
Total: ¥198
Current theme: Light
Current State snapshot
{ "username": "(empty)", "count": 2, "darkMode": false }
Core idea:You only change State. The framework renders the corresponding UI automatically. The same data always renders the same UI: UI = f(State).

2.3 왜 선언형이 명령형보다 나은가?

선언형 작성 방식의 장점은 다음과 같습니다:

비교 차원명령형(프레임워크 없음)선언형(프레임워크 있음)
코드 양매 업데이트마다 구체적인 조작 코드를 작성템플릿을 한 번만 작성하면 프레임워크가 자동 처리
오류 확률특정 위치 업데이트를 빠뜨리기 쉬움프레임워크가 모든 위치가 업데이트되도록 보장
가독성코드에 대량의 DOM 조작이 섞여 있음코드가 인터페이스 구조를 명확하게 설명
유지보수 비용기능 하나를 수정하려면 많은 곳을 변경해야 함데이터 로직만 수정하면 인터페이스는 자동으로 따라감

간단히 말해: 선언형은 "비즈니스 로직"(데이터가 어떻게 변하는지)에 집중할 수 있게 해주며, "인터페이스가 어떻게 업데이트되는지"라는 반복적이고 오류가 발생하기 쉬운 일을 신경 쓰지 않아도 됩니다.


3. 반응형 시스템: 프레임워크는 데이터가 변경된 것을 어떻게 알까?

3.1 "반응형"이란 무엇인가?

앞서 "데이터가 바뀌면 인터페이스가 자동으로 업데이트된다"고 말했습니다. 그런데 여기에는 기술적인 문제가 있습니다: JavaScript 자체에는 "변수가 수정될 때 자동으로 다른 사람에게 알리는" 능력이 없습니다.

count = 4라고 쓰면, JavaScript는 count의 값을 3에서 4로 바꿀 뿐, 누구에게도 자동으로 알리지 않습니다. 프레임워크는 데이터가 수정되었음을 "발견"할 수 있는 메커니즘이 필요합니다.

반응형(Reactivity) 은 바로 이 메커니즘의 총칭입니다: 데이터가 변경될 때, 시스템이 자동으로 그 변화를 감지하고, 해당하는 업데이트 작업을 실행하는 것입니다.

3.2 세 가지 다른 구현 방식

프레임워크마다 반응형을 구현하기 위해 다른 기술 방안을 채택했습니다. 이것이 바로 Vue, React, Svelte 사이의 가장 근본적인 차이입니다.

방식 1: 프록시 인터셉트(Vue의 방식)

Vue는 JavaScript 내장 Proxy(프록시) 메커니즘을 사용합니다. Proxy는 객체의 속성을 읽거나 수정할 때, 지정한 코드를 자동으로 실행할 수 있습니다.

Vue는 데이터 객체를 Proxy로 감쌉니다. count = 4를 실행하면, Proxy가 이 쓰기 작업을 가로채서 Vue에 알립니다: "count 값이 변경되었습니다". 그러면 Vue는 count를 사용하는 모든 인터페이스 부분을 업데이트합니다.

개발자는 추가 작업이 전혀 필요 없습니다 — 그냥 할당만 하면 Vue가 자동으로 감지합니다.

방식 2: 명시적 호출(React의 방식)

React는 Proxy를 사용하지 않습니다. 대신 전용 함수를 통해 데이터를 수정해야 합니다:

javascript
// React의 작성 방식
const [count, setCount] = useState(0)

// 직접 count = 4라고 쓸 수 없음 (React가 감지하지 못함)
// 반드시 setCount를 호출해야 함:
setCount(4)

setCount()를 호출할 때만 React는 데이터가 변경되었음을 알고 인터페이스를 업데이트합니다. 직접 count = 4라고 쓰면, React는 전혀 알지 못하고 인터페이스도 업데이트되지 않습니다.

이 방식은 더 "명시적"입니다 — 매 데이터 변경이 개발자가 능동적으로 프레임워크에 알리는 것이므로, 의도치 않은 업데이트가 발생하지 않습니다.

방식 3: 컴파일러 분석(Svelte의 방식)

Svelte는 완전히 다른 노선을 취했습니다. Svelte에는 컴파일러(Compiler)가 있어서, 코드가 실행되기 전에 컴파일러가 먼저 소스 코드를 분석합니다.

컴파일러가 count += 1과 같은 할당문을 발견하면, 이 코드 줄 뒤에 "인터페이스 업데이트를 알리는" 코드를 자동으로 삽입합니다. 즉, 코드가 실행될 때 "알림"이라는 동작은 컴파일러에 의해 미리 준비된 것입니다.

코드는 평범한 JavaScript 할당처럼 보이지만, 컴파일된 코드에는 인터페이스 업데이트 로직이 추가되어 있습니다.

👇 직접 클릭해보세요: 다른 프레임워크 탭을 선택하고, "데이터 수정"을 클릭하여 각 프레임워크가 "엔진 후드 아래"에서 데이터 변경 감지와 인터페이스 업데이트를 완료하기 위해 어떤 단계를 거치는지 관찰하세요.

count:0
Under the hood
1count = 1 triggers the Proxy set trap
2Notify the dependency tracker: count changed
3Find all components that depend on count
4Update the DOM automatically
Core idea: Vue uses Proxy to intercept reads and writes automatically, so the code feels natural.

3.3 세 가지 방식의 비교

비교 차원Vue(Proxy 프록시)React(명시적 호출)Svelte(컴파일러)
개발자 작성 방식직접 할당 count = 4반드시 setCount(4) 사용직접 할당 count = 4
변화 감지 시점런타임에 자동 인터셉트개발자가 능동적 통지컴파일 시점에 미리 알림 코드 삽입
런타임 성능 오버헤드Proxy 인터셉트에 약간의 오버헤드setState 스케줄링에 약간의 오버헤드추가 오버헤드 거의 없음
디버깅 난이도중간데이터 흐름이 명확하여 비교적 쉬움컴파일된 코드를 이해해야 함
적합한 시나리오개발 효율과 자연스러운 작성법을 추구예측 가능한 데이터 흐름을 추구극한의 실행 성능을 추구

세 가지 방식에 절대적인 좋고 나쁨은 없습니다. Vue는 작성이 가장 자연스럽고, React는 데이터 흐름이 가장 통제 가능하며, Svelte는 실행 성능이 가장 좋습니다. 어느 것을 선택할지는 프로젝트의 구체적인 요구에 달려 있습니다.


4. 컴포넌트: 인터페이스를 재사용 가능한 작은 조각으로 분할

4.1 왜 분할하는가?

하나의 완전한 웹페이지에는 내비게이션 바, 사이드바, 콘텐츠 영역, 검색창, 사용자 아바타, 다양한 버튼…… 이 모든 코드를 하나의 파일에 작성하면, 그 파일은 매우 길어지고 유지보수가 매우 어려워집니다.

컴포넌트(Component) 는 인터페이스를 하나하나의 독립적인 작은 조각으로 분할하여, 각 조각이 자신의 데이터, 자신의 인터페이스, 자신의 로직을 관리하게 하는 것입니다.

예를 들어 전자상거래 페이지는 다음과 같은 컴포넌트로 분할할 수 있습니다:

  • NavBar 컴포넌트: 상단 내비게이션 바 담당
  • SearchBox 컴포넌트: 검색창 담당
  • ProductCard 컴포넌트: 상품 카드 하나 담당
  • ShoppingCart 컴포넌트: 장바구니 담당

각 컴포넌트는 독립적입니다. ProductCardNavBar에 어떤 코드가 작성되어 있는지 알 필요가 없으며, 오직 자신만 잘 관리하면 됩니다.

4.2 컴포넌트의 세 가지 장점

장점 1: 재사용. ProductCard 컴포넌트 하나를 잘 작성하면, 페이지에서 100번 사용할 수 있습니다 — 매번 다른 상품 데이터를 전달하면, 다른 상품 카드가 렌더링됩니다. HTML 코드를 100번 복사하여 붙여넣을 필요가 없습니다.

장점 2: 캡슐화. 컴포넌트 내부의 데이터와 로직은 독립적입니다. SearchBox 컴포넌트의 코드를 수정해도 ProductCard 컴포넌트에 영향을 주지 않습니다. 여러 사람이 협업할 때, 서로 다른 사람이 동시에 다른 컴포넌트를 개발할 수 있으며 서로 간섭하지 않습니다.

장점 3: 유지보수성. 어떤 기능에 문제가 생겼을 때, 해당 컴포넌트로 바로 찾아가 수정할 수 있으며, 수천 줄짜리 큰 파일에서 뒤질 필요가 없습니다.

👇 직접 클릭해보세요: 왼쪽의 컴포넌트 이름을 클릭하여, 페이지에서 해당하는 영역을 확인하세요. 주목해서 보세요: 동일한 ProductCard 컴포넌트가 여러 번 재사용되며, 매번 다른 데이터를 표시합니다.

Component breakdownHow one page is split into independent components
Component tree
📱App (root component)
🧭NavBar
🔍SearchBox
🛒CartIcon
📦ProductCard×3
📄Footer
Page preview
🏠 E-commerce site🔍 Search box🛒 Cart (3)
📦
Product 1
¥199
📦
Product 2
¥298
📦
Product 3
¥397
📦 ProductCard
A card for one product. Write the code once, pass in different product data, and reuse it many times.
Independent dataIsolated stylesReused 3 times
Core idea:Componentization means splitting a large page into independent small pieces. Each component owns its own data, UI, and styles. The same component can be reused in multiple places with different input data.

4.3 컴포넌트는 코드에서 어떻게 생겼을까?

Vue를 예로 들면, 컴포넌트 하나는 .vue 파일이며, 세 부분으로 구성됩니다:

html
<!-- ProductCard.vue -->
<template>
  <!-- 여기에 HTML 구조 작성 —— 컴포넌트의 "외관" -->
  <div class="card">
    <h3>{{ name }}</h3>
    <p>가격: ¥{{ price }}</p>
    <button @click="addToCart">장바구니 담기</button>
  </div>
</template>

<script setup>
// 여기에 JavaScript 로직 작성 —— 컴포넌트의 "동작"
const props = defineProps(['name', 'price'])

function addToCart() {
  // "장바구니 담기" 로직 처리
}
</script>

<style scoped>
/* 여기에 CSS 스타일 작성 —— 컴포넌트의 "스타일" */
.card {
  border: 1px solid #ccc;
  padding: 16px;
}
</style>

이 컴포넌트를 사용할 때는, 마치 커스텀 HTML 태그를 사용하는 것과 같습니다:

html
<!-- 다른 곳에서 ProductCard 컴포넌트 사용 -->
<ProductCard name="무선 이어폰" price="299" />
<ProductCard name="기계식 키보드" price="599" />
<ProductCard name="모니터" price="1999" />

세 줄의 코드로 세 개의 서로 다른 상품 카드가 렌더링됩니다.


5. DOM 조작의 비용: 왜 프레임워크는 이렇게 큰 노력을 기울일까?

5.1 DOM 조작이란 무엇인가?

앞서 DOM에 대해 언급했습니다 — 브라우저가 HTML을 파싱하여 생성한 트리 구조입니다. DOM 조작은 JavaScript로 이 트리의 노드를 수정하는 것입니다. 예를 들어 텍스트 변경, 요소 추가, 요소 삭제, 스타일 수정 등입니다.

이러한 조작 자체는 복잡하지 않지만, 브라우저는 DOM 조작을 실행한 후 화면 표시를 업데이트하기 위해 많은 추가 작업을 해야 합니다:

  1. 스타일 재계산: 이 노드와 그 자식 노드의 CSS 스타일에 변화가 필요한가?
  2. 재배치(Layout / Reflow): 페이지의 모든 요소 위치와 크기를 다시 계산해야 합니다. 한 요소의 변경이 다른 요소의 위치에 영향을 줄 수 있기 때문입니다.
  3. 재그리기(Paint): 계산된 내용을 화면에 그립니다.

이 세 단계는 각각 계산 비용이 듭니다. 코드가 빈번하게 DOM 조작을 트리거하면, 브라우저는 이 단계들을 반복해서 실행하고 페이지는 버벅거리게 됩니다.

👇 직접 클릭해보세요: 직접 DOM 조작과 배치 DOM 조작의 소요 시간을 비교하세요. 수정 횟수가 증가할수록 "개별 조작"의 소요 시간이 급격히 상승합니다.

DOM operation cost comparisonOne-by-one operations vs batch operation
Update DOM one by one
Each data change immediately touches the real DOM, so the browser repeatedly lays out and paints.
Simulated time
1Change → layout → paint
2Change → layout → paint
3Change → layout → paint
4Change → layout → paint
... repeat 16 more times ...
Batch first, update once
All changes are computed in memory first, then committed to the real DOM once.
Simulated time
1Compute 20 changes in memory
2Commit once → layout → paint
Core idea:The real cost of DOM operations is not changing a value itself, but the layout and paint work the browser must do afterward. Reducing DOM writes reduces these expensive calculations.

5.2 프레임워크는 이 문제를 어떻게 해결할까?

직접 DOM을 조작하는 것이 비용이 많이 들기 때문에, 프레임워크는 DOM 조작 횟수를 줄이는 방법을 고안했습니다. 구체적으로 두 가지 전략이 있습니다:

전략 1: 가상 DOM + 차이 비교(Vue, React의 방식)

가상 DOM(Virtual DOM)은 JavaScript 객체로, 그 구조는 실제 DOM 트리와 일대일로 대응하지만 메모리에만 존재하며 브라우저의 배치와 그리기를 트리거하지 않습니다.

데이터가 변경되면, 프레임워크의 처리 흐름은 다음과 같습니다:

  1. JavaScript 객체로 "새로운 가상 DOM 트리"를 만들어, 데이터 변경 후 인터페이스가 어떻게 생겨야 하는지 설명합니다
  2. 이 새 트리와 이전 트리를 비교하여(이 과정을 Diff, 즉 차이 비교라고 함), 어떤 노드가 변경되었는지 찾아냅니다
  3. 실제로 변경된 부분만 실제 DOM에 적용합니다(이 과정을 Patch, 즉 패치라고 함)

이렇게 하면, 데이터가 어떻게 변경되든 최종적으로 실제 DOM에 대한 조작은 항상 최소화됩니다.

👇 직접 클릭해보세요: "데이터 수정"을 클릭하여, 가상 DOM이 어떻게 새 트리와 이전 트리를 비교하고 변경된 노드를 찾아내는지 관찰하세요. 가장 오른쪽의 "실제 DOM"을 주목하세요 — 실제로 변경된 부분만 깜빡입니다.

Virtual DOM diff processThe core mechanism for minimizing DOM updates
Old VTree
div.app
h1: Todo list
ul.list
li: Learn Vue
li: Do homework
li: Play games
Diff Result
div.app
h1: Todo list
ul.list
li: Learn Vue
li: Do homework
li: Play games
Real DOM
div.app
h1: Todo list
  • Learn Vue
  • Do homework
  • Play games
7
Total virtual DOM nodes
0
Real DOM updates needed
Saved DOM operations
Core idea: The virtual DOM compares old and new trees in memory, finds the minimal difference, and updates only the necessary real DOM nodes.

전략 2: 컴파일 시점 정밀 위치 지정(Svelte의 방식)

Svelte는 가상 DOM을 사용하지 않습니다. Svelte의 컴파일러는 코드를 작성할 때 이미 분석을 마칩니다: "count가 변경되면, 3번째 줄의 <span> 요소를 업데이트해야 한다". 런타임에는 그 요소로 직접 찾아가 업데이트하며, 새 트리와 이전 트리를 비교할 필요가 전혀 없습니다.

이 방식은 Diff 단계를 건너뛰므로 이론적으로 성능이 더 좋습니다. 하지만 컴파일러의 분석 능력에 의존합니다 — 컴파일러가 업데이트가 필요한 모든 위치를 올바르게 식별할 만큼 충분히 똑똑해야 합니다.


6. 런타임 vs 컴파일 타임: 프레임워크 설계의 핵심 트레이드오프

6.1 두 단계

프론트엔드 코드는 작성부터 최종적으로 브라우저에서 실행되기까지 두 단계를 거칩니다:

  • 컴파일 타임(Compile-time / Build-time): 소스 코드가 빌드 도구(Vite, Webpack 등)에 의해 처리되어, 브라우저가 직접 실행할 수 있는 코드로 변환됩니다. 이 과정은 개발자 컴퓨터에서, 사용자가 웹페이지를 열기 전에 발생합니다.
  • 런타임(Runtime): 변환된 코드가 사용자 브라우저에서 실행됩니다. 프레임워크의 핵심 로직(가상 DOM의 Diff, 반응형 추적 등)이 이 단계에서 작동합니다.

6.2 두 단계에서의 프레임워크 작업 분배

프레임워크마다 이 두 단계에 분배하는 작업량이 다르며, 이것이 성능 특성과 번들 크기를 결정합니다:

  • React: 대부분의 작업이 런타임에 완료됩니다. 가상 DOM의 생성, Diff, Patch가 모두 브라우저에서 이루어집니다. 장점은 높은 유연성; 대가는 전체 프레임워크 런타임 코드(약 40KB)를 브라우저에 전송해야 한다는 것입니다.
  • Vue: 혼합 방식. 템플릿은 컴파일 타임에 최적화되고(컴파일러가 정적이며 변경되지 않는 노드를 표시), 최종 인터페이스 업데이트는 여전히 런타임의 가상 DOM을 통해 완료됩니다. 런타임 코드는 약 30KB입니다.
  • Svelte: 대부분의 작업이 컴파일 타임에 완료됩니다. 컴파일러가 코드를 분석하여, 정밀한 DOM 업데이트 명령을 직접 생성합니다. 런타임에는 프레임워크 코드가 거의 없습니다 — 최종 번들은 비즈니스 코드만 포함됩니다. 번들 크기가 가장 작습니다.

👇 직접 클릭해보세요: 다른 프레임워크 탭을 클릭하여, "런타임 ↔ 컴파일 타임" 스펙트럼에서의 위치와 각각의 번들 크기, 실행 성능, 개발 경험에서의 트레이드오프를 확인하세요.

Framework spectrumRuntime ↔ compile time
More runtimeMore compile time
ReactVue 3Vue VaporSvelteSolid.js
💚Vue 3
Hybrid: compiled templates + runtime virtual DOM
Runtime work
60%
Compile-time work
40%
Bundle sizeMediumDeveloper experience★★★★★
Trend: The trend is clear: frameworks keep moving work from runtime to compile time to improve both developer experience and runtime performance.

6.3 업계 트렌드

최근 몇 년간 프레임워크의 발전 방향은 매우 명확합니다: 점점 더 많은 작업을 런타임에서 컴파일 타임으로 이동시키는 것. 컴파일 타임의 계산은 사용자 기기의 리소스를 소비하지 않고 페이지 로딩 속도에 영향을 주지 않기 때문입니다.

  • Vue는 Vapor Mode(베이퍼 모드)를 개발 중이며, 가상 DOM을 건너뛰고 컴파일 타임에 DOM 조작 코드를 직접 생성할 수 있습니다
  • React는 React Compiler를 출시하여, 컴파일 타임에 컴포넌트의 재렌더링 동작을 자동으로 최적화합니다
  • Svelte 5는 Runes 시스템을 도입하여, 컴파일 타임 분석 능력을 더욱 강화했습니다

7. 정리

이 글의 핵심 포인트를 되짚어 보겠습니다:

프론트엔드 프레임워크가 해결하는 근본적인 문제: 애플리케이션의 데이터가 변경될 때, 개발자가 수동으로 DOM을 조작할 필요 없이 자동으로, 효율적으로, 신뢰성 있게 인터페이스를 업데이트하는 것입니다.

프레임워크들이 공통으로 따르는 핵심 사상: UI = f(State) — 인터페이스는 데이터의 함수이며, 개발자는 데이터의 변화에만 집중하면 되고, 프레임워크가 데이터의 변화를 인터페이스에 반영하는 것을 책임집니다.

주요 기술적 차이점:

기술 포인트의미
반응형 시스템프레임워크가 데이터 변경을 어떻게 감지하는가. Vue는 Proxy 인터셉트, React는 명시적 setState, Svelte는 컴파일러 분석을 사용.
가상 DOMVue와 React는 JavaScript 객체로 DOM 트리를 시뮬레이션하고, 새 트리와 이전 트리의 차이 비교(Diff)를 통해 최소 업데이트량을 찾아 실제 DOM 조작을 줄임.
컴포넌트화인터페이스를 독립적이고 재사용 가능한 작은 조각으로 분할하여, 각 컴포넌트가 자신의 데이터와 인터페이스를 관리.
컴파일 타임 최적화코드 빌드 단계에서 미리 분석과 최적화를 수행하여, 런타임 계산량을 줄임. Svelte가 이 방면에서 가장 앞서 있음.

한 마디로: 프론트엔드 프레임워크의 본질적인 역할은 — "데이터에서 인터페이스로"의 동기화 과정을接管하여, 개발자가 데이터 로직만 생각하면 되고 더 이상 수동으로 인터페이스를 조작할 필요가 없게 하는 것입니다.


용어 대조표

영문 용어한국어 대조설명
Framework프레임워크미리 작성된 코드와 규칙의 모음으로, 개발자에게 애플리케이션의 기본 구조와 공통 기능을 제공.
DOM문서 객체 모델브라우저가 HTML을 파싱하여 생성한 트리형 데이터 구조, JavaScript가 이를 조작하여 페이지를 수정.
Virtual DOM가상 DOMJavaScript 객체로 DOM 트리를 시뮬레이션하며, Diff 알고리즘을 통해 최소 업데이트 경로를 찾아 실제 DOM 조작 횟수를 줄임.
State상태애플리케이션의 데이터. 예: 사용자 정보, 장바구니 내용, 페이지 현재 상태 등.
Reactivity반응형데이터가 변경될 때, 시스템이 자동으로 감지하고 해당하는 인터페이스 업데이트 작업을 실행하는 것.
Proxy프록시JavaScript 내장 메커니즘, 객체에 대한 읽기와 쓰기 작업을 가로챌 수 있음. Vue 3가 이를 사용하여 반응형을 구현.
Component컴포넌트독립적이고 재사용 가능한 인터페이스 코드 조각으로, 자신의 HTML 구조, JavaScript 로직, CSS 스타일을 포함.
Declarative선언형프로그래밍 방식의 하나: "최종적으로 어떤 결과를 원하는지"를 기술하며, 어떻게 구현할지는 프레임워크가 결정.
Imperative명령형프로그래밍 방식의 하나: 프로그램에 "구체적으로 어떻게 할지"를 한 걸음씩 지시.
Diff차이 비교새 가상 DOM 트리와 이전 가상 DOM 트리를 비교하여, 어떤 노드가 변경되었는지 찾아냄.
Patch패치Diff로 찾은 변경 부분을 실제 DOM에 적용.
Compile-time컴파일 타임코드가 빌드 단계에서 처리되는 시기, 사용자가 웹페이지를 열기 전에 발생.
Runtime런타임코드가 사용자 브라우저에서 실행되는 시기.
Compiler컴파일러소스 코드를 다른 형태의 코드로 변환하는 프로그램. Svelte의 컴파일러는 .svelte 파일을 효율적인 JavaScript로 변환.