1. 반응성 시스템
React
- 불변성(Immutability) 기반
- 상태 변경시 setState, useState로 새 객체를 생성해야 리렌더링 트리거
- 변경 감지를 개발자가 명시적으로 수행
//React : 새 참조를 만들어야 감지
const [user, setUser] = useState({ name: 'Rami' });
setUser({ ...user, name: 'New' });
Vue 3
- 반응형 프록시 기반(reactive, ref)
- 객체를 직접 변이(mutate)해도 Vue가 자동 감지
- 내부적으로 의존성 추적을 런타임에 수행
//Vue : 직접 변이 가능
const user = reactive({ name : 'Rami' });
user.name = 'New'; //그냥 대입해도 리렌더링
2. 렌더링 트리거 방식
| 구분 | React | Vue 3 |
| 감지 방식 | 상태 참조 비교(Object.is) | Proxy getter/setter 인터셉트 |
| 추적 시점 | 렌더 시 전체 트리 순회 | 컴포넌트 렌더 중 자동 수집 |
| 기본 동작 | 부모 리렌더 -> 자식 전부 리렌더 | 실제로 사용된 의존성만 리렌더 |
3. Virtual DOM 처리 전략
React
- 렌더마다 전체 컴포넌트 함수 재실행
- useMemo, useCallback, React.memo로 수동 최적화 필요
- Fiber 아키텍처 : 렌더 작업을 청크로 분리, 우선순위 스케줄링
Vue 3
- 컴파일 타임 최적화 수행
- render 함수로 변환하면서 정적/동적 부분을 분석한다. 이 과정에서 3가지 핵심 최적화가 들어간다.
- hoistStatic - 정적 노드 호이스팅
- 한 번도 변하지 않는 노드는 렌더 함수 바깥으로 끌어올려서 재사용한다. 매 렌더마다 새로 생성하지 않음.
- 효과 : diff 알고리즘이 이 노드를 만나면 "참조가 동일하다"는 걸 알고 즉시 건너 띔(skip)
- patchFlag - 동적 노드에 변경 부위 마킹
- 컴파일러가 템플릿을 분석해서 이 노드의 어떤 부분이 동적인가를 미리 파악하고, 비트 플래그로 표시한다. diff 시 전체 속성을 비교하지 않고 표시된 부분만 비교.
- 핵심 효과 : 비교해야할 속성의 개수가 줄어든다. -> React의 전체 diff보다 빠름.
- cacheHandlers - 이벤트 핸들러 캐싱
- 인라인 이벤트 핸들러는 보통 매 렌더마다 새 함수가 생성되는데, Vue 컴파일러는 이를 자동으로 캐싱해서 같은 함수 참조를 재사용한다.
- hoistStatic - 정적 노드 호이스팅
- render 함수로 변환하면서 정적/동적 부분을 분석한다. 이 과정에서 3가지 핵심 최적화가 들어간다.
//예시 템플릿
<template>
<div>
<h1>고정 제목</h1>
<p>{{ count }}</p>
</div>
</template>
// hoistStatic 적용 전 - 매 렌더마다 새 vnode 생성
function render() {
return h('div', [
h('h1', '고정 제목'), // 매번 새로 생성됨
h('p', count.value)
]);
}
// hoistStatic 적용 후 - 함수 바깥에서 1회만 생성
const _hoisted_1 = h('h1', '고정 제목'); // 컴포넌트 생성 시 1회만
function render() {
return h('div', [
_hoisted_1, // 캐시된 vnode 재사용
h('p', count.value)
]);
}
//예시 템플릿
<template>
<div :class="dynamicClass" :id="staticId" @click="handleClick">
{{ message }}
</div>
</template>
//컴파일 결과
function render() {
return h(
'div',
{
class: dynamicClass, // 동적
id: staticId, // 정적 (한 번 설정 후 안 변함)
onClick: handleClick
},
message,
1 /* TEXT */ | 2 /* CLASS */ // ← patchFlag
);
}
//예시 템플릿
<template>
<button @click="count++">증가</button>
</template>
//컴파일 결과
// cacheHandlers 적용 전
function render() {
return h('button', {
onClick: () => count.value++ // 매 렌더마다 새 함수 생성
}, '증가');
}
// cacheHandlers 적용 후
function render(_ctx, _cache) {
return h('button', {
onClick: _cache[0] || (_cache[0] = () => count.value++)
// ↑ 캐시 확인 ↑ 없으면 1회 생성 후 캐시에 저장
}, '증가');
}
React와 비교
// React: 매 렌더마다 새 함수 → 자식이 React.memo여도 리렌더 발생
<Button onClick={() => setCount(c => c + 1)} />
// React에서 막으려면 직접 useCallback 필요
const handleClick = useCallback(() => setCount(c => c + 1), []);
<Button onClick={handleClick} />
<!-- Vue: 컴파일러가 자동으로 useCallback 같은 효과를 적용 -->
<Button @click="count++" /> <!-- 별도 처리 불필요 -->
4. 배칭 처리
React 18+
- 자동 배칭(Automatic Batching) 도입
- 이벤트 핸들러, setTimeout, Promise 내부까지 모두 배치처리
- 강제 동기 처리가 필요하면 flushSync사용
Vue 3
- 상태변경을 마이크로태스크 큐에 모아 다음 tick에 한번에 렌더
- nextTick()으로 DOM 업데이트 완료 후 콜백 실행 가능
5. 실무적 시사점
React : 개발자가 최적화를 직접 제어 -> 유연하지만 실수 여지 있음
Vue : 프레임워크가 최적화를 담당 -> 예측 가능하지만 내부 추상화 의존
React는 렌더링을 추적하는 습관, Vue는 반응형 데이터 범위를 명확히 하는 습관이 성능 디버깅의 핵심이 된다.
6. Vue SFC
SFC(Single File Component)
<!-- MyComponent.vue -->
<template>
<div class="box">{{ message }}</div>
</template>
<script setup>
const message = 'Hello'
</script>
<style scoped>
.box { color: red; }
</style>
HTML + JS + CSS가 한 파일에 합쳐진 .vue 파일을 SFC라고 부른다.
.vue 파일을 vite를 통해 빌드하면
// MyComponent.vue → 빌드 후 (개념적 결과물)
import { createElementVNode as _createElementVNode } from 'vue'
export default {
setup() {
const message = 'Hello'
return { message }
},
render(_ctx) {
return _createElementVNode('div', { class: 'box' }, _ctx.message)
}
}
7. Vue와 React 렌더링 과정(초기 렌더 ~ 업데이트)
전체 흐름을 초기 마운트와 업데이트 두 단계로 나눠서 비교한다.
1) React 렌더링 과정
1-1 초기 마운트
1. JSX 작성
<div><Counter /></div>
↓ Babel 컴파일
React.createElement(Counter)
↓
2. createElement → Element 객체 생성 (description일 뿐, 실제 DOM 아님)
{ type: Counter, props: {}, ... }
↓
3. ReactDOM.createRoot().render() 호출
↓
4. React가 컴포넌트 함수 실행
function Counter() {
const [count, setCount] = useState(0); // Fiber 노드에 hook 상태 저장
return <button>{count}</button>;
}
↓
5. Fiber 트리 생성 (Reconciliation의 작업 단위)
각 컴포넌트/엘리먼트 = Fiber 노드
↓
6. Commit Phase
실제 DOM 노드 생성 → document에 삽입
1-2. 업데이트
이벤트 발생 (예: setCount(1))
↓
1. State 변경 예약 (스케줄링)
- React 18: 자동 배칭으로 여러 setState 묶음
↓
2. Render Phase (재조정/Reconciliation)
- 컴포넌트 함수 재실행 → 새 Element 트리 생성
- 이전 Fiber 트리와 새 Element 트리 비교 (diffing)
- 변경 필요한 Fiber에 표시(flag)
⚠️ 이 단계는 중단 가능 (Concurrent Mode)
- 우선순위 높은 작업(예: 사용자 입력)이 끼어들면 일시 중지
↓
3. Commit Phase
- 표시된 변경사항만 실제 DOM에 적용
- 동기적으로 한 번에 실행 (중단 불가)
- useEffect 콜백 실행
React 핵심 키워드
Element → Fiber → Reconciliation → Commit
↑
이 단계가 "Virtual DOM diffing"
2) Vue3 렌더링 과정
2-1. 초기 마운트
1. .vue 파일 작성
<template><button>{{ count }}</button></template>
↓ 컴파일러 (빌드 타임)
2. render 함수로 변환 + 최적화 적용
function render(_ctx) {
return h('button', null, _ctx.count, 1 /* TEXT patchFlag */)
}
↓
3. createApp().mount() 호출
↓
4. setup() 실행
const count = ref(0)
return { count }
↓
5. 반응형 객체 생성 (Proxy)
ref(0) → { value: 0 }을 Proxy로 감쌈
getter/setter 인터셉트 가능 상태
↓
6. 첫 render() 실행
- render 함수 실행 중 count.value 접근
- "이 컴포넌트는 count에 의존한다" → 의존성 자동 수집(track)
↓
7. VNode 생성 → 실제 DOM 생성 → 마운트
2-2. 업데이트
count.value++ 실행
↓
1. Proxy의 setter 트리거 (trigger)
- "count를 구독 중인 effect들"을 찾아서 큐에 등록
↓
2. 비동기 큐잉 (스케줄러)
- 같은 tick에 여러 변경이 있어도 1번만 재실행 예약
- 마이크로태스크로 처리 → nextTick에서 실행
↓
3. 컴포넌트 re-render 함수 실행
- 컴파일 타임에 박힌 patchFlag 확인
- patchFlag=1(TEXT)이면 → 텍스트 콘텐츠만 비교
- hoistStatic된 노드는 비교 자체 스킵
↓
4. 최소 범위 patch → 실제 DOM 업데이트
Vue 핵심 키워드
Proxy(반응형) → track/trigger → Scheduler(큐잉) → 컴파일 최적화 patch
↑
"필요한 컴포넌트만, 필요한 부분만" 자동 추적
┌─────────────────────────────────────────────────────────┐
│ React │
├─────────────────────────────────────────────────────────┤
│ 트리거: setState 호출 │
│ 추적 방식: 없음 (호출되면 무조건 재실행) │
│ 비교 범위: 컴포넌트 전체 트리 재실행 후 diff │
│ 비교 대상: 이전/이후 Element 트리 전체 │
│ 최적화 책임: 개발자 (memo, useMemo, useCallback) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Vue 3 │
├─────────────────────────────────────────────────────────┤
│ 트리거: reactive 데이터 변경 (Proxy setter) │
│ 추적 방식: 렌더 중 자동 의존성 수집(track) │
│ 비교 범위: patchFlag로 표시된 동적 부분만 │
│ 비교 대상: 정적 노드는 애초에 비교 대상 아님 (hoisted) │
│ 최적화 책임: 컴파일러 (개발자는 거의 신경 안 씀) │
└─────────────────────────────────────────────────────────┘
3) 타임라인
React
[State 변경]
↓
[전체 함수 재실행] → "이 컴포넌트가 의존하는 모든 값" 다시 계산
↓
[새 Element 트리 vs 이전 Fiber 트리 diff]
↓
[변경분만 DOM commit]
Vue3
[Proxy 변경 감지]
↓
[해당 값을 구독한 effect만 큐에 등록]
↓
[다음 tick에 해당 컴포넌트 render 재실행]
↓
[patchFlag로 동적 부분만 비교 → DOM patch]
8. 가장 큰 철학 차이
React: "값이 바뀌면 함수를 다시 실행해서 무엇이 바뀌었는지 비교한다"
→ Pull 방식 (재계산 후 비교)
Vue: "어떤 값이 바뀌면 그 값을 쓰는 곳만 정확히 알고 있다"
→ Push 방식 (변경 즉시 구독자에게 알림)
이 차이 때문에 :
- React : 컴포넌트가 커질수록 불필요한 재실행 가능성 ↑ -> 수동 최적화 필요
- Vue : 의존성 추적이 정교해서 자동으로 최소 범위만 업데이트 -> 단, Proxy 추적의 엣지 케이스(배열 인덱스 직접 할당, 구조 분해 시 반응성 손실 등) 주의 필요
'tech epilogue' 카테고리의 다른 글
| 브라우저 렌더링 최적화: Canvas vs CSS Transform, 그리고 WebWorker (0) | 2026.06.26 |
|---|---|
| Rolling Session (0) | 2026.06.23 |
| 파편적으로 알던 SSR을 제대로 연결해보기 (0) | 2026.06.02 |
| Jira 티켓 하나로 PR까지 자동 생성하는 AI 에이전트 만들기 (0) | 2026.05.28 |
| 권한 체계 설계하기(feat. vue, pinia) (0) | 2026.05.20 |