인터랙티브한 웹의 경우
레이아웃과 리페인팅에 발생하는 비용이 크다
자식 요소가 많은 경우 더욱 그렇다
싱글페이지 어플리케이션의 경우에는 더더욱 비용이 커진다
이러한 문제를 해결하고자 탄생한 것이 가상 DOM 이다
가상 DOM 은 브라우저에서 관리하는 것이 아니라
React 에서 관리한다
실제 DOM 과 같은 객체를 리액트에서 가지고 있다가 변경점만 찾아 변경 후 실제 DOM 에 적용시킨다
가상 DOM 의 오해는 빠르다는 것이다
항상 빠른 것은 아니고 대부분의 상황에서 충분히 빠르다
리액트는 특히 클라이언트의 자원을 이용해 렌더링 하기 때문에 클라이언트의 성능에 많이 의존해야 하는데
최소한의 DOM 업데이트를 수행하도록 하면 렌더링하는 비용을 아껴 성능에도 이점을 갖게 되지 않을까 싶다
변경점을 찾기 위해서는 state 를 사용한 곳을 표시하고 state 변경 시 해당 위치에서만 변경이 일어나야 한다
React 는 babel.transform 을 하여
JSX 코드를 jsx() jsxs() 함수로 변환
jsx() 와 jsxs() 함수에서는 각각
createElement() 를 실행하여 virtualDOM 객체를 생성한다
state 를 변경하여 App() 컴포넌트의 리렌더링을 일으킬 때 변경점을 찾기 위해 렌더링 전/후 App() 을 실행해보면 실행된 App() 컴포넌트에서는 항상 state 초깃값만을 반환한다
App() 이 리렌더링 되면서 useState() 가 재실행 될 때 기존의 값을 참조하지 않고 있어 발생하는 문제로 보인다
currentRender 필드 추가로 기존의 값을 저장하여
리렌더링 시에 참조하도록 수정
class React {
states = new Map()
stateIndex = 0
listeners = []
currentRender = null
constructor() {
this.currentComponent = null
}
setState = (index, newState) => {
const states = this.states.get(this.currentComponent)
if (typeof newState === 'function') {
states[index] = newState(states[index])
} else {
states[index] = newState
}
this.listeners.forEach(listener => listener())
}
subscribe = listener => {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter(l => l !== listener)
}
}
useState = initialValue => {
if (!this.states.has(this.currentComponent)) {
this.states.set(this.currentComponent, [])
}
const states = this.states.get(this.currentComponent)
if (states.length <= this.stateIndex) {
states[this.stateIndex] = initialValue
}
const index = this.stateIndex
const setState = this.setState.bind(this, index)
if (!this.currentRender) {
this.currentRender = states
}
this.stateIndex++
return [this.currentRender[index], setState]
}
render = tree => {
const rootElement = document.createElement(tree.type)
if (tree.type === 'h1') {
console.log(tree, 'tree') // + 1
}
const { children, onClick, onChange, checked, ...elementProps } = tree.props
if (onClick) rootElement.addEventListener('click', onClick)
if (onChange) rootElement.addEventListener('change', e => onChange(e))
if (checked) rootElement.checked = checked
Object.keys(elementProps).forEach(key => {
rootElement.setAttribute(key, elementProps[key])
})
if (children) {
if (Array.isArray(children)) {
children.map(child => {
if (typeof child !== 'object') {
rootElement.appendChild(document.createTextNode(child))
} else {
rootElement.appendChild(this.render(child))
}
})
} else {
if (typeof children !== 'object') {
rootElement.appendChild(document.createTextNode(children))
} else {
rootElement.appendChild(this.render(children))
}
}
}
return rootElement
}
renderComponent = component => {
this.currentComponent = component
this.stateIndex = 0
return this.render(component())
}
}
const react = new React()
export default react
class React {
states = new Map()
stateIndex = 0
listeners = []
currentRender = null
currentComponent = null
prevVDOM = null
setState = (index, newState) => {
const states = this.states.get(this.currentComponent)
if (typeof newState === 'function') {
states[index] = newState(states[index])
} else {
states[index] = newState
}
this.listeners.forEach(listener => listener()) // subscribe 함수 호출
}
subscribe = listener => {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter(l => l !== listener)
}
}
useState = initialValue => {
if (!this.states.has(this.currentComponent)) {
this.states.set(this.currentComponent, [])
}
const states = this.states.get(this.currentComponent)
if (states.length <= this.stateIndex) {
states[this.stateIndex] = initialValue
}
const index = this.stateIndex
const setState = this.setState.bind(this, index)
if (!this.currentRender) {
console.log(this.currentRender, 'this.currentRender')
this.currentRender = states
}
this.stateIndex++
return [this.currentRender[index], setState]
}
diff = (currentTree, prevTree, currentTarget = [], prevTarget = []) => {
if (currentTree === prevTree) return { isChanged: false }
const { type: currentType, props: currentProps, childrenNode: currentChildrenNode } = currentTree
const { type: prevType, props: prevProps, childrenNode: prevChildrenNode } = prevTree
if (currentType !== prevType) {
return {
isChanged: true,
currentTarget: [{ changeType: 'replace', element: currentTree.element }],
prevTarget: [{ changeType: 'replace', element: prevTree.element }],
}
}
const { children: currentChildren, ...currentRestProps } = currentProps
const { children: prevChildren, ...prevRestProps } = prevProps
if (currentChildren) {
const isChanged = currentChildren.some((child, index) => {
if (typeof child === 'object') return false
if (child !== prevChildren[index]) {
return true
}
})
if (isChanged) {
currentTarget.push({ changeType: 'innerHTML', element: currentTree.element })
prevTarget.push({ changeType: 'innerHTML', element: prevTree.element })
}
}
const currentPropsKeys = Object.keys(currentRestProps)
const diffProps = currentPropsKeys.filter(key => currentRestProps[key] !== prevRestProps[key])
diffProps
.filter(key => !key.startsWith('on'))
.forEach(key => {
currentTarget.push({
changeType: 'propsChange',
element: currentTree.element,
prop: { key, value: currentRestProps[key] },
})
prevTarget.push({
changeType: 'propsChange',
element: prevTree.element,
prop: { key, value: prevRestProps[key] },
})
})
if (currentChildrenNode) {
currentChildrenNode.forEach((child, index) => {
this.diff(child, prevChildrenNode[index], currentTarget, prevTarget)
})
}
if (currentTarget.length > 0) {
return { isChanged: true, currentTarget, prevTarget }
}
return { isChanged: false }
}
render = tree => {
const rootElement = document.createElement(tree.type)
const virtualDOM = {
type: tree.type,
props: tree.props,
element: rootElement,
childrenNode: [],
}
const { children, onClick, onChange, checked, ...elementProps } = tree.props
if (onClick) rootElement.addEventListener('click', onClick)
if (onChange) rootElement.addEventListener('change', e => onChange(e))
if (checked) rootElement.checked = checked
Object.keys(elementProps).forEach(key => {
rootElement.setAttribute(key, elementProps[key])
})
if (children) {
children.map(child => {
if (typeof child !== 'object') {
rootElement.appendChild(document.createTextNode(child))
} else {
const { rootElement: element, virtualDOM: VDOM } = this.render(child)
rootElement.appendChild(element)
virtualDOM.childrenNode.push(VDOM)
}
})
}
return { rootElement, virtualDOM }
}
rerenderComponent = component => {
this.stateIndex = 0
return this.diff(component(), this.prevVDOM)
}
renderComponent = component => {
if (!this.currentComponent) this.currentComponent = component
this.stateIndex = 0
const { rootElement, virtualDOM } = this.render(component())
if (!this.prevVDOM) this.prevVDOM = virtualDOM
const { isChanged, currentTarget, prevTarget } = this.diff(virtualDOM, this.prevVDOM)
if (isChanged) {
prevTarget.forEach((elementObj, index) => {
if (elementObj.changeType === 'innerHTML') {
elementObj.element.innerHTML = currentTarget[index].element.innerHTML
}
if (elementObj.changeType === 'propsChange') {
elementObj.element.setAttribute(elementObj.prop.key, currentTarget[index].prop.value)
} else {
const parentElement = elementObj.element.parentElement
for (const child of parentElement.children) {
if (child === elementObj.element) {
parentElement.replaceChild(currentTarget[index].element, elementObj.element)
break
}
}
}
})
}
this.prevVDOM = virtualDOM
return rootElement
}
}
const react = new React()
export default react
diff 함수를 구현하여
기존 virtualDOM 과 비교하여 변경점을 찾고
찾은 변경점을 업데이트 renderComponent 내에서
업데이트 하도록 구현 하였으나
diff 함수의 실행이 render 내부로 옮겨져야 할 것 같기도 한데 다음 기회에..
SyntheticEvent(합성 이벤트)는 리액트의 이벤트 객체로 이벤트 핸들러는 이벤트 객체를 받게 된다
DOM 이벤트와 같이 표준을 준수하지만 브라우저별 불일치를 해소하기 위해 만들어졌다
HTML 의 이벤트 전파를 이용하여
이벤트 핸들러를 한 곳에 등록하여 관리하는 것
effectIndex = 0
effectListeners = []
useEffect = (effect, deps) => {
if (!deps) {
effect()
return
}
if (!this.effectListeners[this.effectIndex]) {
this.effectListeners.push({ effect, deps })
effect()
} else {
const { effect: prevEffect, deps: prevDeps } = this.effectListeners[this.effectIndex]
if (prevDeps.some((dep, index) => dep !== deps[index])) {
this.effectListeners[this.effectIndex] = { effect, deps }
prevEffect()
}
}
this.effectIndex++
}
useEffect 는 동기적으로 실행되어 실행 순서를 보장한다
의존성 배열에 값을 주지 않으면 렌더링 시마다 실행,
의존성 배열의 값이 비어있으면 첫 렌더링 시 한 번,
의존성 배열의 값이 있을 때, 첫 렌더링 시와 내부 값 변경 시 한 번 실행된다
리액트의 hooks 를 최상위에 작성해야 하는 이유