220214 TIL React.StrictMode

React.StrictMode

오늘 스터디 후반부에 동기님이 만든 어플리케이션 작동과 코드를 보여주면서 왜 두 번 랜더가 되는지 함께 답을 찾는 시간을 가졌었다. 상태 값이나 setTimeout의 문제는 아닌 것 같은데 왜 2초 뒤 콘솔에 동시에 2번 찍히는지 모두들 이해가 되지 않았다.

그러다 예전에 강의에서 진짜 잠깐 React.StrictMode일 때 랜더가 2번 된다는 말을 흘려들었었는데 그게 생각나 동기님에게 index.js에서 React.StrictMode를 지워보라고 요청했다.

그러자 1번 랜더로 바로 바뀌며 문제를 해결했다!
하지만 정확한 원리를 모르기 때문에 블로그에 정리하려고 한다.

React.StrictMode란 무엇인가?

React.StrictMode는 자바스크립트를 쓸 때 use strict와 마찬가지로 코딩하다가 실수하거나 잘못되거나 위험한 상황이 발생하면 바로 에러메세지가 표기되도록 도와준다.

배포할 때는 React.StrictMode가 활성화되지 않기 때문에 사용자의 콘솔에서는 볼 수 없다.

공식문서에서는 아래처럼 설명되어 있다.

StrictMode는 애플리케이션 내의 잠재적인 문제를 알아내기 위한 도구입니다. Fragment
와 같이 UI를 렌더링하지 않으며, 자손들에 대한 부가적인 검사와 경고를 활성화합니다.

보통 CRA 생성 후 index.js에 들어가면 이렇게 아래처럼 컴포넌트에 StrictMode가 감싸져있는데 꼭 index.js 파일이 아니더라도 애플리케이션 내 원하는 컴포넌트에 사용이 가능하다.

공식문서의 예제처럼 아래 같은 경우 HeaderFooter 컴포넌트는 Strict 모드 검사가 이뤄지지 않는다.

<div>
  <Header />
  <React.StrictMode>
    <div>
      <ComponentOne />
      <ComponentTwo />
    </div>
  </React.StrictMode>
  <Footer />
</div>

React.StrictMode의 두 번 랜더링

StrictMode는 5가지 부분에서 도움이 된다고 적혀있는데
여기서 4번째 예상치 못한 부작용 검사가 두 번 랜더링 되는 이유임을 발견했다.

리엑트는 가상의 돔트리로 리엑트 컴포넌트들이 메모리 상에 보관되어지는데 컴포넌트에 변동사항이 생겨서 render 함수가 호출되면 리엑트가 이전 가상 돔트리와 비교해 어떤 부분이 업데이트되어야하는지 계산한 다음 실질적 데이터가 변동되면 DOM트리를 업데이트한다.

위와 같이 리엑트 이해하고 개념적으로 보면 렌더링 그리고 커밋 이 두 단계로 나눌 수 있다.

  • 렌더링 단계는 이전 VDOM트리와 비교해 어떤 부분이 업데이트되야하는지 결정하는 단계다.
  • 커밋 단계는 변경사항을 반영하는 단계다. 즉 DOM트리를 업데이트하는데 이때 리엑트는 componentDidMount나 componentDidUpdate 와 같은 생명주기 메서드를 호출한다.

이렇게 봤을 때 당연히 계산하는 과정이 오래걸리지 않을까?
그래서 리엑트는 커밋 단계가 일반적으로 매우 빠르지만 렌더링 단계는 느릴 수 있다고 한다.

이때 리엑트는 이 오래걸릴 수 있는 렌더링 작업을 더 작은 단위로 나눠 작업을 중지했다가 재개하는데 브라우저가 멈추는 것을 피하기 위함이다.

그래서 커밋하기 전에 렌더링 단계의 생명주기 메서드를 여러 번 호출하거나 아예 커밋을 하지 않을 수도(에러나 우선순위에 따른 작업 중단) 있다.

공식문서에 나온 리엑트에서 렌더링 단계 생명주기 메서드는 클래스형 컴포넌트의 메서드를 포함해 다음과 같다.

  • constructor
  • componentWillMount (or UNSAFE_componentWillMount)
  • componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
  • componentWillUpdate (or UNSAFE_componentWillUpdate)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • setState 업데이트 함수 (첫 번째 인자)

조금 전 랜더링 단계를 작은 단위롤 나누기 때문에 이 메서드들이 여러번 호출될 수 있다고 했는데 이때 부작용을 포함하지 않는 것이 중요하다고 한다.

메모리 누수 혹은 의도하지 않은 애플리케이션의 상태 등 다양한 문제를 일으킬 가능성이 생기기 때문이다.

하지만 모든 버그가 그렇듯 예측할 수 있었다면 버그가 버그가 아니지 않을까?
때문에 리엑트에서는 Strict 모드를 넣어 완벽하진 않지만 문제를 조금 더 예측할 수 있겠끔 도와준다고 한다.

이때 아래의 함수를 의도적으로 이중으로 호출하여 찾을 수 있다는데 이게 바로 두 번 호출된 이유였다!!

의도적 이중 호출

  • 클래스 컴포넌트의 constructorrender 그리고 shouldComponentUpdate 메서드
  • 클래스 컴포넌트의 getDerivedStateFromProps static 메서드
  • 함수 컴포넌트 바디
  • State updater 함수 (setState의 첫 번째 인자)
  • useStateuseMemo 그리고 useReducer에 전달되는 함수

여기서 useState를 사용했기 때문에 두 번 호출된 것이다.

추가로 리엑트 17부터는 리엑트에서 자동으로 console.log()같은 콘솔 메서드를 수정해 생명주기 함수의 두 번째 호출에도 로그를 찍지 않는다고 한다.

끝으로 동기님이 이래서 스터디를 해야 한다고 했는데 정말 동의한다.
스터디를 할 때마다 내가 몰랐던 부분이나 혹은 정확하게 알지 못한 부분을 알 수 있고 무엇보다 함께 성장하는 게 느껴져서 재밌다.
오늘만 해도 다른 동기분들이 발표해주신 리엑트 생명주기와 브라우저랜더 과정을 들으면서 새로 알게 된 지식을 메모하며 다시 자세하게 공부하는 중이다. 매일 매일 이렇게 성장해나가는 게 뿌듯하다. 😆

좋은 웹페이지 즐겨찾기