2번째 후기 리액트 따라하려다 바짓가랑이 찢어진 이야기의 연계 후기로 3번째 가계부 프로젝트의 해당 Wooact를 어떻게 손봐서 조금 더 쓸 만한 수준으로 만들었는지 적어보려고 합니다. 지난 글을 요약해보자면, JSX 비스무리하게 만들기, Props를 이용한 컴포넌트화, state 변경에 따른 re-render가 wooact를 만들게 된 주요 원인이었습니다. 그리고 결론은 나는 페이스북이 아니니 만든 것을 최대한 이용하는 선에서 적정하게 타협해서 이용했다로 끝맺음을 했습니다. 사실 진짜 끝맺음은 리액트의 핵심 알고리즘인 diffing 알고리즘을 쥐꼬리만큼이라도 공부/적용 해서 조금 더 쓸만한 수준으로 만들겠다는 목표가 들어있었습니다.

사실이라고 쓰고 변명이라고 읽는다.

실제로 해당 개념을 공부하고, 그 개념들을 조금씩 차용하여 기존보다는 더 쓸 만한 형태로 wooact의 작동 방식을 더 개선했습니다. 불필요한 rerender를 줄였고, 이전에 갖고 있던 많은 문제점들을 개선했습니다. redux와 비슷한 구조로 만든Store와의 연계를 통해 확장성도 높여 개인적으로는 쬐금 만족할 만한 수준으로 끌어 올렸다고 생각합니다. 물론 아직도 개선 가능한 부분이 훨씬 많습니다. 아니 사실 시작점 부터가 리액트와는 달랐기에, 구현이 불가한 부분 또한 많습니다.

따라서 어떻게 구현했는지는 개인의 만족이지, 공유된 내용을 보는 입장에서 큰 도움이 되지 않으리라고 판단했습니다. 그래서 다음주 부터 사용할 리액트가 어떻게 작동하는지 공유해드려고 합니다. 이런 상태로 크롱님께 어떻게 쓰는지를 배우시면 더 재밌게 리액트와 더 친해질 수 있지 않을까요? 따라서 해당 글은 리액트의 가벼운 사용 방법 및 작동 원리를 다루는 글이며, 고급 기술은 크롱님을 찾아주시면 감사하겠습니다.

VirtualDOM

일단 리액트는 기본적으로 실제 DOM을 이용하지 않습니다. 자신들이 만든 VirtualDOM 혹은 React Element라고 불리우는 객체 형태를 이용합니다. 이 VirtualDOM이 ReactDOM.render( /* React Elements */, $target)에서 실제 브라우저에 DOM 형태로 렌더링 됩니다.

type Props = {
	content: string
	price: number
	categoryId: number
	paymentId: number
	...
}
// React의 class based component
class TransactionItem extends React.Component<Props> {
	constructor(props: Props){
		super(props)
		...
	}

	render(){
		// <> 은 React.Fragment와 동일
		return <> 
			<p>{this.props.content}</p>		
			...
		</>
	}
}
// React의 functional component
const TransactionItem: React.FC<Props> = (props: Props) => {
	...
	return <> 
			<p>{this.props.content}</p>		
			...
		</>
}

간단한 예시로 이번 프로젝트의 거래내역 페이지의 하나의 거래를 TransactionItem이라는 리액트 컴포넌트로 작성해 보았습니다.(타입스크립트로 작성되었었습니다. 혹 익숙치 않으시다면 : 이후에 것이 타입이구나 정도만 알고 지나가시면 되겠습니다). 클래스형 컴포넌트에서는 render 함수 내에서 리턴하는 JSX, 또한 함수형 컴포넌트에서 리턴하는 JSX는 모두 아래와 같은 형태의 virtual Dom을 리턴합니다.

{
      $$typeof: Symbol(react.element),
      type: 'p',
      key: "XXXX", // unique value
			ref: "YYYY",
      props: {
          children: [
						{
							$$typeof:Symbol(react.element),
							type: '' // tag name
							...
						}
					],
          onClick: () => { ... }
      }
  },

하나의 JSX 태그는 위와 같은 javscript 객체(object) 형태로 구성되어 있습니다. 해당 객체가 React의 virtualDOM임을 확인해주는 Symbol값과, 각각의 virtualDom을 고유하게 구분하는 key값이 기본적으로 들어가게 됩니다. 그 외에는 jsx 태그 attribute로 혹은 그 자식으로 작성한 코드가 저 형태로 변환되어 전달되는 것이죠. 리액트는 이 virtualDOM 과 브라우저에 렌더된 DOM tree와 비교(diffing)을 통해 돔 조작을 효율적으로 해내고 있습니다.

Diffing Algorithm

이제부터가 사실 진짜 이야기인데요. 지난번에도 간략히 설명드렸지만, 세상만사 자기 일도 기억하기 힘든 세상이니 지난번 보다는 조금 더 상세히 설명을 드리자면, 해당 컴포넌트 내에 스테이트가 변경된 경우에는, 리액트는 해당 컴포넌트를 dirty 하다고 표시하고 batch에 추가합니다. 물론 앞으로 쓰게 될 redux와 같은 store를 이용하게 되면, 컴포넌트 단위가 아닌 root 노드(App component)에 dirty 마크가 찍히게 됩니다. 이건 나중 얘기니 일단 pass.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1dcc814a-311a-4076-9e49-010aded32f6e/Untitled.png

https://calendar.perfplanet.com/2013/diff/

그리고 Virtual Dom 엘리먼트와 실제 브라우저에 등록되어있는 DOM 엘리먼트를 비교/순회하며 dirty 체크된 엘리먼트들을 처리합니다. 이것을 처리하는 과정에서 속성 값만 변한 경우에는 속성 값만 업데이트하고, 해당 엘리먼트의 태그 혹은 컴포넌트가 변경된 경우라면 해당 노드를 포함한 하위의 모든 노드를 언마운트(제거)한 뒤에 새로운 virtualDom으로 대체합니다. 이러한 변경 혹은 업데이트가 모두 마무리 된 후에(batch에 쌓인 모든 것들을 처리한 후에) 한 번 실제 돔에 이 결과를 업데이트 합니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/30498109-6ee9-4797-8b6a-18fb02521707/Untitled.png

https://calendar.perfplanet.com/2013/diff/