본문 바로가기
개발/리액트

[리액트] 두번 렌더링 되는 이슈에 대하여(Feat.StrictMode)

by 핸디(Handy) 2021. 2. 2.

들어가며

요새 교학성장이라는 말을 실천해보기로 했습니다.

교학상장(敎學相長)
[가르칠 교, 배울 학, 서로 상, 길 장]

‘가르치고 배우면서 서로 성장한다’는 뜻. 스승은 제자를 가르치는 과정에서 아직도 막히는 부분이 있음을 느껴 더욱 정진케 되고, 제자는 배울수록 자신의 부족함을 알게 돼 학업에 힘쓰니 가르침과 배움이 서로를 성장케 한다는 뜻이다.

속담에 ‘세살먹은 아이한테도 배운다’고 했다. 배움의 길에 모든걸 가르쳐주는 절대스승은 없다. 가르치고 배우며 부족함을 채울 뿐이다. <출전: 예기(禮記)>

하지만 가르칠 곳이 마땅치 않는 저에겐 그나마 오픈채팅방에서 답변해주는 게 그나마 지식을 나누는 방식이라 생각해서 알만한 질문이 나오면 최대한 답변을 하려고 노력하고 있습니다.

이번 글 또한 비슷한 맥락에서 작성하는 글이기도 합니다.

오픈채팅방에서 있다 보면 가끔씩 아래와 비슷한 질문들이 나옵니다.

  • api 요청이 2번 가요.
  • 랜더링이 2번 되는데 이유를 모르겠어요

그래서 이번 글에서는 제 사례와 함께 왜 2번씩 랜더링, 요청이 되는지에 대해 알아보도록 하겠습니다.

 

차트가 두번씩 그려진다?

일반적으로 컴포넌트가 여러 번 변경되거나 자주 바뀌는 경우에도 React가 V-DOM를 통해 멋지게 최적화해주고 있기에 대부분 신경 쓰지 않습니다.

하지만 특정 컴포넌트에 api 요청이 있는 경우에는 최적화, 여러번 랜더링에 신경 써야 하는데요.
제가 구현하고 있는 컴포넌트가 그랬습니다.

차트 라이브러리인 Plotly 를 컴포넌트 모듈화를 하고 있었고, 차트 자체가 브라우저의 리소스를 엄청 잡아먹는 요소인지라 조금 더 신경 써서 개발하고 있었습니다.

구현이 완료된 후에 값을 찍어보니 차트가 두번 랜더링이 되고 있었습니다. (??? 너 왜이래..)

constructor 내부에선 비동기 못해?

두번 랜더링 되는 문제를 찾기전에  잠깐 차트 코드를 한번 살펴보겠습니다.

class Chart {
  private __meta: ChartInfo | undefined;

  constructor(chartType : string) {
    this.__meta = undefined;
    this.getChartMeta(chartType);
  }
  private async getChartMeta(chartType : string) {
    let responseMeta = await getMetaFromServer(chartType);
    this.__meta = responseMeta;
  }

let barChart = new Chart("bar")

constructor에서 비동기로 값을 받아 받아온 값을 __meta에 넣는 간단한 Chart class입니다.

getChartMeta를 통해 비동기 처리를 분리한 게 눈에 보일 겁니다. 그리고 한 가지 의문이 생깁니다. 굳이 왜 나눴지?

"왜 constructor안에서 this.__meta를 초기화시키지 않고 내부 함수로 빼서 다시 넣어주는 과정을 하지?"

this.__meta = getMetaFromServer(chartType);

답은 constructor 안에서는 비동기 함수를 사용할 수 없기 때문입니다.

설명을 보자면 constructor는 고정된 객체가 필요한데, 비동기 함수는 promise를 주고 개념적으로 딱 떨어진 객체가 아니다. 따라서 constructor와 개념적으로 상충되는 관계인 것이죠.

그리고 해당 문제를 회피하는 패턴이 크게 2가지가 있습니다.

  1. 클래스는 생성한 다음, 별도의 init()를 만들어 값을 넣어주는 방법.
  2. builder를 만들어 값을 넣어주는 방법이다.

  *더 자세한 설명 => stackoverflow.com/questions/43431550/async-await-class-constructor

저는 여기서 2번째 방법을  async, await를 이용한 방법으로 구현을 했습니다.

그리고 간단하지만 아주 강력한 툴인 console.log(ㅎㅎㅎㅎ)로 값을 찍어보았습니다.

