Memoization
하는 메소드// 최적화 전
function My(props) {
return (
<div>
{props.data}
</div>
)
}
function App() {
const [state, setState] = useState(0)
return (
<>
<button onClick={()=> setState(0)}>Click</button>
<My data={state} />
</>
)
}
// React.memo()를 활용한 최적화 코드
function My(props) {
return (
<div>
{props.data}
</div>
)
}
// React.memo() 활용
const MemoedMy = React.memo(My)
function App() {
const [state, setState] = useState(0)
return (
<>
<button onClick={()=> setState(0)}>Click</button>
<MemeodMy data={state} />
</>
)
}
React.memo()가 props 값을 memoization한 후, 캐싱된 결과를 리턴하기 때문에 동일한 입력 값에 대해 My 컴포넌트를 실행하지 않음.
// 최적화 전
function App() {
const [count, setCount] = useState(0)
const expFunc = (count)=> {
waitSync(3000);
return count * 90;
}
const resCount = expFunc(count)
return (
<>
Count: {resCount}
<input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
</>
)
}
병목 현상
유발// useMemo()를 활용한 최적화 코드
function App() {
const [count, setCount] = useState(0)
const expFunc = (count)=> {
waitSync(3000);
return count * 90;
}
// useMemo() 활용
const resCount = useMemo(()=> {
return expFunc(count)
}, [count])
return (
<>
Count: {resCount}
<input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
</>
)
}
function App() {
const check = 90
const [count, setCount] = useState(0)
// useCallback() 활용
const clickHndlr = useCallback(()=> { setCount(check) }, [check]);
return (
<>
<button onClick={()=> setCount(count + 1)}>Set Count</button>
<TestComp func={clickHndlr} />
</>
)
}
React.lazy()
를 사용함.일반적으로 규모가 큰 React 애플리케이션은 많은 요소, 라이브러리 등으로 구성됨.
애플리케이션의 다른 부분을 로드하려고 노력하지 않을 경우, 사용자가 첫 페이지를 로드하는 즉시 대규모 단일 Javascript 번들이 사용자에게 전송됨 → 페이지 성능에 상당한 영향을 미침.
React.lazy()를 통해 손쉽게 개별 Javascript 청크로 분리해보자!
React.lazy()
const About = React.lazy(() => import('./About'));
<Suspense />
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element=<About/>} />
</Routes>
</Suspense>
비동기 데이터 가져오기
코드 분할
Suspense를 사용하면 코드 분할과 함께 앱 번들의 크기를 줄이고 초기 로딩시간을 최적화할 수 있음. 필요한 컴포넌트만 로드하고 사용자가 해당 컴포넌트를 요청할 때까지 다른 컴포넌트를 로드하지 않도록 해야 함.React Router와 함께 사용
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element=<About/>} />
</Routes>
</Suspense>
</Router>
);
windowing
이라는 기법을 사용하는 것을 추천한다고 함.handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
// 불변성을 지키지 않은 코드
function updateColorMap(colormap) {
colormap.right = 'blue';
}
이런 코드를 작성하고 싶다면, 위와 같이 객체 원본을 변경시키지 말고 Object.assign()이나 object spread properties를 활용해보자 !
// Object.assign() 활용 코드
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
// object spread properties 활용 코드
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}
immer
를 활용하면 불변성이 가져다주는 이득을 잃지 않고 가독성있는 코드를 작성할 수 있음.React를 여러번 다뤄봤지만, 항상 최적화적인 부분에선 우선순위가 밀려 뒤쳐졌던 부분이 있었던 거 같은데요, 그래서 인지 이번 글이 굉장히 흥미롭고 도움이 많이 됐던 거 같습니다.
마지막 부분에 "기능구현 단계에서부터 최적화를 미리 수행하기 보다, 프로젝트를 모두 개발한 후 필요한 곳에 최적화를 적용시키는 것이 중요함 !" 글에 공감했는데요, 개발자의 성장은 코드 리펙토링과 디벨롭 그리고 최적화를 얼마나 많이 했냐에 따라서도 변화하는거 같습니다 !!!
윗 내용에 있는 최적화를 꼭 활용하여 리펙토링 해봐야겠다는 생각이 들었습니다!
좋은 글 감사합니다!
최적화에 대해 고민해볼 수 있는 의미있는 글을 작성해주셔서 감사합니다. 저는 리스트가상화에 대해 처음 접해서 더 알아보았는데요. 리스트 가상화는 "뷰포트만 보이는 것만 렌더링 해준다." 라는 말이 이해가 안가서 찾아보니 스크롤 한 이후에 아이템은 렌더링해주지 않는 "눈에 보이는 것만 렌더링 해준다" 였습니다. 구현을 위해서는 element의 위치가 화면 내부에 있는지 가늠하고 Intersection Observer로 교차를 감지하는 방법으로 구현을 생각해볼 수 있겠네요. 그 후 스크롤 이벤트를 감지해서 추가적으로 데이터를 렌더링 하는 방법으로 구현한다고 합니다.
구체적인 구현방법을 찾아보니 리스트에에 position relative와 innerHeight 를 설정해주고 각 항목은 position: absolute를 주면서 렌더링한다고 합니다!
좋은 글 감사합니다 아름님! 처음으로 접한 리스트 가상화 등을 통해 더 넓게 보는 시야를 갖게된 것 같습니다~!
저는 lazy에 대해서 알아봤는데요, lazy 부분의 리액트 공식 문서에 parameter를 이렇게 설명하고 있습니다.
load: A function that returns a Promise or another thenable (a Promise-like object with a then method). React will not call load until the first time you attempt to render the returned component. After React first calls load, it will wait for it to resolve, and then render the resolved value as a React component. Both the returned Promise and the Promise’s resolved value will be cached, so React will not call load more than once. If the Promise rejects, React will throw the rejection reason for the nearest Error Boundary to handle.
여기서, thenable
이나 Promise를 반환하는 함수를 매개변수로 가진다고 하는데, 여기서 thenable이 무엇인지에 대해서 잠깐 알아보았습니다.
thenable
: then 메서드를 가진 promise와 유사한 객체로, {then : () => {} } 또는 Promise.resolve()
와 같은 형식이 thenable이라고 합니다!
따라서, 매개변수에는 비동기가 오는 게 일반적(꼭 와야하는 건 아니므로)이지만, 그렇지 않은 객체가 들어와도 됩니다! 또 다시한번 보고 넘어가면! 반환된 컴포넌트하려고 처음 렌더링하려고 시도할 때까지는 lazy안에 있는 함수를 절대 실행시키지 않으며 렌더링 필요 시점에 import를 진행하는게 lazy의 역할이고, 한 번 load되면 두 번 이상 호출하지 않는다고 합니다.
그리고 저는 memo 관련해서도 알아보았는데용!
제가 알아본 거는 모든 곳에 memo를 추가하는 것이 좋을까? 였습니다.
일단 일반적으로 인터렉션이 투박한 경우 (페이지 교체 등만 있는 경우) 일반적인 메모화는 불필요하다고 합니다! 반면 편집기나 인터렉션이 세분화되어 있다?? -> 메모화
그리고 memo는 컴포넌트에 전달되는 prop이 항상 다른 경우 무용지물이 될 수 있기 때문에 useMemo, useCallback을 필요로 한다고 합니다!!
여러가지 최적화 방법에 대해서 정리해서 알 수 있어서 너무나도 좋았습니다!
마지막에 소개된 immer에 대해 좀 더 공부해 보고 싶어 조사를 해봤습니다.
immer를 왜 사용해야 할까요?
아티클에 적혀있듯이 리액트에서 배열이나 객체를 업데이트 해야 할 때 직접 수정하면 안되고 불변성을 지켜주어야 합니다. 이에 대한 추가 설명은 위에 잘 적혀있으므로 생략하도록 하겠습니다.
그렇다면 아래와 같은 객체가 있다고 하면 어떨까요?
여기서 만약 id 1인 post의 comments를 추가하고 싶다면,
상태를 업데이트 하기 위해선 위와 같이 다소 읽기 복잡한 코드가 필요하게 됩니다.
이와 같은 상황에서 immer의 유용성이 발휘됩니다.
immer를 사용하게 된다면
다음과 같이 깔끔하게 구현이 가능해집니다.
그렇다면 어떻게 사용하는 걸까요?
일단
라이브러리를 다운로드 받은 뒤,
immer를 import 해줍니다. 보통은 produce라는 이름으로 import 한다고 합니다.
produce는 함수로 첫 번째 파라미터는 수정하고 싶은 상태, 두 번째 파라미터는 어떻게 업데이트 하고 싶은지 정의하는 함수를 넣어줍니다. 이때 불변성을 신경쓰지 않고 그냥 업데이트 해주면 됩니다.
아래는 그 예시 입니다.
여기까지 immer의 가장 기본적인 내용이었고, 추가적인 내용이 궁금하시다면 아래의 참고 자료를 확인하시면 될 거 같습니다!
참고자료
https://react.vlpt.us/basic/23-immer.html
아티클 잘 읽었습니다. ☺️ 아티클 작성하시느라 고생 많으셨습니다!