LifeCycle.tsx
import React from "react";
interface IProps {
color: string;
}
interface IState {
number: number;
color: string | null;
}
// 컴포넌트 생명 주기 제대로 이해하기
// 마운트 | 업데이트 | 언마운트 | 에러 처리
// 전체 과정: Render 단계 (render까지)
// -> Pre-commit 단계 (getSnapshotBeforeUpdate)
// -> Commit 단계
// 마운트
// constructor -> static getDerivedStateFromProps
// -> render -> componentDidMount
// 업데이트
// 조건 발동 -> static getDerivedStateFromProps
// -> shouldComponentUpdate(F 리턴 시 중단) -> render
// -> getSnapshotBeforeUpdate -> 실제 DOM 변화
// -> componentDidUpdate
// 업데이트 조건
// 1. props 변경
// 2. state 변경
// 3. 부모 컴포넌트 리렌더링
// 4. this.forceUpdate로 강제 렌더링 트리거 - 비권장
// 언마운트: componentWillUnmount
class LifeCycle extends React.Component<IProps, IState> {
// 초기 state 설정 및 타입 설정
state: Readonly<IState> = {
number: 0,
color: null,
};
myRef: HTMLHeadingElement | null = null;
// constructor: 마운트 1
// JS 문법 생성자, super 및 초기 state 설정
constructor(props: IProps) {
super(props);
console.log("constructor");
}
// getDerivedStateFromProps: 마운트 2, 업데이트 1
// 새로운 prop을 기반으로 state 업데이트
// 업데이트 된 state는 리턴 (불변성 유지), 업데이트 안 할거면 null 리턴
static getDerivedStateFromProps(
nextProps: Readonly<IProps>,
prevState: Readonly<IState>
) {
console.log("getDerivedStateFromProps");
if (nextProps.color !== prevState.color) return { color: nextProps.color };
return null;
}
// componentDidMount: 마운트 -1
// 마운트시 DOM이 업데이트 되는 부분, state 변경 및 DOM 정보 사용 가능
// 여기에서 외부 함수, 이벤트 등록, 비동기 작업(api, timeout, interval 등) 처리
componentDidMount(): void {
console.log("componentDidMount");
// ErrorBoundary 오류 처리 확인을 위한 코드
// throw Error("hey");
}
// shouldComponentUpdate: 업데이트 2, forceUpdate시 X
// 최적화 목적 -> 그러나 버그 가능성 때문에 정말 확실한 경우에만 사용
// 되도록이면 얕은 비교를 진행하는 PureComponent 사용 (일반 Component는 setState시 비교 없이 렌더링)
// JSON.stringify 및 깊은 동일성 검사 비권장
// 이후 버전에서는 false를 반환해도 힌트로서만 작용할 예정
// 자식 컴포넌트의 리렌더링에는 영향을 주지 않음
shouldComponentUpdate(
nextProps: Readonly<IProps>,
nextState: Readonly<IState>,
nextContext: any
): boolean {
console.log("shouldComponentUpdate", nextProps, nextState);
return nextState.number % 10 !== 4;
}
// componentWillUnmount: 언마운트
// 언마운트시 호출, 타이머 제거, api 요청 취소, 구독 해제 등의 정리 작업 수행
// setState 사용 불가
componentWillUnmount(): void {
console.log("componentWillUnmount");
}
handlePlusClick = () => {
this.setState({ number: this.state.number + 1 });
};
// getSnapshotBeforeUpdate: 업데이트 -2
// DOM 반영 직전에 호출, 리턴 된 snapshot은 componentDidUpdate에 전달 됨
// 보통 스크롤 위치 관련 작업에 사용, 업데이트 직전의 값을 참고할 일이 있을 때
getSnapshotBeforeUpdate(
prevProps: Readonly<IProps>,
prevState: Readonly<IState>
) {
console.log("getSnapshotBeforeUpdate");
if (this.myRef && prevProps.color !== this.props.color)
return this.myRef.style.color;
return null;
}
// componentDidUpdate: 업데이트 -1
// state 변경 및 DOM 정보 사용 가능
// 여기에서 외부 함수, 이벤트 등록, 비동기 작업(api, timeout, interval 등) 처리
// setState 사용 시 조건문을 이용해서 무한 반복 방지 중요!!
// props를 state에 복사하는 것은 권장되지 않음!!
componentDidUpdate(
prevProps: Readonly<IProps>,
prevState: Readonly<IState>,
snapshot?: string
): void {
console.log("componentDidUpdate", prevProps, prevState);
if (snapshot) console.log("업데이트 전 색상: ", snapshot);
}
// render: 마운트 -2, 업데이트 -3
// 유일한 필수 메소드, 컴포넌트 렌더링
// setState 불가, DOM 접근 불가
render(): React.ReactNode {
console.log("render");
const style = { color: this.props.color };
return (
<div>
<h1>LifeCycle</h1>
<h2 style={style} ref={(ref) => (this.myRef = ref)}>
{this.state.number}
</h2>
<p>Color: {this.state.color}</p>
<button onClick={this.handlePlusClick}>Plus</button>
</div>
);
}
}
export default LifeCycle;
ErrorBoundary.tsx
import React from "react";
interface IProps {
children: React.ReactNode;
}
interface IState {
hasError: boolean;
}
// 에러 처리: static getDerivedStateFromError -> componentDidCatch
// 해당 에러 처리 메소드는 모두 '렌더링 과정 중의 오류'를 처리함!!
// 또한 props.children으로 전달 된 컴포넌트의 오류만 처리 가능, 자기 자신의 오류는 catch하지 못함
class ErrorBoundary extends React.Component<IProps, IState> {
state: IState = {
hasError: false,
};
// static getDerivedStateFromError - Render 단계
// Render 단계이므로 부수 효과 불가능
// 갱신된 state를 리턴 - 반드시 리턴!!
static getDerivedStateFromError(error: Error): IState {
console.log("check");
return { hasError: true };
}
// componentDidCatch - Commit 단계
// 렌더 이후에 처리되므로(Commit), 부수 효과 발생 가능
// 주로 오류 로그 기록에 사용
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// 에러로 인한 state 변화는 getDerivedStateFromError에서 보통 구현
// this.setState({ hasError: true });
console.log(error, errorInfo);
}
render() {
return this.state.hasError ? (
<div>에러가 발생했습니다.</div>
) : (
<>{this.props.children}</>
);
}
}
export default ErrorBoundary;
getRandomColor.ts
function getRandomColor() {
return "#" + Math.floor(Math.random() * 16777215).toString(16);
}
export default getRandomColor;
App.tsx
// ...imports
function App() {
const [color, setColor] = useState("#000000");
const handleClick = () => setColor(getRandomColor());
return (
<>
<ErrorBoundary>
<LifeCycle color={color} />
<button onClick={handleClick}>Color Change</button>
</ErrorBoundary>
</>
);
}
export default App;