이번 편은 css 시리즈의 네 번째, 다섯 번째에서 나오는 그 프로젝트의 react 로직에 대한 문제점이라고 봐도 무방하다. 아무래도 페이지 하나에 기능이 많이 들어가다보니 쓸 말이 많은것같다.😂 징글징글하다. 여튼 문제는 과도하게 발생하는 리렌더링이였다. 그 이유는 props가 과하게 많음 + 시간 압박에 의해 분리되지 못한 components가 문제였다. 그래서 오늘은 이것을 어떻게 개선했는지 기록하려한다.
첫 번째로 components에 대해 얘기를 하는 이유는 이 이유가 제일 크다고 생각했다. 정말 사소하게 변명을 하자면 시간의 압박으로 인해 우선은 성능보단 기능 구현을 택했고, 이것이 부메랑이 되어 다시 돌아온것같다... 이것을 어떻게 해야할지 많은 고민을 했고, 이 이상 기능 개발이 크게 추가 되지않을것 같았다. 그래서 노트에 프로토타입을 대략적으로 그렸고, 이것을 기반으로 파일을 정리했다. 대략 6개의 폴더가 나왔고, 한 폴더당 적으면 3개 많으면 6개가 나왔다. 생각보다 많이 쪼개졌는데 에러가 크게 안나서 안도했다.😂 어느정도 생략했지만 대략적으로 보면 아래 폴더 구조와 같다!
📦folder
┣ 📂components
┃ ┣ 📂A
┃ ┃ ┣ 📜 AA.tsx
┃ ┃ ┣ 📜 AAA.tsx
┃ ┃ ┗ 📜 AAAA.tsx
┃ ┣ 📂B
┃ ┃ ┣ 📜 BB.tsx
┃ ┃ ┣ 📜 BBB.tsx
┃ ┃ ┣ 📜 BBBB.tsx
┃ ┃ ┣ 📜 BBBBB.tsx
┃ ┃ ┣ 📜 BBBBBB.tsx
┃ ┃ ┗ 📜 BBBBBBB.tsx
┗ 📜 index.tsx
Prop Drilling
여러 레벨의 중첩된 컴포넌트를 거치는 현상
폴더 구조를 개선했으니, 이제 과도하게 내려주는 props를 개선할 차례다. prop drilling이 제일 시급했으니 어떤 것을 redux toolkit으로 분리할지 고민했다. 그리고 다음과 같은 기준으로 나누었다.
위와 같은 기준으로 컴포넌트를 나눴는데, 네 번째의 기준이 보는 이의 경우 '읭?'스러울 것이다. 내가 맡은 페이지의 경우 모바일과 웹 ui가 굉장히 다른 부분이 있다. 물론 현재 회사에는 웹 / 모바일을 break point를 나누는 재사용 가능한 component가 있지만 한 component에 웹 / 모바일 두 가지의 로직을 구현하기엔 DX(Developer experience)가 안 좋을것이라 생각했다. 그 이유는 ui가 다른 부분이 있다면 이것을 웹 / 모바일 따로 따로 name을 사용해서 구현해야할텐데, emotion의 사용으로 인해 가뜩이나 코드라인이 길어서 한 파일에 웹 / 모바일을 두고싶지 않았다. 이런 이유로 인해 나눠두었다. 이 때, emotion을 쓰면서 좋은 점이 많았지만 코드라인이 길어지는게 조금 아쉬웠다. 하지만 Storybook을 쓰면 이런 아쉬움도 사라지지않았을까..? 라는 생각도 하곤했다.🙄
component를 단계별로 나눈 후 크롬 확장팩인 React Developer Tools를 이용해 렌더링이 자주 발생하는 부분이 어떤 곳인지 살펴보았는데... 마치 별이 반짝 반짝 빛이나는 것 같달까? 스크린샷으로 전/후를 기록하고 싶었지만 혹시나싶어 글로만 설명하는 내가 아까울 정도였다. 페이지 전체가 반짝 반짝 빛나는게 꽤 충격적이여서 후다닥 React.memo와 useMemo를 도입했다.
React.memo와 useMemo는 비슷한것 같지만 React.memo는 이전 props와 새로운 props를 비교해 특정 props의 변화에만 component가 업데이트 될 수 있게하는 장점을 가지고있다. 나의 경우 변화하는 props와 바뀌지 않는 props가 있었는데, 이것들을 어떻게 해야할지 구글링한 결과 React.memo를 사용했고 결과는 만족스러웠다.
chatGPT에서 가져온 간단한 예제코드
const MyComponent = React.memo(function MyComponent(props) {
}, (prevProps, nextProps) => {
return prevProps.importantProp === nextProps.importantProp;
});
useMemo는 계산 비용이 많이 드는 연산 결과를 메모리에 저장하고, 의존성 배열에 지정된 값들이 변경됐을 때 다시 수행된다고한다. 나의 상황에선 캐릭터의 승률을 계산해서 ui상에 그려줘야할 때 사용했다.
chatGPT에서 가져온 간단한 예제코드
import { useMemo } from 'react';
function MyComponent({ a, b }) {
const memoizedValue = useMemo(() => {
console.log('Recalculating only when `a` changes');
return a + b;
}, [a]); // `a`가 변경될 때만 재계산
return <div>{memoizedValue}</div>;
}
이렇게 두 가지를 사용하니 리렌더링이 눈에 띄게 줄어든것이 보였다. 엄청나게 반짝거렸던 별들이 사라졌고, 이것을 대략적인 수치로 나타낸다면 처음엔 70% ~ 80%의 리렌더링을 경험했다면 현재는 30~40%정도?였다. 물론 리렌더링 개선을 하면서 css도 같이 개선했는데, 간단하게 한 가지 예시를 들자면 최대한 공통 css로 만들어서 import 해오는 방식으로 사용했다.(css 얘기도 하면 길어지니 짧게 예시를 들고 넘어가기로..)
사실 최적화를 한다거나 어떤것을 개선한다거나 하는 것들은 시니어급(그러니까 한 5~6년차 수준)의 사람들만 하는것이라 생각했는데, 막상 해보니까 너무 그들만의 세계라고 생각하고 도전을 안했던것이 부끄러웠다. 블로그에 적은 방법이 다는 아니겠지만 겉핥기식이라도 해봤다는 경험에 감사했다. 다음부턴 더 적극적으로 아키텍처를 생각하고, 클린코드를 생각하며 로직을 구현해야겠다. 😎