- Published on
성능최적화
- Authors
- Name
- 길재훈
성능최적화
성능 최적화를 위해 여러가지 방법이 존재한다. 각각의 방법을 살펴본다.
기본적으로 리렌더링 되는것을 알아보는것은 중요하다. 다음을 살펴보자
import { useState } from 'react'
const MemoizationPage = (): JSX.Element => {
console.log('컴포넌트가 렌더링 되었습니다')
let countLet = 0
const [countState, setCountState] = useState(0)
const onClickCountLet = (): void => {
console.log(countLet + 1)
countLet += 1
}
const onClickCountState = (): void => {
console.log(countState + 1)
setCountState(countState + 1)
}
return (
<>
<div>카운트[let]: {countLet}</div>
<button onClick={onClickCountLet}>카운트[let] + 1 올리기</button>
<div>카운트[state]: {countState}</div>
<button onClick={onClickCountState}>카운트[state] + 1 올리기</button>
</>
)
}
export default MemoizationPage
위의 로직을 살펴보면 매우 명확하게 rerender
이 되는것을 알수있다. 카운트[let] + 1 올리기
를 누르면, 해당 값이 올라가지만 화면상에 나오지는 않는다.
하지만 카운트[state] + 1 올리기
를 누르면 리렌더링 되면서 let
의 값이 초기화되는것을 알수 있다. 그렇다면 해당 값을 지속 유지할 수 있는 방법이 없을까?
그때 사용하는 hooks 가 바로 useMemo
이다. useMemo
는 해당 값을 cache
해 놓고, 해당 cache
한 값을 재사용할 수 있다.
useMemo
의 특징으로 한번 연산해 놓고 연산된 값을 리턴하면 그 다음부터는 리턴된 값만을 사용한다. 다음을 보자
const bbb = useMemo(() => {
let result = 0
for (let i = 0; i < 900000; i += 1) {
result += i
}
return result // 이후 연산된 result를 저장해서 사용한다.
}, [])
useCallback
useCallback
역시 useMemo
와 비슷하지만, 함수
를 저정한다는 것이 다르다 다음을 보자.
const onClickCountLet = useCallback((): void => {
console.log(countLet + 1)
countLet += 1
}, []) // useCallback 으로 함수를 저장함
useCallback
사용시 주의해야할 사항이 존재한다. useCallback
은 state
를 사용시 주의해야 한다.
다음을 보자
const onClickCountState = useCallback((): void => {
console.log(countState + 1)
setCountState(countState + 1) // 이때 countState 값 역시 기억하므로, 값이 변경되지 않는다.
}, [])
위의 setCountState
를 사용하면 제대로 동작하지 않는다. usecallback
시 해당 내용을 기억하고 있기에 변경자체가 되지 않는것이다.
useCallback
사용시 state
를 사용한다면 다음처럼 prev state
를 사용하여 확인해야 한다.
const onClickCountState = useCallback((): void => {
console.log(countState + 1)
setCountState((prev) => prev + 1) // 제대로 작동한다
}, [])
이러한 방식으로 useCallback
을 사용할 수 있다
useMemo 를 이용한 useCallback
useMemo
를 사용해 단순한 useCallback
을 만들 수 있다
const onClickCountState = useMemo(() => {
return (): void => {
console.log(countState + 1)
setCountState((perv) => prev + 1)
}
}, [])
위처럼 함수를 useMemo
해서 return
하면 useCallback
과 똑같이 행동한다. useCallback
처럼 작동하므로 setCountState
를 perv
를 사용해서 +1
시켰다.
memo
memo
는 컴포넌트 자체를 memo
하여, 부모 컴포넌트가 리렌더링 될때 자식 컴포넌트의 변경사항이 없다면 리렌더링 하지 않는다
이는 성능최적화 작업시 중요한 부분이다. 다음을 살펴보자
const MemoizationPage = (): JSX.Element => {
console.log('부모 컴포넌트가 렌더링 되었습니다')
let countLet = 0
const [countState, setCountState] = useState(0) // 기존의 값을 기억하고 있다.
const onClickCountState = useCallback((): void => {
console.log(countState + 1)
// setCountState(countState + 1); // 이때 countState 값 역시 기억하므로, 값이 변경되지 않는다.
setCountState((countState: number) => countState + 1)
}, [])
return (
<>
<div>카운트[state]: {countState}</div>
<button onClick={onClickCountState}>카운트[state] + 1 올리기</button>
<MemoizationWithChildPage />
</>
)
}
export default MemoizationPage
위처럼 부모 컴포넌트가 자식 컴포넌트를 가진 상황이라고 생각해보자. 만약 MemoizationWithChildPage
가 memo
된 상황이 아니라면 MemoizationPage
의 setCountState
가 실행될때마다 MemoizationWithChildPage
는 리렌더링 된다.
굉장히 불필요한 상황이다. MemoizationWithChildPage
는 변경되는 값이 없는데 부모 컴포넌트가 렌더링 되었다는것 하나만으로 지속 리렌더링 되는것이다.
memo
를 사용하면 이러한 불필요한 rerendering
이 발생하지 않는다
import React, { memo } from 'react'
const MemoizationWithChildPage = (): JSX.Element => {
console.log('자식이 렌더링 됩니다.')
return (
<>
<div>저는 자식 컴포넌트 입니다!!!</div>
</>
)
}
export default memo(MemoizationWithChildPage)
이때 memo
사용시 주의해야할 점이 있다 만약 memo
를 map
에 적용해 보도록 해보자.
// MemoizationWithMapParentPage.tsx
const MemoizationWithMapParentPage = (): JSX.Element => {
const [data, setData] = useState('철수는 오늘 점심을 맛있게 먹었습니다.')
const onClickChange = (): void => {
setData('영희는 오늘 저녁을 맛없게 먹었습니다.')
}
return (
<>
{data.split(' ').map((el, index) => (
<Word key={index} el={el} />
))}
<button onClick={onClickChange}>클릭</button>
</>
)
}
export default MemoizationWithMapParentPage
import React, { memo } from 'react'
interface IWordProps {
el: string
}
const Word = (props: IWordProps): JSX.Element => {
console.log('자식이 렌더링 됩니다', props.el)
return <span>{props.el}</span>
}
export default memo(Word)
위의 로직은 Wrod 컴포넌트
를사용하여 map
을 순회한다. 순회된 map
에 key
값으로 고정된 index
를 주므로, change
된 문자만 변경된다.
즉 영희는
, 저녁을
, 맛없게
만 리렌더링 된다.
이는 우리가 원하는 map
으로써의 기능에 충실한 상황이다. 그런데 key
값을 index
로 주는것은 탐탁치 않다. 그러므로 uuid
를 주도록 하자.
import React, { useState } from "react";
import Word from "./02-child";
import { v4 as uuidv4 } from "uuid";
const MemoizationWithMapParentPage = (): JSX.Element => {
const [data, setData] = useState("철수는 오늘 점심을 맛있게 먹었습니다.");
const onClickChange = (): void => {
setData("영희는 오늘 저녁을 맛없게 먹었습니다.");
};
return (
{data.split(" ").map((el) => (
<Word key={uuidv4()} el={el} />
))}
<button onClick={onClickChange}>체인지</button>
</>
);
};
export default MemoizationWithMapParentPage;
이때 변경되는 문구는 영희는
, 오늘
, 저녁을
, 맛없게
, 먹었습니다.
로 전부다 변경되는것을 볼수 있다.
이러한 이유는 memo
는 전달되는 props
가 변경되어도 리렌더링 되기 때문이다. 위의 key
는 props
로 넘어가는 uuidv4()
를 사용하여 매번 key
값이 변경된다.
변경된 key
값으로 인해 memo
가 재역할을 못하고 있는 상황으로 매번 리렌더링 된다. 이러한 부분을 상당히 조심히 생각하고 코드를 작성해야 한다.
브라우저의 rendering 과정
|--[ HTML ] -- [ DOM ] |
| |
[ download ] --> | |-[Attach]--[reflow]--[repaint]--[composition]--[display]
| |
|--[ CSS ] -- [ CSSDOM ] |
실상 repaint
만 작동시 큰 영향을 미치지는 않는다. 여기서 문제를 일으키는 상황은 reflow
이다.
reflow
시 repaint
까지 같이 진행되므로, 레이아웃 변경이 커질수로 재계산이 많이 일어난다 이러한 모든 상황을 예상하여 코드를 작성하는 것은 한계가 있지만, 관심을 갖고 생각보는것도 좋다.
Layout shift
Layout shift
는UX/UI
에서 중요하게 생각하는 것중하나이다.
이미지의 로딩이 느려 화면 레이아웃이 밀리는 현상을 말한다.
frontend
는 UX/UI
에 대해서 중요하게 생각해야 하므로, 이러한 detail
한 부분도 신경써서 만들 필요가 있다