주제 : React 성능최적화 & Best Practice

성능 개선이 필요한 이유

JS도 React도 성능을 고려하지 않고 개발하면 갑자기 사이트가 느려지는 순간이 올 수 있기 때문에 미리 성능을 개선하는 방식으로 프로그래밍을 해야 합니다. 알면서 안 쓰는 것과 모르는 것은 다르기 때문에 개선에 대해서 알아서 쓰면 좋을 것 같습니다. (실제로는 0.01초 밖에 개선이 안 되겠죠. 그래도 쌓이면 커요!)

  • 학습 할 수 있는 내용은 아닙니다.
  • 실제로 일하면서 접하면 익힐 수 있을 것입니다.
  • 하지만 알고 있다면 실무에서 도움이 될 것입니다.
  • 효율이 있는 코드가 이런 코드가 아닐까요? (리팩토링, 성능개선)

Class/Functional Component

  • 요즘 리엑트는 Functional인데, 과거에는 Class형태가 많아서 레거시 코드가 남아 있을 것입니다.
  • 그래서 Class형태를 알고 있어야 합니다.

알아보기

  • 리액트 라이프사이클 순서와 역할?
  • 클래스 형태에서는 언제 생성이 되고, 랜더가 업데이트 되고, 언제 사라지는지 명확히 보였습니다. 하지만 함수형은 명확하게 보이지 않습니다.
  • 라이프사이클 중 써본 메소드가 뭐가 있는지?
  • 함수형 컴포넌트와 클래스형 컴포넌트의 차이는?
  • 지금 배우는 프론트엔드 개발자들은 Class를 잘 사용하지 않는 경우가 많습니다.
  • 클래스 부분을 알고 있어야 합니다.

React에서 좋은 코드는?

회사의 수익!

성능 향상 목적은 회사 수입을 위해서 하는 것입니다. 유명한 서비스를 보면 로딩이 빠른데, 로딩이 조금만 느려도 서비스를 이용하지 않는 사용자가 있고, 그건 회사의 수익과 연결되기 때문입니다. 따라서 회사 매출을 위해 최적화가 필요합니다.

빠르게 협업하고 유지보수 할 수 있는 것

새로운 코드, 더러운 코드는 다른 동료들이 힘들지 않을까요? 서로 잘 사용하고 쉽게 이해할 수 있는 코드를 사용했을 때 유지보수 시간이 짧아지고, 시간이 짧아진다는 것은 곧 비용의 절약입니다. 따라서 빠르게 협업하고 유지보수할 수 있는 **‘클린코드’**를 작성해야 합니다.

DOM 조작을 줄여라

DOM 조작할 때 CPU리소스를 가장 많이 사용합니다. react에서는 rerender가 안 되는 것이 중요하죠. props랑 state 바뀔 때 rerender가 되잖아요. 이런 것을 잘 조절해야 합니다.

JavaScrfipt에서의 클린코드

클린코드 책이 있습니다. 그 책의 JavaScript 버전이 github에 올라와 있습니다. 여기를 참고해서 기본적인 클린코드를 작성해야 합니다. 코딩할 때 좋은 코드인지 깃허브 페이지를 켜 놓고 확인해 보면서 코딩하면 좋지 않을까요?

‘변수명, 함수명, 코드가 지저준한 것’ 이것들은 협업할 때 유지보수 비용의 증가로 이어지기 때문에 많은 고민이 필요합니다.

함수는 오로지 한 행동만 해야 합니다.

함수는 하나의 기능만 가지고 있는 것입니다. 컴포넌트도 함수 아닌가요? 그럼 하나의 컴포넌트에 여러 기능이 있으면 안 되잖아요. 쪼갤 수 있는 최대한으로 쪼개는 것이 중요합니다.

리팩토링을 할 때 가장 중요한 것이 함수를 하나의 기능만 넣는 것으로 줄이는 것입니다. 그 이유는 유닛 테스트를 위해서죠. 유닛 테스트에서 input과 output을 테스트할 수 있잖아요. 그럼 한 개의 기능만 있어야겠지요?

