주니어는 기능의 구현에 힘을 쓰고, 시니어는 이후 유지보수와 확장에 힘을 쓴다고 한다.
지난 근 한달간 성장을 위해 배웠던 CS 기초들과 진행했던 포트폴리오 프로젝트, 돌이켜보면 구현을 위해 중요한 것을 놓치고 있었을지도 모른다.
최근에 진행한 아주 간단한 기능을 구현하는 프로젝트 코드를 다른 주니어 개발자 지인분들과 리뷰를 해보았다.
주니어 개발자 분들은 하나같이 성능과 최적화에는 신경을 쓴것이 보이지만, 가독성이 좋지 못했다는 평가를 했다.
1. pages 디렉토리의 의미를 퇴색시키는 컴포넌트의 작성 방식
2. 엔지니어링 관점에서 컴포넌트가 가지는 의미를 퇴색시키는 습관
3. recoil persist 관점으로 보았을때 동적으로 변경되는 키는 의미가 없게됨
4. 레이아웃을 작성한다는 관점에서 남에게 쉽게 레이아웃을 구성하고 유지보수하기 편함을 인식시켜줄만한 작성 기법이 필요함
5. Recoil을 통해 상태를 관리하기 위해 기본적으로 해당 도메인을 이루는 캡슐화와 추상화가 가능하도록 처리 필요함
6. View Logic 내부에 조건 처리가 많을 경우 각 논리식이 무엇을 의미하는지를 나타내기 위해 변수로 만들 필요성이 있어보임
7. 직관적인 코드를 중시해야함 (개발을 못하는 사람도 볼 수 있을 만한 코드, 내부 컴포넌트를 뜯어보지 않고 이해할 수 있을만한)
자신이 작성한 코드는 주관적이기에 문제점을 찾기 힘들다.
그 동안, SOLID 객체 지향 설계는 프론트에 그다지 필요가 없을 것이라는 안일한 생각을 가졌다.
"그래봤자 프론트인데 UI만 보여주면 될 뿐이잖아?"
오만하고 못돼먹은 생각이었다.
유지보수와 확장이 활발히 진행되고, 수 많은 개발자들이 컴포넌트에 관여할 수 있다면?
그동안의 나처럼 컴포넌트를 작성했으면, 분명 건들이기도 싫은 레거시 덩어리가 되었을 것이다. (레거시조차 아까운 표현이다. 정상적인 레거시는 시간이 지남에 따라 병들어가는 코드이기 때문에..)
이러한 이유들 때문에 나는 그 동안 중요한 것을 놓치고 있었던 것일지도 모른다.
지금부터 나는 리액트 컴포넌트 작성에 있어, 가독성이 저조한 코드 조각들을 제시하고, 이를 읽기 쉬운 코드로 리팩토링해보겠다.
기초가 단단한 개발자란 많은 기술을 사용하고 원리를 이해하는 것보다, 바로 이런 것을 말하는 것일지도 모른다.
- 컴포넌트는 최소한의 기능만을 지닌다.
- SRP원칙에 따라 컴포넌트에 추가 기능이 필요할 경우, 수정이 아닌 확장으로 이를 충족시킬 수 있도록 초기에 설계한다.(OCP)
- 사용하지 않는 타입이 발생하지 않도록 인터페이스는 최대한 분리한다.
- 컴포넌트는 그자체로 의존성을 가지지 않는다. 이 말은 서브 컴포넌트의 변경이 부모 컴포넌트에게 영향을 끼쳐서는 안된다는 의미이다.(DIP)
여기 카드가 있다.
추가를 누르면 상품 정보에 수량값이 추가되어야하며, 이후 해당 상품을 빼기위한 버튼이 조건부 렌더링된다.
해당 카드에 대한 코드는 아래와 같다. (개선 전 코드)
<div
css={{
display: 'flex',
flexDirection: 'column',
width: '416px',
height: '240px',
position: 'relative',
}}
>
{/* 프리미엄 상품 유무에 따라 좌측 상단에 라벨을 붙여줍니다.*/}
{isPlus && <label
css={{
fontStyle: 'italic',
fontSize: 20,
fontWeight: 'bold',
color: colors.orange800,
position: 'absolute',
userSelect: 'none',
}}
>
Plus
</label>}
<div
css={{
display: 'flex',
flex: 1,
}}
>
<div
css={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
}}
>
<Text
css={{
fontSize: '80px',
}}
>
{image}
</Text>
</div>
<div
css={{
display: 'flex',
flexDirection: 'column',
flex: 1,
paddingTop: '1rem',
gap: '1rem',
justifyContent: 'space-between',
}}
>
<div
css={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
}}
>
<Text fontSize="md" fontWeight="bold">
{name}
</Text>
<Text>{price.toLocaleString()}원</Text>
<div
css={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}>
<div css={{ display: 'flex' }}>
<ProductAmountTypeLabel color={colors.gray700}>잔량</ProductAmountTypeLabel>
<Text fontWeight="bold">{availbleStock}</Text>
</div>
<div
css={{
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
}}
>
{/* quantity이 1 이상일 경우 조건부로 렌더링합니다. */}
{quantity ? (
<div css={{ display: 'flex' }}>
<ProductAmountTypeLabel color={colors.gray700}>수량</ProductAmountTypeLabel>
<Text fontWeight="bold">{quantity}</Text>
</div>
) : null}
</div>
</div>
</div>
<div css={{
display: 'flex',
justifyContent: 'flex-end',
gap: '0.5rem',
}}>
{/* quantity와 onRemoveFromCart Handler가 truthy 하다면 빼기 버튼을 조건부 렌더링합니다. */}
{quantity && onRemoveFromCart ? (
<Button onClick={removeFromCartHandler}>빼기</Button>
) : null}
{/* onAddToCart Handler가 truthy 하다면 추가 버튼을 조건부 렌더링합니다. */}
{onAddToCart ? (
<Button type={isPlus ? 'prime' : 'primary'} onClick={addToCartHandler} disabled={availbleStock === 0}>
추가
</Button>
) : null}
</div>
</div>
</div>
</div>
어떠한가? 코드가 참으로 더럽기 그지없다.
만약 내가 이 코드를 처음본다면, 어떤 라인이 레이아웃의 어디에 관여하는지 파악하기가 너무 힘들 것이다.
문제는 레이아웃의 컴포넌트 설계 자체에 있다.
보여주어야할 상품 카드를 최소한의 규격으로 나누어보자.
필요한 컴포넌트
최종적으로 상단의 컴포넌트를 활용해서 아래와 같은 JSX를 구성할 것이다.
이러면 훨씬 더 깔끔해진다.
<>
{isPlus && <Absolute left={0} top={0} ml={1} mt={3}>
<Image src={image} />
</Absolute>}
<ListRow
left={
<Flex flex="1" alignItems="center" justifyContent="center">
<Image src={image} />
</Flex>
}
right={
<Flex flex="1">
<List>
<List.Group space={1}>
<ListRow contents={<Text fontSize="md" fontWeight="bold">{name}</Text>} />
<ListRow contents={<Text>{price.toLocaleString()}원</Text>} />
</List.Group>
<Spacing size={4} />
<List.Group space={1}>
<ListRow contents={
<ListRow.Text2Rows
left="잔량"
leftProps={{ color: colors.gray700 }}
right={availbleStock}
rightProps={{ fontWeight: 'bold' }}
/>
}/>
{quantity > 0 && (
<ListRow contents={
<ListRow.Text2Rows
left="수량"
leftProps={{ color: colors.gray700 }}
right={quantity}
rightProps={{ fontWeight: 'bold' }}
/>
}/>
)}
</List.Group>
</List>
<Flex justifyContent="flex-end" mx={1} my={1.5}>
<Button.Group space={2}>
{isShownRemoveButton && <Button>빼기</Button>}
<Button type={isPlus ? 'prime' : 'primary'} onClick={addToCartHandler}>추가</Button>
</Button.Group>
</Flex>
</Flex>
}
/>
</>