useState

crazy4u2·2020년 11월 9일
0

리렌더링

react 를 쓰는 이유 중에 하나는 바로 빠른 리렌더링이다. 그 중에 특히 몇가지 상황이 발생하면 빠르게 리렌더링을 하는데 그 중에 하나가 바로 state 가 변할 때 이다. 클래스 형을 사용할 때 setState 가 바로 그것이다.

함수형 컴포넌트

react v16.8 이상부터 함수형 컴포넌트를 사용할 수 있다. 굉장히 간결(?)하고 사용하기 좋다. 여기서 기존 클래스형 컴포넌트의 state 를 대체하는 hook 이 바로 useState 이다. 하지만 이 hook 은 한 가지 특징이 있었다.

문제발생

차에서 부터 나오는 각종 정보가 1초에 10번(10Hz)속도로 내려오고 그 소켓 이벤트에 맞춰서 초시계(랩 타이머)가 작동하고 중간에 한번식 시간을 보정하고 차량의 vital(엔진회전수, 속도, 등등)을 계속 화면에 그려줘야 한다. 그때 바로 useState 를 사용하였다. 그러다보니 랩 타이머를 구현하기 위해 100ms 간격으로 state 를 갱신하면서 화면을 그리기 시작했다.

그러다 보면 일정시간 (영암 상설코스 기준 0랩 포함 3랩 이후 정도.. 시간으로는 4분 30초 이후부터) 화면의 렌더링이 늦어지고 성능이 급격히 나빠지며 브라우저가 죽어버리는 일이 발생한다. 그동안 주워들은 풍월로 추리해보면 이것이 바로 그 유명한 메모리 누수이다.

useState의 특성

useState 의 두번째 인자인 set 함수가 호출되면 함수형 컴포넌트가 처음부터 다시 실행된다(!) 라는 특성이 있다. 아마 나처럼 야매가 아닌 사람들은 문서를 잘 읽고 금방 알았겠지만 나같은 야매들은 이 내용을 이해하고 알아차리는데 시간이 걸렸다. 그러면서 몇 일의 시간을 까먹었고.. 덕분에 1초 단위로 지나간 시간을 구하는 setInterval() 을 이용해서 시계를 그리는 부분에서도 해당 함수형 컴포넌트의 제일 처음에 선언한 console.log() 가 계속 찍히고 있는 것을 보았는데 그걸 보고도 알아차리지 못하고 있었다.

문제의 이유

이 서비스를 만들며 차에서 나오는 웹소켓의 데이터를 끌어쓰기 위해서 new webSocket() 객체가 아닌 new Faye() 라는 것을 사용하였다. 이유는 기존에 서버 측도 이 플러그인에 맞게 코드가 작성되어 있고 클라이언트에서 쓰기에도 간편하기 때문이었다. 여기서 간과한 것이 const client = new Faye.Client() 같은 걸 선언 할 때, useEffect 안에서 하지 않고 최상위 컴포넌트 바로 아래에서 이걸 선언하니까 1초 정도의 간격으로 state가 변하는 페이지에서는 몰랐는데, 랩타이머 처럼 100ms 단위로 state가 지속적으로 갱신되는 페이지에서 문제가 생겨버린 것이다. 즉, 1초에 10번씩 new Faye.Client() 객체를 생성하고 있었던 것이다. (정말 나는 병신인가보다...)

문제의 해결

그래서 여기저기 수소문 해본 결과(정말 리액트 코리아 슬랙에 계신 분들 너무 감사합니다 ㅠ) 한 이용자께서 useState 의 특성을 알려주었고 그래서 new Faye.Client() 부분을 useEffect 안으로 넣어서 해결하게 되었다. 그 외에 컴포넌트 템플릿에서 발생하는 이벤트에 의해 실행되어야 하는 함수(컴포넌트 실행 함수 바로 아래에 위치해야하는 함수를 말함)의 경우는 기존에 컴포넌트 전역처럼 선언되었던 new Faye.Client()를 사용할 수 없어서 해당 함수가 실행될 때, 그 안에서 해당 객체를 생성해서 쓰고 버리는 쪽으로 작업을 완료하였다.

중간에 삽질

처음에는 set을 하는 state가 너무 많아서 저러는 줄 알았다. 실제로도 브라우저의 메모리 사용량이 특정 시점부터 급격하게 늘어났고 문제의 이유 부분을 모르고 있었으니 엄한 곳에 삽질을 하고 있었다. 그도 그럴 것이 랩타이머는 100ms 단위로 화면을 바꾸고 기타 정보들도 상당히 빠른 속도로 계속 state 를 갱신한다. 혹시 갱신되는 state의 횟수에 제한이 있나 하는 둥의 멍청한 생각과 질문을 해댔고 그렇지는 않다고 역시나 많은 분들이 알려주었다. 너무 state 가 많이 갱신되면 알아서 GC 를 한다고 말이다.

향후 튜닝계획

이 서비스는 사실 서비스 전반에 걸쳐서 웹소켓을 쓴다. 지금은 지식이 부족해서 컴포넌트를 옮겨갈 때(페이지 이동)마다 useEffect 의 뒷처리 함수에서 소켓을 끊고 넘어가는 컴포넌트에서 마운트될 때 소켓을 연결하고 하는 식으로 했는데 이번 일을 계기로 최상위 컴포넌트 (나는 nextJS를 쓰니까 _app.js)에서 마운트 될 때 해당 웹소켓 객체를 생성시키고 계속 props 로 넘겨서 이후 마운트 되는 컴포넌트들이 쓸 수 있게 해보는 방향으로 코드를 수정해보려고 한다.

그리고 또 하나는 지금 차량의 속도, 진행하고 있는 섹터, 차량의 엔진 회전수(외 여러가지 더 있음), 직전 랩의 기록, 베스트 기록, 타임델타 등 동시에 리렌더링 되는 녀석들이 많은데 리액트 코리아 슬랙에서 많이 들었던 조언이 리렌더링의 범위를 좁게 하라는 것이었다. 바꿔 말하면 한 컴포넌트에서 state 의 변화에 따라 계속 바뀌는 녀석들을 다 쪼개서 별도의 컴포넌트로 만들고 props 를 내려주면서 바뀌는 걸로 해봐야 할 거 같다. 특히 다는 못하더라도 직전 랩 기록과 베스트 기록의 경우는 한 랩을 돌고 난 다음 결과를 비교해서 내용이 바뀔 경우에만 렌더링이 되면 되기 때문에 필수적으로 해야한다. 그렇지 않은 상태에서는 한 랩(1분 27초 정도)을 도는 동안 값은 바뀌지 않지만 지속적으로 해당 stateset 이 호출되고 있기 때문이다.

profile
야매로 먹고사는 프론트엔드 개발자

0개의 댓글