[React] Component Lifecycle 원코드 정리

<dev Yoo />·2022년 11월 15일
0

[React]

목록 보기
1/2
  • 해당 게시글은 아래 문헌을 참고하여 TS를 기반으로 최신 버전에 맞게 제작, Class 컴포넌트의 라이프 사이클을 모두 경험하기 위해 작성했다.
    • 2022.11. 현재 React v18.2.0에 맞게 제작
  • 참고 문헌

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;
profile
기획하는 개발자, 디자인하는 엔지니어

0개의 댓글