역시는 역시였나..? 만드는 도중에도 계속해서 렌더링횟수를 생각한다고 생각했는데 어림도없었다. 해치웠나..? 생각해보고 성능테스트를 돌려보았지만 라이트하우스에서 나오는 점수는 56...
하하하 라이트하우스에서 제공해주는 지표를 하나하나보면서 다시 리팩토링을 해보자
✅ 개선사항
개선방법1
useCallback사용, loadGames함수 컴포넌트 밖으로 빼기
첫번째로 진행한 리팩토링은 useCallback을 활용하여 콜백함수를 메모이제이션 하는 방법을 진행해보았다. 사용한 방법은 아주 간단했다. 리렌더링이 될 때마다 초기화되는 함수를 useCallback으로 감싸서 초기화를 막고, 함수가 다시 필요할 때마다 함수를 새로 생성하는 것이 아닌 메모리에서 가져와서 재사용할 수 있게 만들어주는 것이다. 직접 바꾼 코드를 예로 들면
// App.tsx 리팩토링 전
const addCartItem = (game: Game) => {
setCartItems((cartItems) => [...cartItems, game]);
};
const removeCartItem = (id: number) => {
setCartItems((cartItems) => cartItems.filter((game) => game.id !== id));
};
위에서 볼 수 있는 addCartItem, removeCartItem 이 두개의 함수는 사실 addCartItem, removeCartItem이라는 변수에 함수가 할당되어 있다고 볼 수 있는데 App컴포넌트가 렌더링이 되면, 해당 컴포넌트 안에있는 모든 변수들이 초기화가 되기 때문에 위의 두개의 변수에 할당되어있는 함수도 초기화가 되기 때문에 메모이제이션을 통해 초기화되는 것을 막는 것이다.
// App.tsx 리팩토링 후
const addCartItem = useCallback((game: Game) => {
setCartItems((cartItems) => [...cartItems, game]);
}, []);
const removeCartItem = useCallback((id: number) => {
setCartItems((cartItems) => cartItems.filter((game) => game.id !== id));
}, []);
즉, 위처럼 App 컴포넌트가 처음 렌더링될 때만 함수 객체를 만들어서 addCartItem, removeCartItem을 초기화 해 메모이제이션해주고, 이후 렌더링에서는 각 두 변수가 새로운 함수객체를 할당받는게 아니라 이미 할당받은 함수 객체를 재사용해주는 것이다.
또한, 서버와 통신하여 데이터를 받아오는 페치 함수를 App컴포넌트 밖으로 빼내서 렌더링될때마다 새로 할당되지않게 App함수 밖으로 빼주었다.
// App.tsx
const loadGames = async (search = "") => {
const response = await gameList({ page_size: 50, search });
let { results } = response;
results = results.filter((game) => game.ratings_count > (search ? 50 : 10));
results.forEach((game) => (game.price = getPrice(game)));
return results;
};
개선방법2
함수를 따로 정의
이부분은 성능 최적화와는 거리가 있을 수 있지만, tsx의 가독성과 유지보수를 위해 리팩토링을 진행하게 되었다.
// Header.tsx 리팩토링 전
<Button
className="Cart"
handleClick={() => {
setIsCartOpen(true);
addScrollableSelector(".Items");
disablePageScroll();
}}
>
Cart
<div>{cartItems.length}</div>
</Button>
여러 컴포넌트의 코드를 고쳤지만 위의 코드로 예를 들면, onClick핸들러 정의를 처음엔 위의 코드처럼 작성하였다. 그렇게 하니 tsx코드들의 가독성이 매우 떨어졌고, 유지보수에서의 단점이 보였다. 그래서 함수를 따로 정의하여 핸들러가 작동할때의 함수를 묶어두고 onClick핸들러에 전달하는 방식으로 리팩토링을 진행하였다.
// Header.tsx 리팩토링 후
const openCart = () => {
setIsCartOpen(true);
addScrollableSelector(".Items");
disablePageScroll();
};
return(
<Button className="Cart" handleClick={openCart}>
Cart
<div>{cartItems.length}</div>
</Button>);
위처럼 openCart라는 함수를 따로 정의해주고 handleClick이라는 이벤트핸들러에 전달해줌으로서 가독성과 유지보수를 향상시키고, 재사용 가능한 코드로 만들어주었다.
개선방법3
React.memo로 컴포넌트 메모이제이션
React.memo는 컴포넌트가 동일한 props로 동일한 결과를 렌더링한다면, React.memo를 호출하여 결과를 메모이징하도록 래핑하여 마지막으로 렌더링된 결과를 재사용하는 고차 컴포넌트이다.
쉽게 말하면, 부모 컴포넌트가 렌더링되면 모든 자식 컴포넌트가 렌더링되게 되는데, props가 변경되지 않았다면 굳이 자식 컴포넌트가 렌더링이 될 필요가 없다. 이때 React.memo를 이용하여 불필요한 렌더링을 방지해주는 역할을 한다고 생각하면된다.
// Header.tsx 리팩토링 전
const Header = ({ cartItems, setIsCartOpen }: Props) => {
.
.
.
};
// Header.tsx 리팩토링 후
const Header = React.memo(({ cartItems, setIsCartOpen }: Props) => {
.
.
.
};
Header컴포넌트는 Home, GameList, GameDetails의 여러가지 페이지에서 동일하게 보여진다. 그때마다 계속해서 렌더링이 되어야하는 상황에서 이전 렌더링과 비교하여 props가 같다면, 메모이징된 내용을 재사용하게 되는 것이다. 이렇게 재사용을 함으로써 리렌더링시 가상DOM에서 달라진 부분을 확인하지 않기 때문에 성능의 이점이 있다.
사실 가져다가 쓰는 api에 이미지들이 너무 많고, jpg로 받아와지기 때문에 이미지에 대한 성능감소가 너무나 크게 작용한다. 그리고 이 정도의 규모면 위의 최적화들이 크게 체감이 되지 않거나 안한것만도 못할 수도 있다. 하지만, 계속 아리송했던 리액트 최적화에 대해 좀더 자세히 알게 된 계기가 된 것 같고, 나중에 더 큰 규모의 프로젝트를 하게 된다면, 이렇게 작게나마라도 해보았던 경험이 그래도 도움이..될거라고 믿고 클린코드와 퍼포먼스에 좀더 치중하여 코드를 쳐버릇하면 언젠가 크게 리턴으로 돌아오지 않을까 생각한다. 아직 리팩토링 해야할게 많이 보이기 때문에 다음 포스팅도 리팩토링한 내용을 작성할 것 같다.