리액트를 처음 시작했을 때는 함수형 컴포넌트에 대해서만 배웠었고,
회사에 다니기 시작하면서는 리액트를 거의 사용하지 않고 Vue 작업을 위주로 했었기 때문에
마음 한 편에는 항상 클래스형 컴포넌트가 궁금하다는 마음이 자리잡고 있었다
클래스형 컴포넌트에서는 생명주기가 중요한 요소로 꼽히는데
처음 리액트를 배울 땐 잘 몰랐었고, vue 작업을 할 때 vue의 생명주기 매서드를 살펴보면서 이런 것도 있구나~ 하고 알게 되었던 것 같다
근데 리액트에서도 클래스형 컴포넌트는 생명주기가 중요하군...🤔
생명주기 메서드가 실행되는 시점은 mount, update, unmount 세가지가 있으며
생명주기 메서드로는 render, componentDidMount, componentDidUpdate, componentWillUnmount, shouldComponentUpdate
등이 있다
🍄 render
리액트 클래스형 컴포넌트의 유일한 필수 값
컴포넌트가 UI를 렌더하기 위해 쓰임 (마운트, 업데이트 과정에서 일어남)
render 함수는 항상 순수해야함 (같은 입력값 state, props가 들어가면 같은 값 반환해야 함) 그러므로 내부에서 setState 를 호출해서는 안 된다
🍄 componentDidMount
클래스형 컴포넌트가 마운트가 되고 준비되면 즉시 호출되는 메서드
API 호출 후 업데이트, DOM에 의존적인 작업(ex : 이벤트 리스너 추가 등)의 작업시 componentDidMount 내에서 할 수 있으나 성능 문제가 있어 여기서 밖에 할 수 없는 작업인지 확인 후 사용해야 함
🍄 componentDidUpdate
컴포넌트 업데이트 후 즉시 실행
적절한 조건문 사용이 필요 그렇지 않으면 setState가 영원히 호출될 수 있음
🍄 componentWillUnmount
컴포넌트 언마운트되거나 사용되지 않기 직전에 호출
메모리 누수, 불필요 작동을 막기 위함으로 내부에서 setState 호출 불가
이벤트를 remove 하거나, API 호출 취소하거나, setInterval, setTimeout 등을 지우는 때 유용
🍄 sholudComponentUpdate
state, props 변경으로 리렌더링되는 것을 막을 수 있다
다만 리렌더링을 막는 것은 특정 성능 최적화를 위해서만 고려해야 함
클래스형 컴포넌트를 작성할 때 extends React.Component
vs. React.PureComponent
두 가지 경우가 있는데, 둘의 차이점이 state,props의 변경으로 리액트 컴포넌트가 다시 리렌더링하는 것을 컨트롤하는 shouldComponentUpdate 매서드에 있다
PureComponent는 state 값에 대한 얕은 비교를 수행하는데, Component는 그렇지 않다
PureComponent는 객체나 복잡한 구조의 데이터 변경을 제대로 감지하지 못하기 대문에 이럴 땐 Component를 사용하는 것이 좋고,
자주 변경 되는 컴포넌트들의 경우 얕은 비교를 했을 때 일치하지 않는 경우가 더 많으므로 이럴 때는 PureComponent를 사용하는 것이 리소스의 낭비가 될 수도 있다
🍄 static getDerivedStateFromProps
render 호출 직전에 호출됨
static으로 선언되어 this에 접근이 불가
여기서 반환하는 객체가 state로 들어가게 되며, null을 반환하면 아무일도 일어나지 않음
🍄 getSnapShotBeforeUpdate
DOM이 업데이트 되기 직전에 호출되며, 여기서 반환되는 값이 componentDidUpdate의 세번째 인자로 전달된다
DOM에 렌더링 되기 전 윈도우 크기 조절 or 스크롤 위치 조절 등 작업을 할 때 사용할 수 있음
아래 예시에서는 props로 받은 배열 길이가 이전 배열 길이보다 길어지는 경우 현재 스크롤 높이를 반환하고, componentDidUpdate 내에서 snapshot이 넘어온 경우에 스크롤 위치를 재조정해 기존 아이템이 스크롤에서 밀리지 않도록 해준다
getSnapShotBeforeUpdate(prevProps, prevState){
if(prevProps.list.length < this.props.list.length){
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null
}
componentDidUpdate(prevProps, prevState, snapshot){
if(snapshot !== null){
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot
}
}
함수형 컴포넌트에서는 getSnapshotBeforeUpdate, getDerivedStateFromError, componentDidCatch 세 가지 생명주기 메서드가 훅으로 구현되어 있지 않기 때문에 이 메서드들이 필요한 경우 클래스형 컴포넌트를 사용할 수밖에 없다
(리액트 쪽에서 해당 기능들을 가진 훅을 업데이트 할 예정이라고 하긴 하는데 아직 자세한 일정은 모름)
🍄 getDerivedStateFromError
자식 컴포넌트에서 에러가 발생한 경우 호출되는 에러 메서드
//ErrorBoundary 설정 시 사용 가능
//자식 컴포넌트에서 에러가 발생했을 때 어떻게 자식 컴포넌트를 렌더링 할 것인가
//해당 메서드는 반드시 미리 정의해둔 state 값을 반환해야 하며
//렌더링 과정에서 호출되므로 부수 효과를 발생시켜서는 안 됨
//(*여기서 부수효과 : state 반환 외의 모든 작업)
type Props = PropsWithChildren<{}>
type State = { hasError: boolean; errorMessage: string}
export default class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props){
super(props)
this.state = {
hasError: false,
errorMessage: '',
}
}
static getDerivedStateFromError(error:Error){
return {
hasError: true,
errorMessage: error.toString(),
}
}
render(){
if(this.state.hasError){
return (
<div>
<h1> 에러가 발생했습니다. </h1>
<p>{this.state.errorMessage}</p>
</div>
)
}
return this.props.children
}
}
//App.tsx
funciton App(){
return (
<ErrorBoundary>
<Child/>
</ErrorBoundary>
)
}
🍄 componentDidCatch
이 매서드는 자식 컴포넌트 에러가 발생 시 실해오디며, 위 메서드에서 에러를 잡고, state를 결정한 뒤에 실행된다
error, info 두 개의 인수를 받는데 뒤쪽의 info는 어느 컴포넌트가 에러를 발생시켰는지에 대한 정보이다
//위 예제에서 해당 매서드를 추가한다면
componentDidCatch(error:Error, info:ErrorInfo){
console.log(error);
console.log(info);
}
이 메서드에서는 위 메서드에서 하지 못했던 부수효과 (ex: 예시에서는 콘솔 찍어보는거) 를 수행할 수 있음
최근에 Concurrent pattern 관련한 아티클에서 ErrorBoundary 컴포넌트라는 개념을 처음 알게 되었는데, 클래스형 컴포넌트를 이용해서 ErrorBoundary 컴포넌트를 만드는 과정을 따라가보고, 사용되는 생명주기 메서드를 알아볼 수 있어 유용했다...