From 9df3a440981afc7e122644ef33d76afc84d23680 Mon Sep 17 00:00:00 2001 From: dhp94d Date: Wed, 26 Mar 2025 11:42:40 +0900 Subject: [PATCH 1/2] =?UTF-8?q?dhyun2(2=EC=A3=BC=EC=B0=A8):=20=EB=A6=B4?= =?UTF-8?q?=EB=A6=AC=EC=A6=88=20=EB=85=B8=ED=8A=B8=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=ED=9B=88=EB=A0=A8(vue)=20-=20reactivity=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=20=EC=9E=AC=EA=B7=80=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EA=B1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\352\267\200 \353\260\251\354\247\200 .md" | 145 ++++++++++++++++++ .../readme.md" | 115 ++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 "1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/fix(reactivity): \352\263\204\354\202\260\353\220\234 \352\262\214\355\204\260\354\227\220\354\204\234 \353\254\264\355\225\234 \354\236\254\352\267\200 \353\260\251\354\247\200 .md" create mode 100644 "1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/\352\266\214\355\230\204\354\247\200/readme.md" diff --git "a/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/fix(reactivity): \352\263\204\354\202\260\353\220\234 \352\262\214\355\204\260\354\227\220\354\204\234 \353\254\264\355\225\234 \354\236\254\352\267\200 \353\260\251\354\247\200 .md" "b/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/fix(reactivity): \352\263\204\354\202\260\353\220\234 \352\262\214\355\204\260\354\227\220\354\204\234 \353\254\264\355\225\234 \354\236\254\352\267\200 \353\260\251\354\247\200 .md" new file mode 100644 index 0000000..b41e2b8 --- /dev/null +++ "b/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/fix(reactivity): \352\263\204\354\202\260\353\220\234 \352\262\214\355\204\260\354\227\220\354\204\234 \353\254\264\355\225\234 \354\236\254\352\267\200 \353\260\251\354\247\200 .md" @@ -0,0 +1,145 @@ +# Vue 3.5 RC1 - Reactivity 무한 재귀 버그 수정 + +## 개요 + +Vue 3.5 RC1에서는 중요한 반응성 관련 버그가 수정되었다. +기존에는 정상적으로 작동하던 `ref`와 `reactive` 조합이 +Vue 3.5에서 **무한 재귀 호출**로 인해 `Maximum call stack size exceeded` 에러를 유발하게 되었다. +이 글에서는 그 원인과 해결 과정을 Vue 내부 동작과 함께 분석한다. + +--- + +## 문제 상황 + +Vue 공식 CHANGELOG에서도 해당 문제는 다음과 같이 명시되어 있다. + +> [reactivity: avoid infinite recursion when mutating ref wrapped in reactive](https://github.com/vuejs/core/blob/main/CHANGELOG.md#350-rc1-2024-08-29) +> [commit 313e4bf](https://github.com/vuejs/core/commit/313e4bf)) +> Closes [#11696](https://github.com/vuejs/core/issues/11696) + +--- + +## 재현 코드 + +```vue + + + +``` + +--- + +## 내부 동작 설명 + +Vue에서 `reactive()`는 내부적으로 `Proxy`를 생성하고, 이를 조작할 수 있는 **핸들러 클래스**를 지정한다. +가장 일반적인 경우, Vue는 `BaseReactiveHandler`를 기반으로 한 `MutableReactiveHandler` 를 사용한다. + +`MutableReactiveHandler`는 `Proxy`의 `set()` 을 다음과 같이 오버라이드한다: + +### set 메서드 구조 + +```ts +set( + target: Record, + key: string | symbol, + value: unknown, + receiver: object +): boolean +``` + +### 기존 구현 방식 + +```ts +const result = Reflect.set(target, key, value, receiver); +``` + +여기서 중요하게 볼 인자는 4번째 `receiver`이다. 해당 인자는 setter가 실행될 때 this로 사용할 값을 전달한다. + +이때 문제는 `receiver`가 `ref` 객체일 경우, 내부 `setter`에서 `this`가 Proxy로 바뀌어버린다는 점이다. +`ref`는 내부적으로 다음과 같은 getter/setter 구조를 가진다: + +```ts +{ + get value() { + track() + return _value + }, + set value(newVal) { + _value = newVal + trigger() + } +} +``` + +만약 `this`가 `ref`가 아닌 Proxy라면 → `this._value`가 정의되지 않아 동작이 꼬이게 된다. +그리고 내부에서 다시 reactive 처리가 일어나면서 **무한 재귀**가 발생하는 것이다. + +--- + +## 문제가 되는 케이스 예시 + +```ts +import { reactive, ref, effect } from 'vue'; + +const a = reactive(ref(1)); + +effect(() => { + console.log(a.value); // 이걸 반응형으로 감시함 +}); + +a.value++; +``` + +`Maximum call stack size exceeded`이 발생하게 된다. + +1. effect(() => console.log(a.value)) 실행 + -> a.value에 의존성을 등록하여 a.value가 바뀌면 effect가 다시 실행되어야함 +2. a.value++ 실행 + -> set a.value = 2 가 실행됨 이제 proxy의 set()트랩이 실행되고, vue 내부에서 trigger()가 호출됨 +3. trigger()가 종속된 effect 재실행 +4. effect안에 a.value가 있음 + -> 다시 getter 부르고 끝나야하지만, vue 내부 로직이 깨졌기 때문에 setter를 또 트리거 + +> Q. 왜 setter를 또 트리거 하나? +> this가 ref가 아니라 Proxy가 돼서, trigger()가 ref가 아닌걸 트리거하거나, 값 비교가 꺠져서 항상 바뀌었다고 인식됨. +> getter가 내부에서 Proxy객체의 .value를 다시 평가하게되고, 이 과정에서 proxy set트랩에 걸려서 setter가 또 호출되는 형식 + +--- + +## 해결 방법 + +Vue 팀은 `MutableReactiveHandler` 내부에서 `Reflect.set()` 호출 시 +다음처럼 **receiver의 정확한 컨텍스트**를 보장하도록 수정했다: + +```ts +const result = Reflect.set(target, key, value, isRef(target) ? target : receiver); +``` + +즉, `target`이 `ref`라면 → `receiver`로 `target` 자체를 넘겨서 +ref 내부의 setter에서 `this === ref`가 되도록 보장해준다. + +--- + +## 핵심 요약 + +| 항목 | 설명 | +| --------- | ------------------------------------------------------------------------- | +| 문제 버전 | Vue 3.5 RC 이전 | +| 증상 | `ref`를 `reactive`로 감쌌을 때 값 변경 시 무한 루프 | +| 원인 | Proxy의 `set` 내부에서 ref의 `this` 컨텍스트가 깨짐 | +| 해결 | `Reflect.set()`에서 `receiver`를 `isRef(target)`인 경우 `target`으로 지정 | +| 상태 | Vue 3.5 RC1에서 패치 완료 | + +--- diff --git "a/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/\352\266\214\355\230\204\354\247\200/readme.md" "b/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/\352\266\214\355\230\204\354\247\200/readme.md" new file mode 100644 index 0000000..19dff00 --- /dev/null +++ "b/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/\352\266\214\355\230\204\354\247\200/readme.md" @@ -0,0 +1,115 @@ +라이브러리가 제공할 수 있는 세 가지 주요 자바스크립트 파일 유형 (ES 모듈, “클래식” 전역 변수 유형, CommonJS) +- [제가 최근에 쓴 블로그 글이에요 .](https://velog.io/@khjbest/%EC%8B%9C%EB%82%98%EB%B8%8C%EB%A1%9C-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%EA%B0%80%EC%83%81-%EB%8F%94%EA%B3%BC-%EC%9B%B9-%EC%84%9C%EB%B2%84-%EA%B8%B0%EC%B4%88Express-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4#-commonjs%EC%99%80-es-modules%EC%9D%98-%EC%B0%A8%EC%9D%B4) + +```json +// 패키지를 CommonJS 방식(예: Node.js의 require())으로 불러올 때 기본으로 사용하는 파일 + "main": "./dist/mini-query.umd.cjs", +// ES 모듈(ESM)을 지원하는 환경에서 import 문으로 불러올 때 사용할 파일을 지정 + "module": "./dist/mini-query.js", +// 엔트리 포인트를 보다 명시적으로 제어 + "exports": { + ".": { +// ES 모듈(import)로 불러올 경우: "./dist/mini-query.js" 파일을 사용 + "import": "./dist/mini-query.js", +// CommonJS(require)로 불러올 경우: "./dist/mini-query.umd.cjs" 파일을 사용 + "require": "./dist/mini-query.umd.cjs" + } +``` + +> Q . 그러면 exports를 선언했을 때, main이나 module옵션은 뺴도 되지않아 ? +> +> A . 일부 구 버전 Node.js나 exports를 지원하지 않는 도구에서는 여전히 main이나 module 필드를 참고할 수 있으므로, 호환성을 고려한다면 함께 선언해주는 것이 좋다. + +--- +Import Maps는 웹 브라우저에서 ES 모듈을 로드할 때, 모듈 이름과 URL 간의 매핑을 명시적으로 지정할 수 있는 기능입니다. +이를 통해 코드 내에서 사용하는 모듈의 "별칭"을 실제 파일 경로나 CDN 주소로 연결할 수 있습니다. + +예를 들어, 다음과 같이 HTML 파일에 ` +``` + +위와 같이 설정하면, JavaScript 코드에서 + +```javascript +import _ from 'lodash'; +``` + +라고 작성했을 때 브라우저는 `'lodash'`라는 모듈명을 설정된 URL(`https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js`)로 해석하여 해당 파일을 로드합니다. + +--- + +#### “클래식” JS 파일 +> 아에 그냥 파일이 프로젝트에 포함되어있구나@@근데이제 umd.js확장자 +> +> "클래식"파일? 별도의 빌드 도구나 모듈 시스템 없이, 브라우저가 기본적으로 이해하는 전통적인 방식의 자바스크립트 파일과 동일. UMD 형식은 이러한 클래식 파일처럼 바로 사용할 수 있으면서도, 현대의 모듈 시스템과도 호환되도록 함. +> +> 빌드 시스템 없이도 자바스크립트 라이브러리를 로드하려면, 라이브러리가 전역 변수로 정의되어야 하는데, UMD 파일은 이 점을 보장하면서도, 모듈 시스템을 사용하는 환경에서는 해당 시스템에 맞춰 올바르게 동작할 수 있게 설계되어 있음. + +✔️ 사용 방법: +```html + +``` + +✔️ 식별 방법 +- 웹사이트에 “CDN으로 사용하세요!”와 같은 큰 친절한 배너가 있는 경우 +- .umd.js 확장자 +- ` + ``` + 이렇게 하면, 코드에서는 "my-module"이라는 별칭만 사용해도, 브라우저가 import map을 참고하여 실제 모듈의 위치(예를 들어 외부 라이브러리나 의존성이 있는 모듈)를 찾아서 로드합니다. + +esbuild나 다른 ES 모듈 번들러 사용 +package.json의 "type": "module" (정확히 어떤 파일을 가리키는지는 명확하지 않음) + +>요놈이 이제 표준화 + +#### CommonJS 모듈 +✔️식별 방법: +- 코드에서 require()나 module.exports = ... 찾기 +- .cjs 확장자 +- package.json의 "type": "commonjs" From 05333aeafb2f1d6b7f1444a32f201c3b12570cc6 Mon Sep 17 00:00:00 2001 From: dhp94d Date: Wed, 26 Mar 2025 11:43:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?dhyun2(2=EC=A3=BC=EC=B0=A8):=20v3.5=20vue?= =?UTF-8?q?=20ref=20=ED=83=90=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vue-ref.md" | 597 ++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 "1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/vue-ref.md" diff --git "a/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/vue-ref.md" "b/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/vue-ref.md" new file mode 100644 index 0000000..b76dfa4 --- /dev/null +++ "b/1. \353\271\214\353\223\234 \354\213\234\354\212\244\355\205\234/hyun2/\353\246\264\353\246\254\354\246\210 \353\205\270\355\212\270 \353\266\204\354\204\235 \355\233\210\353\240\250/vue-ref.md" @@ -0,0 +1,597 @@ +# Vue Ref 구현 상세 분석 + +## 개요 + +Vue의 `ref`는 반응성 시스템의 핵심 기능 중 하나다. +이 글에서는 `ref`의 내부 구현과 동작 원리를 자세히 살펴본다. + +## 1. Ref 생성 과정 + +```typescript +// 1. ref 호출 +export function ref(value: T): Ref { + return createRef(value, false); +} + +// 2. createRef 실행 +function createRef(rawValue: T, shallow: boolean): Ref { + if (isRef(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); +} +``` + +여기서 중요한 점은: + +- `ref()` 호출 시 `createRef()`로 전달 +- 이미 ref인 경우 그대로 반환 (중복 래핑 방지) +- 아닌 경우 `RefImpl` 인스턴스 생성 + +### 예시 + +```typescript +const count = ref(1); // RefImpl 인스턴스 생성 +const count2 = ref(count); // 이미 ref이므로 그대로 반환 +``` + +## 2. RefImpl 클래스 + +```typescript +class RefImpl { + _value: T; + private _rawValue: T; + dep: Dep = new Dep(); + public readonly [ReactiveFlags.IS_REF] = true; + public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false; + + constructor(value: T, isShallow: boolean) { + this._rawValue = isShallow ? value : toRaw(value); + this._value = isShallow ? value : toReactive(value); + this[ReactiveFlags.IS_SHALLOW] = isShallow; + } + + get value() { + if (__DEV__) { + this.dep.track({ + target: this, + type: TrackOpTypes.GET, + key: 'value', + }); + } else { + this.dep.track(); + } + return this._value; + } + + set value(newValue) { + const oldValue = this._rawValue; + const useDirectValue = this[ReactiveFlags.IS_SHALLOW] || isShallow(newValue) || isReadonly(newValue); + newValue = useDirectValue ? newValue : toRaw(newValue); + if (hasChanged(newValue, oldValue)) { + this._rawValue = newValue; + this._value = useDirectValue ? newValue : toReactive(newValue); + if (__DEV__) { + this.dep.trigger({ + target: this, + type: TriggerOpTypes.SET, + key: 'value', + newValue, + oldValue, + }); + } else { + this.dep.trigger(); + } + } + } +} +``` + +### Getter/Setter 동작 + +1. **Getter** + + ```typescript + // 예시 + const count = ref(1); + effect(() => { + console.log(count.value); // getter 호출 + }); + ``` + + - `value` 접근 시 `dep.track()` 호출 + - 개발 환경에서는 상세 정보 포함 (디버깅 용이) + - 프로덕션 환경에서는 단순화된 추적 (성능 최적화) + +2. **Setter** + + ```typescript + // 예시 + count.value = 2; // setter 호출 + ``` + + - 값 변경 시 `hasChanged`로 변경 여부 확인 + - 변경된 경우 `_rawValue`와 `_value` 업데이트 + - `dep.trigger()`로 의존성 실행 + +## 3. Dep 클래스 + +```typescript +export class Dep { + version = 0; + activeLink?: Link = undefined; + subs?: Link = undefined; + subsHead?: Link; + map?: KeyToDepMap = undefined; + key?: unknown = undefined; + sc: number = 0; + + constructor(public computed?: ComputedRefImpl | undefined) { + if (__DEV__) { + this.subsHead = undefined; + } + } + + track(debugInfo?: DebuggerEventExtraInfo): Link | undefined { + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + + let link = this.activeLink; + if (link === undefined || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); + + // 이중 연결 리스트로 의존성 관리 + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; + } else { + link.prevDep = activeSub.depsTail; + activeSub.depsTail!.nextDep = link; + activeSub.depsTail = link; + } + + addSub(link); + } else if (link.version === -1) { + // 이전 실행에서 재사용 - 버전만 동기화 + link.version = this.version; + + // tail로 이동 처리 + if (link.nextDep) { + const next = link.nextDep; + next.prevDep = link.prevDep; + if (link.prevDep) { + link.prevDep.nextDep = next; + } + + link.prevDep = activeSub.depsTail; + link.nextDep = undefined; + activeSub.depsTail!.nextDep = link; + activeSub.depsTail = link; + + // head였다면 새로운 head 지정 + if (activeSub.deps === link) { + activeSub.deps = next; + } + } + } + + if (__DEV__ && activeSub.onTrack) { + activeSub.onTrack( + extend( + { + effect: activeSub, + }, + debugInfo + ) + ); + } + + return link; + } + + trigger(debugInfo?: DebuggerEventExtraInfo): void { + this.version++; + globalVersion++; + this.notify(debugInfo); + } + + notify(debugInfo?: DebuggerEventExtraInfo): void { + startBatch(); + try { + if (__DEV__) { + // 개발 환경에서 onTrigger 훅 실행 + for (let head = this.subsHead; head; head = head.nextSub) { + if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) { + head.sub.onTrigger( + extend( + { + effect: head.sub, + }, + debugInfo + ) + ); + } + } + } + // 실제 의존성 실행 (역순) + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + (link.sub as ComputedRefImpl).dep.notify(); + } + } + } finally { + endBatch(); + } + } +} +``` + +### Dep 클래스의 핵심 역할 + +1. **의존성 추적 (track)** + + ```typescript + // Dep 클래스의 track 메서드 + track(debugInfo?: DebuggerEventExtraInfo): Link | undefined { + // 1. 현재 실행 중인 effect 확인 + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + + // 2. 새로운 링크 생성 또는 재사용 + let link = this.activeLink; + if (link === undefined || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); + + // 3. 이중 연결 리스트로 의존성 관리 + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; + } else { + link.prevDep = activeSub.depsTail; + activeSub.depsTail!.nextDep = link; + activeSub.depsTail = link; + } + + // 4. 의존성 등록 + addSub(link); + } + } + ``` + + ```typescript + // 예시 + const count = ref(1); + const doubled = computed(() => count.value * 2); + const quadrupled = computed(() => doubled.value * 2); + + // 의존성 추적 과정 + // 1. count.value 접근 시 + effect(() => { + console.log(count.value); // track 호출 + // activeSub = 현재 effect + // link = 새로운 Link(현재 effect, count의 dep) + // activeSub.deps = link + }); + + // 2. doubled.value 접근 시 + effect(() => { + console.log(doubled.value); // track 호출 + // activeSub = 현재 effect + // link = 새로운 Link(현재 effect, doubled의 dep) + // activeSub.deps = link + }); + ``` + +2. **의존성 실행 (trigger/notify)** + + ```typescript + // Dep 클래스의 trigger 메서드 + trigger(debugInfo?: DebuggerEventExtraInfo): void { + // 1. 버전 증가 + this.version++; + globalVersion++; + // 2. 의존성 실행 + this.notify(debugInfo); + } + + // Dep 클래스의 notify 메서드 + notify(debugInfo?: DebuggerEventExtraInfo): void { + startBatch(); + try { + // 1. 개발 환경에서 onTrigger 훅 실행 + if (__DEV__) { + for (let head = this.subsHead; head; head = head.nextSub) { + if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) { + head.sub.onTrigger( + extend( + { + effect: head.sub, + }, + debugInfo + ) + ); + } + } + } + // 2. 실제 의존성 실행 (역순) + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + (link.sub as ComputedRefImpl).dep.notify(); + } + } + } finally { + endBatch(); + } + } + ``` + + ```typescript + // 예시 + const count = ref(1); + const doubled = computed(() => count.value * 2); + const quadrupled = computed(() => doubled.value * 2); + + // 의존성 실행 과정 + count.value = 2; // trigger 호출 + + // 1. count의 dep.trigger() 호출 + // - version 증가 + // - notify 호출 + + // 2. notify 메서드에서 + // - startBatch() 호출 + // - computed 값 처리 (doubled, quadrupled) + // - effect 처리 + // - endBatch() 호출 + ``` + +### 의존성 추적과 실행의 전체 흐름 + +1. **의존성 추적 (track)** + + ```typescript + // 예시 + const count = ref(1); + effect(() => { + console.log(count.value); // 1. getter 호출 + }); + + // track 메서드에서 + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; // 2. 현재 실행 중인 effect 확인 + } + + let link = this.activeLink; + if (link === undefined || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); // 3. 새로운 링크 생성 + + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; // 4. 첫 번째 의존성 + } else { + link.prevDep = activeSub.depsTail; // 5. 기존 의존성에 추가 + activeSub.depsTail!.nextDep = link; + activeSub.depsTail = link; + } + + addSub(link); // 6. 의존성 등록 + } + ``` + +2. **의존성 실행 (trigger/notify)** + + ```typescript + // 예시 + count.value = 2; // 1. setter 호출 + + // trigger 메서드에서 + this.version++; // 2. 버전 증가 + globalVersion++; + this.notify(debugInfo); // 3. 의존성 실행 + + // notify 메서드에서 + startBatch(); // 4. 배치 시작 + try { + // 5. 개발 환경에서 onTrigger 훅 실행 + if (__DEV__) { + for (let head = this.subsHead; head; head = head.nextSub) { + // ... onTrigger 훅 실행 + } + } + + // 6. 실제 의존성 실행 (역순) + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + (link.sub as ComputedRefImpl).dep.notify(); + } + } + } finally { + endBatch(); // 7. 배치 종료 + } + ``` + +이렇게 의존성 추적과 실행을 통해: + +1. 효율적인 의존성 관리 +2. 안정적인 업데이트 순서 +3. 불필요한 재실행 방지 +4. 메모리 최적화 + 를 달성할 수 있다. + +### 의존성 등록 (addSub) + +```typescript +// Dep 클래스의 addSub 메서드 +function addSub(link: Link): void { + // 1. Dep의 subs 리스트에 추가 + if (!link.dep.subs) { + link.dep.subs = link; + } else { + link.prevSub = link.dep.subs; + link.dep.subs = link; + } + + // 2. 개발 환경에서 subsHead 관리 + if (__DEV__) { + if (!link.dep.subsHead) { + link.dep.subsHead = link; + } else { + link.nextSub = link.dep.subsHead; + link.dep.subsHead = link; + } + } +} +``` + +이 코드에서 중요한 점은: + +1. **Dep의 subs 리스트 관리** + + ```typescript + // 예시 + const count = ref(1); + effect(() => { + console.log(count.value); // 첫 번째 의존성 + }); + + // addSub 호출 시 + if (!link.dep.subs) { + link.dep.subs = link; // 첫 번째 의존성 + } else { + link.prevSub = link.dep.subs; // 기존 의존성 앞에 추가 + link.dep.subs = link; + } + ``` + + - 첫 번째 의존성은 `subs`에 직접 할당 + - 이후 의존성은 `prevSub`로 연결하여 리스트 구성 + - `notify` 메서드에서 `prevSub`로 역순 실행 가능 + +2. **의존성 실행 순서** + + ```typescript + // 예시 + const count = ref(1); + const doubled = computed(() => count.value * 2); + effect(() => { + console.log(doubled.value); + }); + + // 의존성 등록 순서 + count -> doubled -> effect + + // notify 메서드에서 + for (let link = this.subs; link; link = link.prevSub) { + // effect -> doubled -> count 순서로 실행 + } + ``` + + - `addSub`으로 의존성 등록 시 순서 유지 + - `notify` 메서드에서 역순으로 실행하여 최신 값 사용 + +이렇게 `addSub`을 통해: + +1. 의존성의 효율적인 관리 +2. 개발 환경에서의 디버깅 지원 +3. 안정적인 실행 순서 보장 + 을 달성할 수 있다. + +## 4. Effect와 배치 시스템 + +```typescript +export function startBatch(): void { + batchDepth++; +} + +export function endBatch(): void { + if (--batchDepth > 0) return; + + // computed 값 처리 + if (batchedComputed) { + let e: Subscriber | undefined = batchedComputed; + batchedComputed = undefined; + while (e) { + e = e.next; + } + } + + // 일반 effect 처리 + while (batchedSub) { + let e: Subscriber | undefined = batchedSub; + batchedSub = undefined; + while (e) { + if (e.flags & EffectFlags.ACTIVE) { + (e as ReactiveEffect).trigger(); + } + e = e.next; + } + } +} +``` + +### 배치 시스템의 역할 + +1. **중첩 업데이트 처리** + + ```typescript + // 예시 + const count = ref(1); + effect(() => { + count.value = 2; // 첫 번째 업데이트 + count.value = 3; // 두 번째 업데이트 + }); + // 한 번의 배치로 처리 + ``` + + - 여러 업데이트를 하나의 배치로 묶음 + - 중첩된 업데이트를 올바르게 처리 + +2. **순서 보장** + + ```typescript + // 예시 + const count = ref(1); + const doubled = computed(() => count.value * 2); + effect(() => { + console.log(doubled.value); + }); + count.value = 2; + // 1. computed 값 처리 + // 2. effect 처리 + ``` + + - computed 값 먼저 처리 + - 일반 effect 나중 처리 + - 역순으로 실행하여 의존성 순서 보장 + +3. **에러 처리** + + ```typescript + // 예시 + const count = ref(1); + effect(() => { + try { + count.value = 2; + } catch (err) { + console.error(err); + } + }); + ``` + + - try-catch로 안전한 실행 + - 에러 발생 시 적절한 처리 + +## 🧠 핵심 요약 + +| 구분 | 설명 | 목적 | +| ------------- | -------------- | ----------------- | +| Ref | 반응성 값 관리 | 값의 반응성 처리 | +| Dep | 의존성 관리 | 의존성 추적/실행 | +| Track/Trigger | 변경 감지 | 의존성 등록/실행 | +| 배치 | 업데이트 관리 | 효율적인 업데이트 | + +이런 구조를 통해 Vue는: + +1. 효율적인 반응성 처리 +2. 안정적인 상태 관리 +3. 예측 가능한 동작 + 을 달성하고 있다.