지난 글 [React] 리액트를 사용하는 이유 (컴포넌트 분리가 왜 중요할까 ?) 에 이어 제가컴포넌트 분리 기준을 여러 방식으로 했던 경험을 공유하고, 현재는 어떠한 방식으로 컴포넌트를 분리하는 지 작성해보도록 하겠습니다.
컴포넌트란 ?
컴포넌트는 React의 핵심 개념 중 하나입니다.
컴포넌트는 사용자 인터페이스(UI)를 구축하는 기반이 되므로 React 여정을 시작하기에 완벽한 곳입니다!
- 리액트 공식문서-
리액트는 컴포넌트들의 조합으로 UI를 구축합니다. 위 사진처럼 컴포넌트는 트리 구조로 형성되어 있으며, 복잡한 사용자 인터페이스를 작고 관리 가능한 단위로 나누는데 중요한 역할을 합니다.
왜 컴포넌트를 분리해야 할까 ?
지금부터 컴포넌트 분리의 중요성을 컴포넌트 분리라는 것을 모르던 시절의 저의 과거 코드를 보며 설명드리겠습니다.
1. 재사용성 & 유지보수성
function CreateExitButton() {
const navigate = useNavigate();
const navigateToMain = () => {
navigate('/mainpage');
};
return (
<Wrapper>
<Button type="button" onClick={navigateToMain}>
닫기
</Button>
</Wrapper>
);
}
function DestinationButton({ from }: { from: string }) {
const navigate = useNavigate();
const { setDestination } = PotCreateStore();
const navigateToCreateComplete = () => {
if (from === 'create') {
navigate(`/createPotPage`);
} else {
setDestination('');
navigate(`/updatePot/${new URLSearchParams(location.search).get('postId')}`);
}
};
return (
<Wrapper>
<Button type="button" onClick={navigateToCreateComplete}>
도착지로 설정 완료
</Button>
</Wrapper>
);
}
위 두 버튼 모두 동일한 모양의, 비슷한 역할을 하는 버튼입니다. 다른 점이 있다면 버튼 클릭 시 동작하는 함수와 버튼 안에 있는 텍스트가 되겠죠 ?
그럼 우리는 두 가지 생각을 할 수 있습니다.
1. 클릭 함수도 다르고, 버튼 안에 있는 텍스트도 다르니까 페이지에 맞는 버튼을 만들어야지 !
2. 클릭 함수와 텍스트만 다르면 어떻게 공통으로 만들 수 있지 않을까 ?
저의 과거 코드를 보면 1번과 같은 사고를 해서 코드를 작성한 것을 알 수 있습니다. 그렇다면, 1번을 했을 때의 단점이 무엇일까요 ?
컴포넌트를 분리 안 했을 때의 단점
1) 유지 보수
만일 페이지마다 버튼을 모두 만들었을 경우, 갑자기 버튼의 색상이 변경된다거나, 버튼 내의 폰트 사이즈, 색상, 크기 등이 변경되어야 한다면 어떻게 될까요 ?
우리는 아마 모든 페이지의 버튼을 찾아 그 안에 있는 속성 값들을 모두 수정해주어야 할 것입니다. 말 그대로 유지, 보수를 할 경우 불필요한 노동력이 많이 들게 될 것입니다.
심지어 제 코드의 경우 버튼의 이름이 역할마다 모두 다르기 때문에 정말 일일히 찾아 수정해주어야 할 것입니다. 위 캡쳐본만 봐도 Button이라는 이름을 사용하는 컴포넌트가 46개의 파일에 있고, 243개의 결과 값을 가지고 있습니다.
이 모든 것을 수정하려면 .. 정말 끔찍할 것 같네요 🥲
2) 재사용 측면
재사용 측면은 유지보수와 거의 비슷한 내용입니다. 만일 현재의 저라면, 아래와 같이 코드를 작성할 것 같습니다.
우선 공통으로 사용하는 네비게이션 버튼을 하나 만들고
// 공통 버튼 컴포넌트
interface NavigationButtonProps {
children: React.ReactNode;
onClick: () => void;
}
function NavigationButton({ children, onClick }: NavigationButtonProps) {
return (
<Wrapper>
<Button type="button" onClick={onClick}>
{children}
</Button>
</Wrapper>
);
}
아래와 같이 갖다 쓰면 됩니다.
// 수정 전
<Bottom>
<DestinationButton from={from} />
</Bottom>
// 수정 후
<Bottom>
<NavigationButton onClick={goToSearchResults}>도착지로 설정 완료</NavigationButton>
</Bottom>
위와 같이 사용하게 되면 하나의 컴포넌트만을 만들고, 사용할 페이지에서 가져다 쓰기만 하면 됩니다. 이렇게 된다면 버튼의 색, 글자 폰트 등을 수정해야 할 때도 NavigationButton 컴포넌트 하나만 수정하면 됩니다.
따라서 우리는 유지보수성과 재사용성을 고려할 수 있고 더 나아가서 성능까지 생각할 수 있게 됩니다. (성능 얘기는 아래에서)
2. 가독성
컴포넌트를 작게 분리하면, 각 컴포넌트가 담당한 기능과 UI 요소에 집중 할 수 있습니다.
아래와 같은 화면을 구현하기 위한 코드를 보여드리겠습니다.
끔찍한 코드 예시
function Body() {
const { userId } = useMyInfoStore();
const location = useLocation();
const { data } = location.state;
return (
<div
style={{
width: '100vw',
backgroundColor: 'white',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
height: '90vh',
}}
>
<div
style={{
width: width,
marginLeft: '20px',
marginRight: '20px',
backgroundColor: 'white',
}}
>
<S.Title>팟장에게 송금해주세요 !</S.Title>
<S.Content>
팟장이 전체금액 선결제 후, <br /> 파티원들에게 거리별 비용금액을 송금 받을 수 있어요
</S.Content>
<S.Money>
<AccountNumber
onClick={() => {
handleCopyClipBoard(data.account.bankName + data.account.accountNumber);
}}
>
<div style={{ textDecorationLine: 'underline' }}>
{data.account.bankName} {data.account.accountNumber}
</div>
<div
style={{
display: 'flex',
flexDirection: 'row',
gap: '3px',
marginLeft: '1px',
}}
>
<SvgCopyIcon width={10} height={10} />
<CopyText>복사</CopyText>
</div>
</AccountNumber>
<MyPayments>{data.totalAmount}</MyPayments>
<S.PartyOne>
{data.EachAmount.map(
(each: {
userId: number;
amount: number;
name: string;
profileImage: string;
isPartyOwner: boolean;
}) => {
const isMyPayment = each.userId === userId;
return (
<S.PartyOneRow>
<S.MoneyLeft>
{each.isPartyOwner && (
<Icon
style={{
width: '24px',
height: '24px',
backgroundColor: 'none',
position: 'absolute',
marginBottom: '30px',
marginRight: '15px',
}}
>
<PotOwnerCrown width={24} height={24} />
</Icon>
)}
<S.PartyOneImage src={each.profileImage} />
{isMyPayment && <S.PotOwner>나</S.PotOwner>}
<S.PartyOneName>{each.name}</S.PartyOneName>
</S.MoneyLeft>
<S.MoneyRight>
<S.EachMoney
isMyPayment={isMyPayment}
style={{ textDecorationLine: 'none' }}
>
{each.amount}원
</S.EachMoney>
</S.MoneyRight>
</S.PartyOneRow>
);
},
)}
</S.PartyOne>
</S.Money>
<CalaExplain style={{ width: '100%' }}>
<Icon>
<SvgDollar />
</Icon>
최종 금액 총 <CalcExplainMoney> {data.totalAmount} </CalcExplainMoney> / {data.totalPeople}명
</CalaExplain>
</div>
<div
style={{
width: '90%',
height: '50px',
justifyContent: 'center',
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
gap: '10px',
}}
>
<BottomSheetBTN>확인</BottomSheetBTN>
</div>
</div>
);
}
export default Body;
와 .. 정말 난리죠 ? 위 코드는 아래 화면을 구현하기 위한 코드였습니다.. (심지어 헤더는 분리한 상태였습니다)
딱 봐도 가독성이 정말 안 좋아보이죠 ? 제가 짰던 코드지만 읽기 힘들기도 하고, 읽고싶지도 않은 코드입니다 😅
3. 성능 최적화
위 코드의 문제점을 파해쳐보도록 하겠습니다.
1) 컴포넌트 분리가 되지 않아 불필요한 리렌더링이 많이 일어남
현재 코드를 보면, 상태값(ReimbursementList)이 변경될 때마다 Header와 Button, 그리고 정적인 텍스트 모두 동시에 리렌더링 됩니다.
만일 현재의 저라면 아래와 같이 분리할 것 같습니다.
Header: 공통 컴포넌트를 사용하기 때문에 분리
ReimbursementList: 송금하기 데이터를 다루고 리스트로써 렌더링을 위해 분리
Button: 공통 컴포넌트를 사용하기 때문에 분리
function ApplierReimbusement() {
return (
<S.ApplierReimbusementWrapper>
<Header />
<S.Title>팟장에게 송금해주세요 !</S.Title>
<S.Content>
팟장이 전체금액 선결제 후, <br /> 파티원들에게 거리별 비용금액을 송금 받을 수 있어요
</S.Content>
<ReimbursementList />
<BottomButton text="확인" />
</S.ApplierReimbusementWrapper>
);
}
export default ApplierReimbusement;
function ReimbursementList() {
const handleAccountCopy = () => {
handleCopyClipBoard(data.account.bankName + data.account.accountNumber);
};
return (
<S.ApplierReimbursementWrapper>
<S.ApplierReimbursementList>
<S.Money>
<S.AccountNumber onClick={handleAccountCopy}>
<S.BankAccount>
{data.account.bankName} {data.account.accountNumber}
</S.BankAccount>
<S.CopyWrapper>
<SvgCopyIcon width={10} height={10} />
<S.CopyText>복사</S.CopyText>
</S.CopyWrapper>
</S.AccountNumber>
<S.MyPayments>{data.totalAmount}</S.MyPayments>
<S.PartyOne>
<ApplierReimbursementList />
</S.PartyOne>
</S.Money>
<S.CalaExplain>
<S.Icon>
<SvgDollar />
</S.Icon>
최종 금액 총 <S.CalcExplainMoney> {data.totalAmount} </S.CalcExplainMoney> / {data.totalPeople}명
</S.CalaExplain>
</S.ApplierReimbursementList>
</S.ApplierReimbursementWrapper>
);
}
export default ReimbursementList;
위 코드처럼 상태를 담당하는 부분과 아닌 부분을 컴포넌트로 분리함으로써 관심사를 분리할 수 있었습니다.
2) styled-components와 인라인 스타일이 혼재되어있음
'🩵 React' 카테고리의 다른 글
[React] React-Window를 활용해 DOM 성능 최적화 | DOM에 요소 800개가 추가된다고 ..? (0) | 2025.03.12 |
---|---|
[React] 리액트를 사용하는 이유 (컴포넌트 분리가 왜 중요할까 ?) (0) | 2024.11.11 |
[트러블 슈팅] Geolocation API가 비정상적인 데이터를 받아올 때 (0) | 2024.08.26 |
[UI개발] Web에서 사용할 수 있는 BottomSheet를 만들어보자 ! (4) | 2024.07.22 |
[React] Geolocation API의 느린 문제 (2) - 해결방안 (3) | 2024.06.17 |