이 시리즈의 이전 글은 아래로...
feat/#2_CoreComponent
브랜치에서 자세히 확인 하실 수 있습니다! 😊
컴포넌트 구현 시, 중복 코드를 줄이고 컴포넌트들이 모두 동일한 라이프 사이클을 가지게 하여 유지보수를 편리하게 하기 위한 컴포넌트들의 기본 구조
preventRenderStateKey
에 있는 state key가 변경이 되는 경우, 현재 컴포넌트는 상태만 변경이 되고 렌더링은 진행되지 않습니다.node
: 컴포넌트의 nodeinitalState
: 컴포넌트 상태의 초기값preventRenderStateKey
: 구독중인 컴포넌트의 상태 변경 시, 내부적으로 상태만 업데이트 후 자식 컴포넌트만 렌더링하기 위한 keyneedRender
: 컴포넌트의 상태 변경에 따른 렌더링 여부needUpdate
: 컴포넌트의 상태 변경에 따른 setState 여부subscribers
: 컴포넌트 상태 변경 시, 상태가 같이 변경될 하위 컴포넌트template()
: 컴포넌트의 markup을 반환하는 메서드init()
: 렌더링 전, 내부적으로 사용될 변수, 함수 정의 또는 초기 데이터를 받아올 때 사용되는 라이프사이클 메서드fetch()
: 초기 렌더링 이후 컴포넌트의 fetching이 필요할 때 실행되는 라이프 사이클 메서드render()
: 빈 태그를 컴포넌트의 markup으로 변환, 이벤트를 바인딩, 하위 컴포넌트를 부착을 하는 라이프 사이클 메서드update()
: 상태 변경 시, 렌더링을 위한 라이프사이클 메서드updateChildren()
: 상태 변경 시, 하위 컴포넌트의 렌더링을 위한 라이프 사이클 메서드attachChildComponent()
: 하위 컴포넌트를 상위 컴포넌트의 template과 연결하는 라이프 사이클 메서드subscribe()
: 상위 컴포넌트에 구독을 하는 메서드validationState
: 컴포넌트의 상태 변경 시, 현재 컴포넌트가 가지고 있는 상태인지 판별하는 메서드setState()
: 컴포넌트의 상태 변경 시, 컴포넌트의 상태를 업데이트, 하위 컴포넌트들에게 알리는 메서드notify()
: 상위 컴포넌트로부터 받은 새로운 상태로 하위 컴포넌트들의 setState(), render()하게 해주는 메소드setEvent()
: 컴포넌트의 node에 이벤트를 바인딩하는 라이프 사이클 메서드clearEvent()
: 컴포넌트의 node에 바인딩되어 있는 이벤트를 지우는 라이프 사이클 메서드// Component.ts
...
constructor({
node,
initalState,
preventRenderStateKey = []
}: IComponentParams<StateType>) {
this.node = node
this.state = initalState as StateType
this.preventRenderStateKey = new Set(preventRenderStateKey)
this.needRender = false
this.needUpdate = false
this.subscribers = new Set([])
this.init()
this.render()
this.fetch()
}
Component
를 extends해서 컴포넌트를 생성하는 경우,
Component
의 constructor에 의해 init -> render -> fetch
가 자동으로 실행됩니다.
// Component.ts
...
render(): void {
convertTemplateAsComponent.call(this)
this.setEvent()
this.attachChildComponent()
}
render()
라이프 사이클 메서드는 내부에 세 가지 단계를 거치게 됩니다.
convertTemplateAsComponent()
함수의 this를 확장된 현재 컴포넌트로 명시적 바인딩을 하여 호출합니다.setEvent()
라이프 사이클 메서드를 실행합니다.attachChildComponent()
라이프 사이클 메서드를 실행합니다.// utils/dom.ts
...
function convertTemplateAsComponent(this: any): void {
const oldNode = this.node
const componentChildren = Array.from(
new DOMParser().parseFromString(this.template(), 'text/html').body.children
)
const component = new DocumentFragment()
component.append(...componentChildren)
oldNode.after(component)
this.node = oldNode.nextSibling
// CSS 상속
const oldCSS = oldNode.classList.value.trim()
const newCSS = this.node.classList.value.trim()
const isChangedCSS = oldCSS !== newCSS
const cssValue = isChangedCSS ? newCSS || oldCSS : oldCSS
this.node.className = cssValue
oldNode.remove()
}
렌더링에서 가장 중요한 부분을 담당하는 dom 유틸 함수입니다!
현재 컴포넌트의 template을 html로 변경을 해주는 역할을 담당합니다.
Element
를 oldNode
에 임시 저장합니다.template
을 Element
로 변경한 뒤, oldNode
의 뒤에 추가합니다.Element
를 this.node
로 변경합니다.Element
의 class를 상속합니다.oldNode
를 지워주는 것으로 렌더링 끝!// ExampleText.ts
...
template(): string {
return `
<main id="App">
<ExampleText></ExampleText>
<Button>Change State!</Button>
</main>
`
}
attachChildComponent(): void {
const { text, onClick } = this.state
const exampleText = new ExampleText({
node: selectEl(this.node, 'ExampleText'),
initalState: {
text
}
})
new Button({
node: selectEl(this.node, 'Button'),
initalState: {
onClick
}
})
this.subscribe(exampleText)
}
template()
에 컴포넌트로 치환되어야할 부분을 명시합니다.
(creatElement로 해도 되지만 저는.. 리액트를 따라하고 싶었슴니다,,)
attachChildComponent()
내부에서 컴포넌트를 연결합니다.
컴포넌트 연결 방식은 총 2가지 방식입니다 (구독O / 구독X)
subscribe()
메서드에 해당하는 컴포넌트를 넣어줍니다.// Component.ts
...
setState(newState: Partial<StateType>): void {
const validState = this.validationState(newState)
if (!this.needUpdate) {
return
}
const currentState = { ...this.state } as StateType
const preventRenderStateKey = Array.from(this.preventRenderStateKey)
validState?.forEach(key => {
const stateKey = key as keyof StateType
if (!preventRenderStateKey.includes(key)) {
this.needRender = true
}
currentState[stateKey] = newState[stateKey] as StateType[keyof StateType]
})
this.state = currentState
this.notify(newState)
}
stateKey
가 있는지 검사한 뒤, 없으면 바로 종료시킵니다.stateKey
를 순회하며 상태를 업데이트하며 렌더링 필요 여부를 체크합니다.Component.subscribes
)가 있는 경우, notify()
로 새로운 상태를 전달합니다.// Component.ts
...
notify(newState: Partial<StateType>): void {
const subscribers = Array.from(this.subscribers)
const validSubscribers = subscribers.filter(
subscriber => subscriber.validationState(newState).length
)
validSubscribers?.forEach(subscriber => {
subscriber.setState(newState)
if (subscriber.needRender) {
subscriber.update()
return
}
subscriber.updateChildren()
})
}
stateKey
가 있는지 확인합니다.setState()
를 호출합니다.// Component.ts
...
update(): void {
this.needRender = false
this.clearEvent()
this.render()
}
render()
를 다시 진행합니다.// Component.ts
...
updateChildren(): void {
this.needRender = false
this.attachChildComponent()
}
시리즈가 일찍 끝날 줄 알았는데.. 생각보다 오래 걸린다.. 고치고 싶은 부분도 많고...
다음 편은 코어 컴포넌트 활용편으로...