  private async getChartMeta(chartType : string) {
    let responseMeta = await getMetaFromServer(chartType);
    console.log(responseMeta)
    this.__meta = responseMeta;
  }

근데 console.log(responseMeta)가 계속 두 번씩 찍히는 겁니다.. 

물론 그냥 넘어갈 수도 있는 문제이지만 요청 자체가 두 번씩 날라가고 있었고 그럴때마다 차트가 랜더링되는게 눈에 보였기에 최적화가 필요한 컴포넌트라 생각하여 원인을 찾아보았습니다.

코드도 살펴보고 겸사겸사 분리할만한 컴포넌트는 분리하고 했는데도 계속 두번씩 찍히는 문제가 발생했습니다.

상위 컴포넌트 랜더링 X, constructor O

그래서  상위 컴포넌트부터 하나씩 지워가면서 어느 순간부터 2번 그려지는지 찾아보았고 결론을 얻었습니다.
결론은 상위 컴포넌트의 리랜더링으로 인한 호출이 아니고, 그냥 constructor부터 다시 호출되는 것이었습니다.

이러한 결론을 얻었고, 이제부턴 내 코드 문제보다는 react, 또는 js의 class 문법이 원인이라는 생각을 하기 시작했습니다.
원래는 위에서 나온 이슈였던 constructor 내부의 비동기 함수로 인한 문제라고 생각했었거든요..

그래서 react 문서를 뒤져보다가 찾게 된 Strict Mode에서 답을 얻을 수 있었습니다.

React Strict Mode(엄격모드)

 

Strict Mode – React

A JavaScript library for building user interfaces

reactjs.org

글을 읽어보시면 side effect를 직접적으로 찾지 못하지만(can't automatically detect) 도와줄 수 있다.(can help)라는 문구가 있었습니다.

네. 그렇습니다. React가 우릴 도와주고 있었는데 그 도움을 받지 못하고 혼자 우리 팀과 싸우고 있었던 겁니다..

그런 다음 프로젝트에서 StrictMode가 선언될 수 있는 메인  index.tsx에 들어가서 봤습니다.

ReactDOM.render(
  <React.StrictMode>
    <MuiThemeProvider theme={CommonTheme}>
      <HashRouter basename="/myproject">
        <Switch>
          <Route path="/login" exact component={Login} />
          <Route path="/" exact component={Main} />
          <Redirect path="*" to="/" />
        </Switch>
      </HashRouter>
    </MuiThemeProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

보이십니까.. <React.StrictMode>... </React.StrictMode>... 

문서를 보면 아시겠지만 개발 모드에서만 2번씩 호출하고 실제 프로덕션으로 내보낼 때는 코드대로 동작한다고 되어있습니다.

당신뿐만 아니라 글로벌로 낚인다.

여담이지만 react strict mode 연관 검색어 상단에는 twice render가 있습니다. 
우리뿐만 아니라 전부다 이 부분에 대해 고민을 해봤다는 겁니다. ㅎㅎ

그리고 개발을 하다 보면 어느 순간 주석 처리된 React.StrictMode를 확인할 수 있을 겁니다...(나 뿐만 아니라 이거 때문에 빡친 누군가가 주석 처리한 거거든요 ㅋㅋㅋ)

실제 우리팀의 index.tsx

마치며

마지막에 언급했다시피 어느 순간 React.StrictMode가 주석 처리되어있거나 하는 모습을 스스로 발견할 것입니다.

일단 저는 의도치 않게 2번씩 호출되는 방식이 상당히 불안정하게 느꼈고, 사이드 이펙트를 제거하기 위해선 별도의 판단 로직이 있어야지 공식문서 언급대로 help 하는 것에 의존하기엔 어렵다고 느꼈습니다. 

실제고 차트 랜더링 하는 로직은 큰 데이터로 한번 찍어보거나 수십 개의 차트를 동시에 랜더링 해보는 방식으로 부하 테스트를 진행하고 있습니다.

물론 StrictMode를 사용하게 되면 이런 부하 테스트뿐만 아니라 레거시 코드 판별이 가능하고 react18부터는 좀 더 좋은 개발 환경을 위해 컴포넌트를 자동으로 언마운트 했다가 마운트하고 상태를 보장해주는 기능도 추가했다고 합니다.

하지만 아직 써본 적이 없어서 정확히 장점인지는 모르겠네요. 얼른 react 18 도입하고 그때 한번 주석 해체해 봐야겠습니다.

 

댓글