1. 리액트 딥다이브를 읽고 나서
리액트 딥다이브를 읽고 나서 기억하면 좋을 것 같은 부분을 정리했다. 아직 작성중...
1. 리액트 개발을 위해 꼭 알아야할 자바스크립트
shallowEqual
리액트에서 동등 비교는 ==
나 ===
가 아닌 Object.is
이다. 리액트에서는 이 objectIs를 기반으로 동등 비교를 하는 shallowEqual
이라는 함수를 만들어 사용한다. 이 shallowEqual
는 useEffect
와 같은 리액트 메서드의 다양한 곳에서 사용된다.
// JS의 objectIs를 폴리필을 추가한 코드
import is from './objectIs'
import hasOwnProperty from './hasOwnProperty'
function shallowEqual(objA, objB) {
if (is(objA, objB)) {
return true;
}
// 예외 처리
if (
typeof objA !== object ||
objA === null ||
typeof objB !== object ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) {
return false;
}
// 순회하면서 A의 키를 기준으로, B에 같은 키가 있는지, 그리고 그 값이 같은지 확인한다.
for (let i=0; i<keysA.length; i++) {
const curKey = keysA[i]
if (
!hasOwnProperty.call(objB, curKey) ||
!is(objA[curKey], objB[curKey])
) {
return false
}
}
return true
}
export default shallowEqual
위 코드에서 확인할 수 있듯이 Object.is
는 참조가 다른 객체에 대해 비교가 불가능하지만, 리액트팀에서 구현한 shallowEqual
은 객체의 1 depth까지는 비교가 가능하다.
2 depth까지 가면 이를 비교할 방법이 없으므로 false를 반환한다.
React.memo
를 사용할 때나useEffect
의 의존성 배열을 작성할 때 주의할 것.
이벤트 루프
이벤트루프란 자바스크립트 런타임 외부에서 자바스크립트의 비동기 실행을 돕기 위해 만들어진 장치라 볼 수 있다. V8, Spider Monkey 같은 현대의 자바스크립트 런타임 엔진에는 자바스크립트 코드를 효과적으로 실행하기 위한 여러가지 장치들이 마련돼 있다.
이벤트 루프는 태스크 큐와 더블어 마이크로 태스크 큐를 가지고 있는데 이는 태스크 큐와 다른 태스크들을 처리한다. 마이크로 태스크 큐에서 실행하는 대표적인 태스크는 Promise
이다. 이 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다. 즉, setTimeout
과 setInterval
은 Promise
보다 늦게 실행된다.
function foo() {
console.log("foo")
}
function bar() {
console.log("bar")
}
function baz() {
console.log("baz")
}
setTimeout(foo, 0)
Promise.resolve().then(bar).then(baz)
위 예제 코드를 실행하면 bar, baz, foo 순으로 실행된다.
- 태스크 큐:
setTimeout
,setInterval
,setImmediate
- 마이크로 태스크 큐:
process.nextTick
,Promises
,queueMicroTask
,MutationObserver
2. 리액트 핵심 요소 깊게 살펴보기
가상 DOM
리액트의 특징 중 가장 많이 언급되는 것 중 하나가 가상 DOM을 운영한다는 것이다.
우선 DOM(Document Object Model)이란 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.
DOM을 직접 제어하는 방식은 HTML을 파싱하고, DOM 트리를 만들고 ... CSS 파일을 다운로드하고 레이아웃, 페인팅 과정 등 매우 복잡하고 많은 비용이 든다. 대다수의 앱은 랜더링 이후 정보를 보여주는데 그치지 않고 사용자의 인터랙션을 통해 다양한 정보를 노출하기 때문에 DOM을 직접 제어하는 것은 브라우저와 사용자에게 매우 큰 비용이다. 또한, UI의 상태를 관리함에 있어서 DOM을 직접 조작하는 방식은 UI상태와 DOM 업데이트 로직을 개발자가 명시적으로 관리해야했기 때문에 코드는 점점 복잡해지고, 유지보수도 어려워졌다.
이러한 문제점을 해결하기 위해 탄생한 것이 바로 가상 DOM이다. 가상 DOM은 말 그대로 실제 브라우저의 DOM이 아닌 리액트가 관리하는 가상 DOM을 의미한다. 가상 DOM은 웹 페이지가 표시해야할 DOM을 메모리에 저장하고 리액트가 실제 변경에 대한 준비가 완료 됐을 때 실제 브라우저의 DOM에 반영한다.
가상 DOM을 위한 아키텍처, 리액트 파이버
리액트 파이버는 리액트에서 관리하는 평범한 자바스크립트 객체이다. 파이버는 재조정자(reconciler
)가 관리하는데, 이는 앞서 이야기한 가상 DOM과 실제 DOM을 비교해 변경사항을 수집하며, 만약 이 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 기능을 한다.
리액트 파이버의 목표는 리액트 웹 어플리케이션에서 발생하는 애니메이션, 레이아웃, 그리고 사용자 인터렉션에 올바른 결과물을 만드는 반응성 문제를 해결하는 것이다. 다음과 같은 목표를 가지고 있다.
- 작업을 작은 단위로 분할하고 쪼갠 다음, 우선순의를 매긴다.
- 이러한 작업을 일시 중지하고 나중에 다시 시작할 수 있다.
- 이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우에는 폐기할 수 있다.
이 모든 과정은 비동기로 이루어진다. 파이버는 하나의 작업 단위로 구성돼 있고, 이러한 작업 단위를 하나씩 처리한 뒤 finishedWork()
라는 작업으로 마무리한다. 그리고 이 작업을 커밋해 실제 브라우저 DOM에 가시적인 변경 사항을 만들어낸다.
- 렌더 단계에서 리액트는 시용자에게 노출되지 않는 모든 비동기 작업을 수행한다. 그리고 이 단계에서 앞서 언급한 파이버의 작업, 우선순위를 지정하거나 중지시키거나 버리는 등의 작업이 일어난다
- 커멋 단계에서는 앞서 언급한 것처럼 DOM에 실제 변경 사항을 반영하기 위한 작업,
commitWork()
가 실행되는데, 이 과정은 앞서와 다르게 동기식으로 일어나고 중단될 수도 없다.
클래스형 컴포넌트의 생명주기 메서드
getDerivedStateFromError
자식 컴포넌트에서 에러가 발생했을 때 호출되는 메서드이다.
import React, { PropsWithChildren } from 'react'
type Props = PropsWithChildrem<{}>
type State = { hasError: boolean; errorMessage: string }
export default class ErrorBoundary extends React.Component<Props, State> {
// ... 생략
static getDerivedStateFromError(error: Error) {
return {
hasError: true,
errorMessage: error.toString()
}
}
render() {
// 에러가 발생했을 때
if (this.state.hasError) {
return (
<div>
<h1>에러가 발생했습니다.</h1>
{this.state.errorMessage}
</div>
)
}
// 일반적인 상황
return this.props.children
}
}
// App.tsx
function App() {
return (
<ErrorBoundary>
<Child />
</ErrorBoundary>
)
}
function Child() {
const [error, setError] = useState(false)
const handleClick = () => setError((prev) => !prev)
if (error) {
throw new Error("Error has been occured")
}
return <button onClick={handleClick}>에러 발생</button>
}
getDerivedStateFromError
는 static 메서드로, error를 인수로 받는다. getDerivedStateFromError
는 하위 컴포넌트에서 에러가 발생했을 경우 어떻게 자식 리액트 컴포넌트를 렌더링할 지 결정하는 용도로 제공되는 메서드이기 때문에 반드시 미리 정의해 둔 state값을 반환해야한다.
componentDidCatch
componentDidCatch
에서는 앞서 getDerivedStateFromError()
에서 하지 못했던 부수 효과를 수행할 수 있다. 이는 render 단계에서 실행되는 getDerivedStateFromError
와 다르게 componentDidCatch
는 커밋 단계에서 실행되기 때문이다.
getDerivedStateFromError
: 에러가 발생한 즉시 상태를 변경하여 렌더링에 반영하여 깨진 UI를 사용자에게 적절히 표시할 수 있다.componentDidCatch
: 컴포넌트가 에러를 잡은 후 호출한다. 에러 로깅 및 사이드 이펙트를 처리할 때 유용하다.
리액트의 렌더링
리액트에서 렌더링이 발생하는 시나리오는 다음과 같다.
- 최초 렌더링
- 리렌더링
- 함수형 컴포넌트의 useState 요소인 setter가 실행되는 경우, useReducer의 dispatch가 실행될 때
- 컴포넌트의 key props가 변경되는 경우 (리액트에서 key는 형제 요소들 사이에서 동일한 요소를 식별하는 값이다.)
리액트에서 key가 필요한 이유?
리액트에서 key는 리렌더링이 발생하는 동안 형제 요소들 사이에서 동일한 요소를 식별하는 값이다. 앞서 리액트 파이버 트리 구조를 떠올려보면 해당 트리 구조에서 형제 컴포넌트 컴포넌트를 구별하기 위해 각자
sibling
이라는 속성값을 사용했다. 리렌더링이 발생하면 current 트리와 workInProgress 트리 사이에서 어떠한 컴포넌트가 변경이 있었는지 구별해야 하는데, 이 두 트리 사이에서 같은 컴포넌트인지 구별하는 값이 바로 key이다.