2,3주차도 적었어야 했는데 시간이 훅 가버림..ㅠ
매번 계획대로 나름 잘 진행하는 것 같은데 항상 목요일이 되면 급박하게 하게되는 것 같다ㅠ
react의 동작방식은 항상 공부해야지...해야지...하다가 미뤄둔 주제인데 이번기회에 공부를 해볼 수 있었다. 아래 내용은 공부해서 더 추가될 예정...
일단 어찌저찌 과제 제출하는건 유지하기.
여전히 개념에 대해 깊이 이해하지 못하고 먼저 코드를 작성하게 되는것 같다.
개념먼저 이해 -> 과제 = 시간부족,,,
일단 과제 -> 이후 개념 이해 = 개념 공부를 미뤄버림,,,
적당한 타협점을 찾아봐야 할 것 같다.
그리고 이제 목요일 밤새기 금지..!
이번 코드 리뷰때 AI를 다양하게 활용해서 과제를 푸시는 분들을 봤는데 AI사용에 있어서 나도 좀더 활용할 수 있는 방안에 대해 공부해보면 좋을 것 같다.
이번 4,5주차 과제는 vanillaJS를 이용한 react 만들기였다.
이를 위해서는 react의 virtualDom, reconciliation, hook의 구조를 이해할 수 있어야 했다.
VirtualDom이란 실제 Dom을 좀더 가볍게 나타낸 Dom복사본이라 생각하면 될것 같다.
이 VirtualDom과 실제 DOM을 비교해서 업데이트하는 과정을 Reconciliation(재조정)이라 한다.
[Component render]
↓
Virtual DOM 생성
↓
[이전 VDOM] — diff —> [새 VDOM]
↓
필요한 DOM 변경만 실제 DOM에 반영 (patch)
↓
Memoization ← 렌더 최적화
createElement
createElement 함수는 react에서 JSX를 VirtualDOM 객체로 변환할 때 babel이 호출하는 함수이다. @babel/plugin-transform-react-jsx(React 17이후: @babel/preset-react) 플러그인에 정의된 해당 함수를 사용함.
// JSX
<div>Hello</div>
// vDom
createElement("div", null, "Hello");
babel을 사용하면 위의 jsx를 아래의 vDom으로 변경시켜 준다.
react의 hook정보와 이전 render정보가 저장되는 context
RootContext (렌더링 루트 관리)
렌더링의 최상위 정보를 관리
HooksContext (훅 상태 관리)
각 컴포넌트의 훅 상태를 경로 기반으로 격리하여 관리
EffectsContext (이펙트 큐 관리)
렌더링 완료 후 비동기로 실행
Instance 구조
Instance는 VNode와 실제 DOM을 연결하는 데이터 구조
kind: 노드 타입 (HOST, TEXT, COMPONENT, FRAGMENT)
dom: 실제 DOM 노드 참조
node: 가상 DOM 노드 (VNode)
children: 자식 인스턴스 배열
key: 리스트 렌더링용 키
path: 컴포넌트의 고유 경로 (hooks 관리용)
useState가 호출될때 cursor(컴포넌트에서 hook의 순서)와 path(컴포넌트의 path)값을 클로저로 저장한다.
hooksState[cursor] = inithook의 배열을 업데이트 후 render()context.effects.queue.push)it("상태가 변경되면 다시 렌더링한다", async () => {
const container = document.createElement("div");
let setCount: ((updater: (value: number) => number) => void) | undefined;
function Counter() {
const [count, update] = useState(0);
setCount = update;
return <button onClick={() => update((value) => value + 1)}>{count}</button>;
}
setup(<Counter />, container);
let button = container.querySelector("button") as HTMLButtonElement;
expect(button).not.toBeNull();
expect(button?.textContent).toBe("0");
setCount!((value) => value + 1);
await flushMicrotasks();
button = container.querySelector("button") as HTMLButtonElement;
expect(button?.textContent).toBe("1");
button?.click();
await flushMicrotasks();
button = container.querySelector("button") as HTMLButtonElement;
expect(button?.textContent).toBe("2");
});
여기서 toBe가 3으로 나와서 테스트가 통과가 안됐는데 원인은 addEventlistner가 중복으로 들어가서 였음
export const updateDomProps = (
dom: HTMLElement,
prevProps: Record<string, any> = {},
nextProps: Record<string, any> = {},
): void => {
// 이벤트 핸들러 처리: 변경되거나 제거된 이벤트 핸들러 제거
Object.keys(prevProps).forEach((key) => {
if (key === "children") return;
const prevValue = prevProps[key];
const nextValue = nextProps[key];
// 이벤트 핸들러인 경우
if (key.startsWith("on") && typeof prevValue === "function") {
const eventName = key.slice(2).toLowerCase();
// 이벤트 핸들러가 제거되었거나 변경된 경우
if (nextValue === undefined || nextValue !== prevValue) {
dom.removeEventListener(eventName, prevValue as EventListener);
}
}
});
// nextProps에 없는 일반 속성 제거
Object.keys(prevProps).forEach((key) => {
if (key === "children") return;
if (key.startsWith("on")) return; // 이벤트 핸들러는 위에서 처리됨
if (nextProps[key] === undefined) {
// className은 "class" 속성으로 제거
if (key === "className") {
dom.removeAttribute("class");
} else if (key === "style" && typeof prevProps[key] === "object") {
// 스타일 객체의 경우 모든 속성 제거
Object.keys(prevProps[key] || {}).forEach((styleKey) => {
(dom as HTMLElement).style.removeProperty(styleKey);
});
} else {
dom.removeAttribute(key);
}
}
});
// 새 속성 설정 (변경된 속성 포함)
setDomProps(dom, nextProps);
};
updateDomProps실행시 변경되거나 제거된 이벤트 핸들러 제거후 새롭게 추가된 props 추가하게 변경
// 중첩된 컴포넌트에서 useState가 각각 독립적으로 동작한다[test]
<div>
<h1>Dynamic List</h1>
{Array.from({ length: itemCount }, (_, i) => (
<Item id={i} />
))}
<Footer />
</div>
context.hooks.state
Map(5) {
'0' => [ 2 ],
'0.c0.c1' => [ 1 ],
'0.c0.c2' => [ 2 ],
'0.c0.c3' => [ 0 ],
'0.c0.c4' => [ 101 ]
}
위의 코드에서 length값이 3->2로 바뀌면 저장되었던 Footer의 상태값이 바뀌면서 '0.c0.c4'에 저장된 Footer의 상태값을 쓰지못하고 '0.c0.c3'에 저장된 값인 0이 상태값으로 나오면서 테스트가 실패
${parentPath}.c${index}.t${typeName} 형식으로 경로 생성변경후 context.hooks.state
Map(4) {
'0' => [ 2 ],
'0.c0.tdiv.c1.tItem' => [ 1 ],
'0.c0.tdiv.c2.tItem' => [ 2 ],
'0.c0.tdiv.c4.tFooter' => [ 101 ]
}