-
11/18 TIL | React 성능 최적화 2탄. 참조형 타입 & 얕은 비교📝 기록/매일의 기록 2022. 11. 18. 11:03
어제 TIL에서 양이 많아져 2탄으로 쪼갠 React 성능 최적화하기! 바로 시작해보자👏
🥸 참조형 타입 주의하기
1탄에서 설명한 예제는 값이 바뀌었는데도 리렌더링하지 못한 경우였다면, 이번엔 값이 안 바뀌었는데 리렌더링하는 경우를 살펴보려고 한다.
1. props로 함수 전달 시 useCallback 사용하기
// Before // 자식 컴포넌트 Button.jsx function Button({ handleClick }) { return ( <button onClick={handleClick}> 확인 </button> ); } export default React.memo(Button); // 1️⃣ // 부모 컴포넌트 Modal.jsx function Modal() { return ( <Button handleClick={() => { alert('clicked!!'); }} /> // 2️⃣ ); }
1️⃣ 자식 컴포넌트인 Button를 React.memo로 감싸주었는데도 계속 리렌더링되는 상황이다.
2️⃣ 이는 props로 전달받고 있는 handleClick 함수도 참조형이기 때문에 부모 컴포넌트인 Modal이 리렌더링될 때마다 handleClick 함수 또한 계속 새로운 참조를 만들기 때문이다.
그렇기 때문에 함수를 props로 전달하게 된다면 useCallback을 사용하면 좋다.
// After function Modal() { const onHandleClick = useCallback(() => { // 1️⃣ alert('clicked!!'); }); return ( <Button handleClick={onHandleClick} /> ); }
1️⃣ 부모 컴포넌트인 Modal이 몇 번을 리렌더링 하더라도 useCallback으로 묶어준 함수인 onHandleClick의 이전 값을 다 기억하기 때문에 리렌더링되지 않는다.
2. 객체는 미리 선언해서 사용하기
// Before // 자식 컴포넌트 MenuList.jsx function MenuList({ menu }) { return ( <div> <p>{menu.name}</p> <p>{menu.link}</p> </div> ); } export default React.memo(MenuList); // 1️⃣ // 부모 컴포넌트 Header.jsx function Header() { return ( <MenuList menu={{ name: 'About', link: '/about' }} /> // 2️⃣ ); }
1️⃣ 자식 컴포넌트인 MenuList를 React.memo로 감싸주었는데도 계속 리렌더링되는 상황이다.
2️⃣ 부모 컴포넌트인 Header에서 객체 리터럴 방식으로 자식 컴포넌트 MenuList에 props를 넘기고 있기 때문에 새로운 객체가 계속 생성되기 때문이다. 하여 Header가 리렌더링될 때마다 계속 새롭게 생성된다.
// After const MENU_ITEM = { name: 'About', link: '/about' } function Header() { return ( <MenuList menu={MENU_ITEM} /> ); }
그렇기 때문에 props로 고정값을 넘기는 위와 같은 경우엔 항상 상수로 선언하고 props로 넘겨줄 수 있도록 한다.
🎈 얕은 비교 주의하기
state가 참조형 타입인 경우에는 직접적으로 수정해서는 안된다. 항상 새로 만들어 다시 setState를 하여야 하는데, 이는 참조형 타입의 특성 때문이다. 공식 문서의 코드를 같이 살펴보자.
// 부모 컴포넌트 WordAdder.jsx class WordAdder extends React.Component { constructor(props) { super(props); this.state = { words: [ 'foo' ] }; this.handleClick = this.handleClick.bind(this); } handleClick() { // This section is bad style and causes a bug const words = this.state.words; // 1️⃣ words.push('foo'); // 2️⃣ this.setState({ words: words }); } render() { return ( <div> <button type='button' onClick={this.handleClick} /> <ListOfWords words={this.state.words} /> </div> ); } }
1️⃣ WordAdder 컴포넌트의 handleClick 메서드에서 words의 변수는 배열이다. (= 참조형 타입)
2️⃣ words 배열에 값을 push하면, 배열의 요소가 추가되고 길이도 늘어난다. 내 생각대로라면 전달하고 싶은 words 배열 값이 수정이 되었으니, ListOfWords에 변한 값이 잘 넘어가고 리렌더링이되어야 하는 상황이다. 하지만............
// 자식 컴포넌트 ListOfWords.jsx class ListOfWords extends React.PureComponent { render() { return <div>{this.props.words.join(',')}</div>; // 3️⃣ } }
3️⃣ ListOfWords 컴포넌트에 전달된 props.words는 참조값이 그대로이기 때문에 리렌더링되지 않는다.
⚠️ 이유는 words 변수의 참조값은 그대로인데, 이 값으로 setState를 시전하였기 때문이다. 즉, PureComponent가 this.props.words의 이전 값과 새로운 값을 얕게 비교하기 때문인데, WordAdder의 handleClick 메서드에서 words 배열을 변경시키기 때문에 배열의 실제 단어가 변경되었다 해도 this.props.words의 이전 값과 새로운 값은 동일하게 비교된다. 따라서 ListOfWords는 렌더링되어야 하는 새로운 단어가 있음에도 변동 사항이 반영되지 않는다.
막상 따지고 보니 결국 코어 자바스크립트로 귀결된다. 역시 본질은 코어 자바스크립트를 잘 알야 한다는 것이다. 핵심 개념들을 공부하고 그에 합당한 예제들을 많이 다뤄보면서 자바스크립트 구현 능력을 늘리는 것만이 살 길이라는 생각이 들었다.
이렇게 성능 최적화를 위해 브라우저가 리렌더링되지 않게 하는 팁들을 정리해보았는데, 사실 1탄 서론에서 성능 최적화는 그렇게 중요하지 않다~라고 말했지만, 다룬 내용들이 굳이 성능 최적화를 위해서가 아니라 무조건 해야 하는 내용들인 거 같다(객체 불변성 유지 등). 아샬님이 성능 최적화에 대해서 기본적인 것들에 먼저 최선을 다하고, 정말 커지게 된다면 메모이제이션이나 성능을 최적화할 수 있는 방법들을 고려해서 최후의 수단으로 사용하라고 말씀해주셨는데, 결국 이것도 본질적으로 TDD에서 '우선 동작하는 코드를 먼저 만들고, 그다음에 뭐 더 나은 방법이 없을까?'를 고려하는 생각의 흐름(Red - Green- Refactor)과 일맥상통한 느낌이다.
우선 개발을 당시의 최선의 설계로 구현하고, 무언가 필요하거나 설계의 방향이 좀 수정되어야 하는 상황이 생기면 그때 고쳐나가는 방식으로 작업하자. 물론 그러기 위해서는 당시의 최선의 설계여야 하는 것이 중요하다! 그렇지 못한다면 아예 걍 싹 버리고 다시 시작하는 게 나은 상황이 생길 수도 있으니...(마치 라잌 차세대 프로젝트와 같은...?)
참고
'📝 기록 > 매일의 기록' 카테고리의 다른 글
11/20 TIL | 이쯤에서 다시 보기! (feat. FEConf 2020 & BDD) (0) 2022.11.20 11/19 TIL | 뽀모도로 타이머 '오늘의 명언' 한글 명언으로 리팩터링! (1) 2022.11.19 11/17 TIL | React 성능 최적화 1탄. React.memo & React.PureComponent (0) 2022.11.17 11/16 TIL | React 상태 관리 라이브러리 useStore-ts! 마이크로스토어로 가는 길🚶🏻♀️ (2) 2022.11.16 11/15 TIL | React의 Virtual DOM은 과연 빠른가? (0) 2022.11.15