가볍고 빠르게 시작
12월에 원티드 챌린지 진행하면서 이전에 내가 작성했던 코드와 고민 그리고 디자인 패턴, 설계들을 얼라인하는 시간을 가졌었다.
한 달이 조금 지난 시점에서 요즘 관심있게 보고있는 'https://www.youtube.com/@cosdensolutions'라는 유튜버분이 마침
기초에 가까운 리팩터링 방식을 소개하는 영상을 보게되어 가볍게 따라가며 다시 정리하려고 작성하기 시작했다.
Cosden Solutions
Let's write code to change the world 💻
www.youtube.com
원본 코드
import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { trackEvent } from '@/utils';
import { fetchUser, fetchUserProducts } from './other';
import ProductsList from './ProductsList';
import UserProductsFilters from './UserProductFilters';
//개인적으로 아래처럼 컴포넌트에 직접 인터페이스 적는걸 선호하지 않는다.
export interface Filters {
createdAt?: Date;
}
interface UserProductsPageProps {
userId:number;
}
const UserProductsPage = ({ userId }: UserProductsPageProps) => {
const [filters, setFilters] = useState<Filters>({
createAt: undefined,
});
const [isModalOpen, setIsModalOpen] = useState(false);
const { data: user, isLoading: isLoadingUser } = useQuery({
queryKey: ['user', userId],
queryFn: ()=>fetchUser(userId),
gcTime: 1000 * 60 * 60 // 1 hour
staleTime: 1000 * 60 * 60
})
const { data: products, isLoading: isLoadingProducts } = useQuery({
queryKey: ['userProducts', userId, filters],
queryFn: ()=> fetchUserProducts(userId, filters),
})
const handleButtonClick = ()=> {
setIsModalOpen(!isModalOpen);
}
// Sends analytics event when the page is loaded
useEffect(()=> {
trackEvent('page_view', { pageId: 'user_products_page', data: { userId } });
}, [userId]);
const isLoading = isLoadingUser || isLoadingProducts;
if(isLoading) {
return <div>Loading...</div>
}
return (
<div className='tutorial'>
<h1>hello {user!.name}! //'!의 의미는 널이 아님을 컴파일러에게 전달'
<button onClick={handleButtonClick}>
{isModalOpen ? 'Close Filters' : 'OpenFilters'}
</button>
{isModalOpen && <UserProductsFilters onChange={setFilters} />}
{products && products.length ? (
<ProductsList products={produts} />
) : (
'No Products available.'
)}
</div>
);
};
export default UserProductsPage;
아주 흔하게 볼 수 있는 컴포넌트의 형태이다.
이 컴포넌트는 아래의 것들을 수정하면 조금 더 좋아질 가능성이 있다.
1. 컴포넌트 내부에서 useQuery를 직접 사용한다.
2. 모달 관련 지역 상태, 업데이트 함수가 포함되어있으며 이를 통해 JSX를 렌더링한다.
3. 액션이 포함되어있다.
고민해봐야할 내용
유튜버분은 다음과 같이 말한다.
당신의 리액트 코드에 지역 상태가 포함되어있다면 스스로에게 물어보세요.
이 상태(코드)가 이 곳에 있어야하는가
아니면 다른 곳(부모, 다른 컴포넌트 등)으로 이동해야하는가
정말 많이 했던 고민이라서 반가운 내용이었다.
여러가지 이유나 근거가 있겠지만, 가장 보편적인 이유는 리렌더링이다.
지금의 상황이라면, UserProductsPage는 모달이 열리고 닫힐때마다 컴포넌트가 리렌더링 될 것이다.
단일 책임의 원칙을 가볍게 설명하며 '분리하자'고 말한다.
//UserProductsFiltersButton
import { useState } from 'react';
import UserProdutsFilters from "./UserProductsFilters";
interface UserProductsFiltersButtonProps {
}
const userProductsFiltersButton = ({}: UserProductsFiltersButtonProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleButtonClick = ()=> {
setIsModalOpen(!isModalOpen);
}
return (
<button onClick={handleButtonClick}>
{isModalOpen ? 'Close Filters' : 'OpenFilters'}
</button>
{isModalOpen && <UserProductsFilters onChange={setFilters} />}
)
}
위 코드를 보면 setFilters가 필요하다.
모달 오픈같은 경우는 이 컴포넌트에서 사용되는 것이니까 옮기는 것이 좋다.
반면에 filters는 부모에 유지하는 것이 좋다.
1. filters는 fetching을 위해 부모에서 사용된다.
2. fetching을 위한 코드들까지 모달로 옮기는 것은 말이 안 된다.
라는 근거가 있기때문이다.
//UserProductsFiltersButton
import { useState } from 'react';
import UserProdutsFilters from "./UserProductsFilters";
import { Filters } from './UserProductsPage';
interface UserProductsFiltersButtonProps {
onChange: (filters: Filters) => void;
}
const userProductsFiltersButton = ({ onChange }: UserProductsFiltersButtonProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleButtonClick = ()=> {
setIsModalOpen(!isModalOpen);
}
return (
<button onClick={handleButtonClick}>
{isModalOpen ? 'Close Filters' : 'OpenFilters'}
</button>
{isModalOpen && <UserProductsFilters onChange={onChange} />}
)
}
부모에서는 해당 컴포넌트를 콜하고, setFilters를 넘겨주기만 하면 된다.
두 번째는 useQuery를 사용하는 부분이다.
특정 상황에서 코드 조각들을 반복사용될 수 있는 점 때문에 커스텀 훅으로 옮길 것을 제안한다.
챌린지 때의 내용을 추가하자면, 액션(외부 세계와의 소통)이며 다른 비슷한 라이브러리 또는 대체제로 변경될 때를 위해
'가볍게' 만들어줄 필요가 있다. (다른 말로, 의존 관계를 줄인다)
//useFetchUser
import { useQuery } from '@tanstack/react-query';
import { fetchUser } from './other';
export const useFetchUser = (userId: number) => {
const query = useQuery({
queryKey: ['user', userId],
queryFn: ()=>fetchUser(userId),
gcTime: 1000 * 60 * 60 // 1 hour
staleTime: 1000 * 60 * 60
})
return query;
};
마지막은 useEffect의 관한 부분이다.
프로덕트 관점에서 유지보수를 생각할때 현재의 코드는 더 좋아질 수 있다.
키 값이 변한다던지, 누군가 잘못사용했을때 문제점을 파악하기가 힘들어지는 문제점들을 어떻게 해결해야할까
타입스크립트를 이용하여 더 직관적인 해결책을 이야기한다.
//useTrackPageView
import { useEffect } from 'react';
import { trackEvent } from '@/utils';
interface UserProductsPageEvent {
pageId: "user_product_page";
data: { userId: number };
}
interface ProductPageEvent {
pageId: "product_page";
data: { productId: number };
}
type PageEvent = UserProductsPageEvent | ProductPageEvent;
export const useTrackPageView =(evnet: pageEvent, deps: unknown[]) => {
// Sends analytics event when the page is loaded
useEffect(()=> {
trackEvent('page_view', event);
}, [deps]);
}
1. interface를 통해 혹시 모를 오탈자를 방지한다.
2. pageEvent가 어떤 것인지 직관적이며, 잘못된 props가 넘어오는 것을 방지할 수 있다.
3. 캡슐화되었다.
결과물 & 느낀점
import { useState } from 'react';
import ProductsList from './ProductsList';
import UserProductsFilters from './UserProductFilters';
import UserProductsFiltersButton from './UserProductsFiltersButton';
import useFetchUser from './useFetchUser';
import useFetchProduct from '.useFetchProduct';
import useTrackPageView from './useTrackPage';
export interface Filters {
createdAt?: Date;
}
interface UserProductsPageProps {
userId:number;
}
const UserProductsPage = ({ userId }: UserProductsPageProps) => {
const [filters, setFilters] = useState<Filters>({
createAt: undefined,
});
const { data: user, isLoading: isLoadingUser } = useFetchUser(userId);
const { data: products, isLoading: isLoadingProducts } = useFetchProduct(userId,filters);
useTrackPageView({ pageId: 'user_products_page', data: { userId } }, [userId])
const isLoading = isLoadingUser || isLoadingProducts;
if (isLoading) {
return <div>Loading</div>
}
return (
<div className='tutorial'>
<h1>hello {user!.name}! //'!의 의미는 널이 아님을 컴파일러에게 전달'
<UserProductsFiltersButton onChange={setFilters}
{products && products.length ? (
<ProductsList products={produts} />
) : (
'No Products available.'
)}
</div>
)
}
유튜버 분은 콜러와 콜리의 책임을 명확히하는 것을 통해 치명적인 버그를 피하기 위하는 방법들을 고민해야한다고 한다.
다른 말로 이야기하면 재사용성을 높이기위해, 유지보수를 좋게하기 위한 방법을 고민해야 한다.
많은 방법들 중 지극히 일부분이고, 지극히 초보적인 고민일 수도 있다.
개인적으로는 C언어와 CPP를 공부할때 책임소재 관련 문서들을 봤던 기억도 났고,
일하면서, 프로젝트를 하면서 했던 고민들이, 리팩토링을 통해 사라진 코드들이 생각나기도 했다.
좋은 개발자가 되기위해 내가 갖추고 싶은 것 중 하나가 좋은 결정을 하는 것이다.
많이 고민하고, 많이 쓰다보면 지금보다 더 좋은 결정을 할 수 있을 것이라고 믿는다.
[출처]Refactoring a React component - Design Patterns: https://www.youtube.com/watch?v=PisA-OPisUY
'T.I.L. > Frontend' 카테고리의 다른 글
[깨지면서 배우는 카카오톡 로그인 도입기] (3) | 2024.06.05 |
---|---|
[Refactor]작은 컨텐츠들을 위한 반복작업 (1) | 2024.02.07 |
[Command] Shadcn-ui의 Yarn Command (0) | 2024.02.05 |
[Refactor]아래 글과 이어지는, 단일 책임의 원칙 (4) | 2024.02.02 |
RN 환경설정에서 마주한 문제들 정리 (0) | 2023.06.26 |