주석이 없어도 코드로 설명이 되면 좋습니다!

주석이 없어도 이해가 되는 깔끔한 클린코드가 가장 좋은 것입니다. 하지만 실력이 부족해서 이해하지 못하는 경우도 있겠죠. 이런 것을 공부하는 방법은 코드를 클론해 와서 로컬에서만 사용할 수 있게 하는 것입니다. 그 다음 내 로컬 코드에는 주석을 달아 놓는 거죠. 그렇게 공부하는 것은 괜찮습니다.

오해

  • 클린코드를 위해서 지금 코드를 다 뒤짚으면 안 됩니다.
  • 리팩토링하는 것보다 처음부터 깨끗한 코드를 만드는 것이 좋습니다.
    • 다시 해야지라고 생각해도 다시 할 기회가 올까요?

React로 사고하기

컴포넌트 분리

컴포넌트도 함수이기 때문에 한 기능만 하는 것이 이상적입니다. 따라서 컴포넌트는 작은 단위로 분리되어야 합니다.

state는 정확히 설계하고 사용해야 합니다.

state는 오로지 상호작용을 위해서 사용하는 것입니다. 데이터가 바뀌지 않는데 state를 쓰면 안 됩니다. 최적화를 위해서 렌더 변경을 조금만 해야 하잖아요. 그러니까 최소한으로 사용해야 한다는 것이죠.

컴포넌트 내 변수의 위치

  1. 임포트 순서
    1. 경로에 따라 묶어줍시다 (멀리있는 것을 가장 위에 가까운 위치는 아래요!)
  2. propType (타입 체크를 합시다)
  3. 컴포넌트의 정의
  4. Styled Component
    1. 들어가자 마자 정의하는 방법
    2. 컴포넌트 정의하는 파일을 같은 폴더에 만들어서 임포트해서 쓰는 방법이 있습니다.⭐️
  5. 간단한 상수 설정 (웬만하면 외부로)컴포넌트는 input이 js output이 jsx가 되는 함수입니다. 렌더가 될 때 상수가 있으면 이것도 계속 다시 읽을 거 아니에요. 그러니까 컴포넌트 밖에 상수를 설정해야 한다는 거예요.
  6. 해당 컴포넌트에서만 사용할 함수 (2번 이상 사용하면 utils로 보내세요)왼만한 함수는 다 컴포넌트 바깥으로 빼는 것이 좋습니다. 다시 읽지 않도록 해 주세요!

조건부 렌더링은 패턴이 아니라 가독성을 높이기 위한 팁!

  • and 연산자 (&&)
  • 삼항 연산자 (a ? b : c)삼항 연산자의 중첩은 파악이 힘듭니다. 그리고 and 연산자를 써야 할 곳에 삼항 연산자를 쓰면 안 됩니다.

가독성을 높이려면?

<ul
  style={{
    backgroundColor:
      type === ProgressBarType.Head
        ? 'rgba(0, 0, 0, 0.05)'
        : inProgressIndex !== -1
        ? lightColor
        : 'rgba(0, 0, 0, 0.05)',
  }}
>

<ul
	className="progress-bar"
  style={{ backgroundColor: (inProgressIndex !== -1) && lightColor}}
>
.progress-bar {
	background-color: rgba(0, 0, 0, 0.05);
}

React 성능 향상을 위한 방법

React.PureComponent, React.memo

  • Props 값이 변하지 않으면 다시 렌더하지 않음이 두개를 사용하면 Props값이 변하지 않으면 다시 렌더하지 않도록 하는 것입니다. Memoization은 프로그램이 동일한 계산을 반복할 때 메모리에 값을 저장하여 반복 수행을 제거하는 것입니다.
class CounterButton extends **React.PureComponent** {
  constructor(props) {
    super(props);
    this.state = { count: 1 };
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({ count: state.count + 1 }))}>
        Count: {this.state.count}
      </button>
    );
  }
}
const CounterButton = ({ color }) => {
	const [count, setCount] = useState(1);  

	return (
    <button
      color={color}
      onClick={() => setCount(state => state.count + 1)}>
      Count: {count}
    </button>
  );
}

