본문 바로가기

프로젝트 개발일지/TIFY

올바른 추상화와 선언적인 코드 작성하기

코드를 작성하다보면, 어느 단계까지 추상화를 해야할 지 의문이 생길 수 있다.

또, 지금 작성하고 있는 코드가 다른 사람이 봐도 클린 코드라고 할 만큼 선언적인 코드인지, 올바르게 추상화되어있는지 궁금할 수 있다.

이런 의문들을 다는 아니지만, 조금이나마 해결해보기 위해서 글을 써보게 되었다.

 

선언적 프로그래밍 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 함수를 통해서 렌더링시키는구나라는 것을 한 눈에 파악할 수 있게 되었다.

 

이렇게 상황에 따라서 필요한 만큼 추상화를 진행하면 되는데, 주의할 점은 한 파일에 추상화 레벨이 섞여있으면 코드를 파악하기 어려우니, 추상화 레벨을 맞춰서 전체적인 코드를 작성하도록 하자.

 

참고 자료

https://toss.tech/article/frontend-declarative-code

https://velog.io/@remon/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-Feat.-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C

https://iborymagic.tistory.com/73