코드를 작성하다보면, 어느 단계까지 추상화를 해야할 지 의문이 생길 수 있다.
또, 지금 작성하고 있는 코드가 다른 사람이 봐도 클린 코드라고 할 만큼 선언적인 코드인지, 올바르게 추상화되어있는지 궁금할 수 있다.
이런 의문들을 다는 아니지만, 조금이나마 해결해보기 위해서 글을 써보게 되었다.
선언적 프로그래밍 vs 명령형 프로그래밍
명령형 프로그래밍은 어떤 일을 어떻게 할 것인가에 관한 것이고, 선언적 프로그래밍은 무엇을 할 것인가에 관한 것이다.
이 문장을 들었을 때 확실하게 와닿았다.
코드로 한 번 살펴보자.
sum([1, 2, 3]);
function sum(nums: number[]) {
let result = 0;
for (const num of nums) {
result += num;
}
return result;
}
첫 번째 코드는 sum 함수인데, 배열을 순회하며 값을 더하는 작업을 추상화해서 나타낸다.
그래서, sum 함수를 이용하는 사람은 내부 구현을 모르더라도 `배열 원소의 합을 구하는 함수` 정도로 이해하고 사용할 수 있게 된다.
이처럼, `어떻게`에 집중하게 되면 두 번째 코드처럼, `무엇을`에 집중하게 되면 첫 번째 코드처럼 작성할 수 있다는 것이다.
물론, 두 번째 코드를 사용하여도 결과는 동일하다.
하지만, 내가 아닌 다른 사람이 코드를 보았을 때, 그리고 다른 사람이 그 코드를 사용할 때, 선언적인 코드를 작성했을 때 이해가 훨씬 잘 될 것이고, 사용하기도 쉬울 것이다.
올바른 추상화
좋은 선언적인 코드를 작성하기 위해서는 추상화 레벨을 높이는 것이 좋다.
하지만 그렇다고 해서 무작정 코드들을 다 추상화해버리면 컴포넌트 깊이가 너무 깊어지기 때문에 적절한 정도의 추상화 레벨을 찾는 것이 중요하다.
올바른 추상화란 다른 사람이 컴포넌트를 선언적으로 사용할 수 있게 하는 것이다.
TIFY 프로젝트를 진행하며 작성한 코드를 한 번 살펴보자.
return (
<>
<FlexBox justify="flex-start" style={{ padding: '16px' }}>
<Text
typo="Caption_12R"
children="생일인 친구"
color="gray_100"
style={{ margin: '0 4px 0 0' }}
/>
<Text typo="Mont_Caption_12M" children={2} color="gray_400" />
</FlexBox>
<Padding size={[0, 16]}>
<FriendsListWrapper>
{birthdayFriendsList.map((friend, index) => (
<FriendsListB
key={index}
name={friend.neighborName}
imageUrl={friend.neighborThumbnail}
currentState={friend.onBoardingStatus}
description="birthday"
birthdayDescription={getDayStatus(
parseDateFromString(friend.neighborBirth),
)}
birthday={parseMonthAndDayFromString(friend.neighborBirth)}
onClick={() => handleClickFriend(friend.neighborId)}
/>
))}
</FriendsListWrapper>
</Padding>
<Spacing height={16} />
</>
)
return (
<>
<FlexBox justify="flex-start" style={{ padding: '16px', width: '360px' }}>
<Text
typo="Caption_12R"
children="생일인 친구"
color="gray_100"
style={{ margin: '0 4px 0 0' }}
/>
<Text
typo="Mont_Caption_12M"
children={birthdayFriendsList.length}
color="gray_400"
/>
</FlexBox>
<Padding size={[0, 16]}>
<FriendsListBItem
friendsList={birthdayFriendsList}
description="birthday"
/>
</Padding>
<Spacing height={16} />
</>
)
}
첫 번째 코드는 처음에 작성했던 코드, 두 번째 코드는 리팩터링 후 코드이다.
가장 큰 차이는 FriendsListBItem 컴포넌트인데, FriendsListB 컴포넌트를 렌더링하는 긴 코드들을 FriendsListBItem이라는 컴포넌트로 추상화하였다.
아래 코드는 FriendsListBItem 컴포넌트 코드이다.
const FriendsListBItem = ({
friendsList,
description,
}: FriendsListBItemProps) => {
const navigate = useNavigate()
const { getDayStatus, parseDateFromString, parseMonthAndDayFromString } =
useGetDate()
const handleClickFriend = (friendId: number) => {
navigate(`/profile/${friendId}`)
}
const renderFriend = (friend: FriendsType) => {
const commonProps = {
key: friend.neighborId,
name: friend.neighborName,
currentState: friend.onBoardingStatus,
imageUrl: friend.neighborThumbnail,
onClick: () => handleClickFriend(friend.neighborId),
}
if (description === 'birthday') {
return (
<FriendsListB
{...commonProps}
description="birthday"
birthdayDescription={getDayStatus(
parseDateFromString(friend.neighborBirth),
)}
birthday={parseMonthAndDayFromString(friend.neighborBirth)}
/>
)
} else {
return (
<FriendsListB
{...commonProps}
description={
new Date(friend.updatedAt).getTime() >
new Date(friend.viewedAt).getTime()
? 'newUpdate'
: 'none'
}
/>
)
}
}
return (
<FriendsListWrapper>
{friendsList.map((friend) => renderFriend(friend))}
</FriendsListWrapper>
)
}
코드만 봐도 차이가 확 느껴지지 않나 싶다.
FriendsListBItem은 props로 받은 친구 목록들을 map 함수를 이용해서 렌더링해주는 컴포넌트인데, 사용처에서는 굳이 내부 구현을 알 필요도 없고, 다른 컴포넌트와의 추상화 정도도 맞지 않으니 리팩터링해주었다.
또한, return 문 안에 props가 많은 FriendsListB를 렌더링하는 것들을 모두 보여줄 필요가 없으니 renderFriend라는 함수로 분리해서 간단하게 나타내도록 추상화하였다.
그 결과 return 문을 보면 딱 friendsList를 map 함수를 통해서 렌더링시키는구나라는 것을 한 눈에 파악할 수 있게 되었다.
이렇게 상황에 따라서 필요한 만큼 추상화를 진행하면 되는데, 주의할 점은 한 파일에 추상화 레벨이 섞여있으면 코드를 파악하기 어려우니, 추상화 레벨을 맞춰서 전체적인 코드를 작성하도록 하자.
참고 자료
'프로젝트 개발일지 > TIFY' 카테고리의 다른 글
스토리북의 문서화 기능을 조금 더 활용해보자 (1) | 2023.10.12 |
---|---|
타입 가드로 해결한 Array.map is not a function 에러 (2) | 2023.10.11 |
함수 반환 타입을 명확히 지정하고 특정 프로퍼티를 가지는 타입도 지정해보자 (0) | 2023.10.05 |
구체적인 타입을 지정하고 재사용성이 있는 코드를 작성하자 (0) | 2023.09.28 |
Storybook을 활용해서 개발해보자 (0) | 2023.09.10 |