export default **React.memo**(CounterButton);

부모 컴포넌트가 rerender되면 자식 컴포넌트도 모두 rerender되거든요.

const Modal = ({ title }) => {
	const [btnColor, setBtnColor] = useState('yellow');  

	return (
    <div>
			<p>{title}</>
			<button onClick={() => { setBtnColor('black') }}>Change Color!</button>
			<CounterButton color={btnColor} />
		</div>
  );
}
  • Modal 의 props가 변경되어, title 값이 바뀌면,
  • Modal 컴포넌트는 rerender 되고
  • CounterButton 컴포넌트에 넘어가는 btnColor는 그대로인데,
  • CounterButton 컴포넌트 까지 같이 전부 rerender 됩니다.따라서 다시 랜더되지 않게 하기 위해서 React.memo를 사용합니다. 랜더가 되는 것을 확인하기 위해서 콘솔을 넣어 보고 여러번 렌더가 된다면 찾아보면 되겠죠.

⚠️ 주의 ⚠️

  • 아래는 리액트 공식문서 예제입니다!class ListOfWords extends **React.PureComponent** { render() { return <div>{this.props.words.join(',')}</div>; } } class WordAdder extends React.Component { constructor(props) { super(props); this.state = { words: ['marklar'] }; this.handleClick = this.handleClick.bind(this); } handleClick() { // This section is bad style and causes a bug const words = this.state.words; words.push('marklar'); this.setState({ words: words }); } render() { return ( <div> <button onClick={this.handleClick} /> <ListOfWords words={this.state.words} /> </div> ); } }
  • state가 변수나 객체일때 직접적으로 수정하지 마세요! (생각치 못하는 side effect 많음)
    • 새로 만들어서 다시 setState 하자~
  • 참조형에 대해서 늘 주의하고 살자!
    • WordAdder 컴포넌트의 handleClick 에서 words 변수는 배열(참조형 데이터 타입)이다.
    • words 배열에 값을 push 하면, 배열요소가 추가되고 길이도 늘어났지만
      • ⚠️ words 변수의 참조값은 그대로인데, 이 값으로 setState 했으니..
      • ListOfWords 컴포넌트에 전달된 props.words 는 참조값 그대로라서 **ListOfWords**는 rerender 되지 않는다.
      • (전달하고 싶은 words 배열 값이 수정됐으니, **ListOfWords**에 변한 값 잘 넘어가고, rerender도 되는게 정상!)
    • **ListOfWords**가 PureComponent 가 아니었다면, 부모 WordAdder 컴포넌트의 words state가 변경되면서, 자식 컴포넌트까지 rerender 됐을텐데 (이건 얻어걸린 것..)
    → 한 번에 술술 안 읽히는 분은 자바스크립트 모든 책. 첫 장. 데이터타입을 읽어야 합니다! (기본형 vs 참조형)위 예제가 리랜더 되지 않는 이유는 참조형은 배열이 아무리 늘어나더라도 실제 배열의 주소는 바뀌지 않잖아요. 그래서 배열의 값은 바뀌어도 연결된 주소가 바뀐 것이 아니니까 리랜더가 안 되는 것입니다.메모는 가장 필요한 경우에만 사용해야 합니다. console.log를 계속 찍어보면서 정말 필요한 상황에 사용하면 좋을 것 같습니다.

Avoid Object/Array Mutation

  • (X) state가 Object나 Array일 때 state 값 자체를 변경하지 말자handleClick() { this.setState(state => ({ words: state.words.concat(['marklar']) })); } or 이런 예제function updateColorMap(colormap) { colormap.right = 'blue'; }
  • (O) ES6의 spread syntaxhandleClick() { this.setState(state => ({ words: [...state.words, 'marklar'], })); }; 전에 공부할 때 많이 본 것처럼 ... 스프레드 신텍스를 사용해서 새로운 배열로 복사하도록 합시다.
  • (O) ES6의 Object.assignfunction updateColorMap(colormap) { return Object.assign({}, colormap, {right: 'blue'}); } 빈 객체, 원래 객체, 넣을 객체를 넣어서 사용하는 방법도 있습니다.
  • (O) ES8의 object spread propertiesfunction updateColorMap(colormap) { return {...colormap, right: 'blue'}; } 결국 이것은 자바스크립트 내용이에요. 데이터 타입 참조형을 보고 정리하고 공부하면 좋을 것 같습니다. 깜빡하고 몰랐던 내용은 나중에 다시 정리를 해 봅시다.

참조형 데이터를 주의

함수 전달할 때는 useCallback 쓰기


// Child
function CounderButton({ handleClick }) {
  return (
    <button onClick={handleClick}>
      확인
    </button>
  );
}

export default React.memo(CounderButton);

// ==========================================
// Parent

function Modal() {
  return (
      <CounderButton 
				handleClick={() => { alert('clicked!!'); }} 
			/>
  );
}
function Modal() {
    const onHandleClick = useCallback(() => {
        alert('clicked!!');
    });

    return (
        <CounderButton
            handleClick={onHandleClick}
        />
    );
}

전에 공부할 때 이런 상황에 왠만하면 useCallback항상 써야한다고 공부 해놓고 잊어버렸네요. 위 예제에서 handleClick={() ⇒ {alert(’blabla’)}} ← 이게 Modal이 리랜더 될 때마다 생성되니까요. 안 되겠죠. 그래서 useCallback을 해야 합니다.

객체는 미리 선언해서


// Child
function MenuList({ menu }) {
  return (
    <div>
      <p>{menu.name}</p>
			<p>{menu.link}</p>
    </div>
  );
}

export default React.memo(MenuList);

// ==========================================
// Parent

function TopBanner() {
  return (
      <MenuList 
				menu={{ name: 'About', link: '/about' }} 
			/>
  );
}
function TopBanner() {
  return (
      <MenuList 
				menu={MENU_ITEM} 
			/>
  );
}

const MENU_ITEM = {
	name: 'About', 
	link: '/about'
}

두 번 이상 쓰는 것은 상수로 빼서 무조건 재사용하셔야 합니다. 갑자기 지난 번 과제의 USD가 다른 이름으로 바뀌면 어떡할 거예요? 그러니까 2번 이상 쓰는 것은 상수로 빼야 한다는 거예요.

useEffect 다시 보기

두 번째 매개 변수를 관리하는 것이 관건입니다. 하지만 두 번째 매개 변수가 늘어나면 오히려 관리하기 힘듭니다. 그러니까 useEffect를 나눠서 써도 됩니다.

  • 원래는 useEffect 안에서 async 함수 선언하고 호출하면 되는데, 억지의 상황 재현했습니다 😬
function BigButton({ message }) {
	const [color, setColor] = useState();
	
	async function changeColor(isPrimary) {
		const data = await fetchColor(message.type, isPrimary);
		setColor(data.color);
	}

	useEffect(() => {
		changeColor(true);
	}, [changeColor])

	// 생략
}

→ BigButton rerender 될 때마다 changeColor 함수가 생성되기 때문에, 렌더링 할 때마다 매번 changeColor 함수 호출

→ changeColor 함수가 정말로 필요할 때만 호출되도록 해야합니다.

function BigButton({ message }) {
	const [color, setColor] = useState();
	
	const changeColor = useCallback(
		async isPrimary => {
			const data = await fetchColor(message.type, isPrimary);
			setColor(data.color);
		},
		

[message]

); useEffect(() => { changeColor(true); }, [changeColor]) }

추가 읽어 보기

https://www.zigae.com/useState-dont-over/ : useEffect 남용 금지

https://velog.io/@edie_ko/lighthouse-performance : 성능 최적화 (코인 차트, 지도 마커 등등의 프론트앤드)

Last modified: 2022년 02월 08일

Comments

Write a Reply or Comment

Your email address will not be published.