예? 공용 컴포넌트를 만든다고요?

예? 공용 컴포넌트를 만든다고요?

내가 느낀 공용 컴포넌트 제작의 이상과 현실

·

7 min read


정들다 만 하테나 블로그를 떠나 결국 미루고 미루던 해시노드로 정착을 했는데, 블로그를 옮기고 나서 첫 글을 뭐로 쓸까? 라고 소재에 대한 고민을 하다보니 어언 2월…

월 1회 글쓰기 해보자고 생각했던 만큼, 뭘 쓸지 고민했던 차에 현재 계약직으로 근무하면서 자주 만들고 있는 공용 컴포넌트에 관한 생각들을 좀 풀고 싶어서 이 블로그의 첫 글로 그 내용을 다뤄보기로 했다.

공용 컴포넌트라는 환상의 포켓몬

프론트엔드 개발자분들께 공용 컴포넌트에 대해서 어떻게 생각하는지 여쭤보고 싶은데, 일단 그에 앞서 내가 가지고 있는 환상은 아래와 같았다.

  • 어디든지 가져다 붙일 수 있는 포스트잇 같은 존재

  • 진짜 내가 원하는 대로, 값을 넣으면 넣는대로 생각하는 대로 동작함

  • 누구든지 가져가서 써도 문제가 생기지 않는 존재

근데, 공용 컴포넌트를 한 번이라도 만든 분들이라면 공용 컴포넌트라는 녀석이 그림처럼 만들어지지 않는다는 걸 다들 알 것이다.

혹시 원큐에 만드셨다? 우리는 그런분을 프론트엔드 고수님이라고 부르기로 했어요…^^

아무튼, 실제로 만들면서 느낀 공용 컴포넌트는 아래와 같은 느낌이었다.

  • 어디든지 가져다 붙이니 이슈가 하나 둘씩 생겨남

  • 여기저기 쓰다 보면 그 페이지에 해당되거나 해당되지 않는 예외 사항이 쏟아지기 시작함

  • API가 붙여지기 시작하는 순간, 도메인 별 다른 API의 생태계로 인해 생태계 혼란이 옴

도메인별 API가 통일된 방식으로 보내주거나 사용되는 API가 단일이면 정말 편-안해진다.
근데 여러 도메인에 걸쳐지는 컴포넌트가 되는 순간부터는 더 이상 확장할 일 없게 해달라고 하느님부처님예수님신령님 기도해야 한다. 오, 갓.

거기에 악재를 추가해보자면, 이 공용 컴포넌트를 만드는 사람이 한 사람이 아니라고 생각해보자.
누군가는 이걸 배치하면서 타인이 만든 공용 컴포넌트를 수정해야 하는 상황이 생겨버린다.

내가 원하는 방향성의 코드를 유지한 상태로 타인이 기능을 추가하거나 수정되면 참 좋겠지만, 현실은 언제나 내가 만든 코드가 제 기능을 못하게 되거나 유지 보수가 어려운 형태의 코드로 변질되는 게 많다.

그야말로 마굴, 형언해서는 안 될 존재가 탄생하는 순간이다.

공용 컴포넌트를 만들 때, 이런 점들을 주의하자.

아무튼 그렇게 5개월 넘게 이런 것들을 만들다보니 자연스럽게 향후 만들 때에는 몇 가지를 고려하거나 신경 써서 만들려고 하는 의식점이 몇 가지 생기게 됐다.

여기서는 그 이야기를 좀 적어보고자 한다.

  1. 확장성을 고려하자

위에서 먼저 꺼내긴 했지만, 내가 열심히 간지나게 ‘완벽하게 모두의 조건을 만족할 수 있는 컴포넌트를 만들었어!’ 라고 해도 배치하는 혹은 사용하는 사람이 자신에게 원하는 기능을 배치하고 싶다고 말하는 순간, 그건 더 이상 완벽한 것이 아니다.

그렇기에 공용 컴포넌트를 만들 때에 가장 첫 번째로 고려해야 할 점은 다른 개발자가 해당 컴포넌트를 가져가서 배치하거나 혹은 변형시킬 수 있다는 가능성을 고려해 확장성 있는 컴포넌트를 만들어야 한다.

그렇게 하면 유연하게 확장이 가능해지면서 좀 더 공용스러운 컴포넌트로 활용할 수 있게 된다.

  1. 분기 처리를 잘하자

공용 컴포넌트를 그대로 가져다 만들다 보면 페이지나 기능 전용으로 넣어야 하는 상황이 생길 수 있다.
그 때 쯤이면 사실상 공용 컴포넌트라는 의미가 퇴색되기 시작하는데, 여전히 공용 컴포넌트로서의 역할을 두고 싶다면 이 때부터는 분기 처리의 중요성이 두드러진다.

보통 분기 처리를 한다면 삼항 연산자나 조건문을 통해서 늘어나게 되는데 분기처리가 적으면 다행이지, 점점 늘어나게 된다면 이젠 코드를 스파게티로 만들어버릴 수 있다.

const AigoBungicheori = ({is분기처리}:Props) => {
  if (is분기처리) {
    return action();
  }

  ...

  if (is분기처리) {
    return action2();
  }

  ...

  if (is분기처리) {
    return action3();
  }

  ...

  if (is분기처리) {
    return action4();
  }

  ...

  if (is분기처리) {
    return action5();
  }

  ...

  if (is분기처리) {
    return action6();
  }

  ...

  if (is분기처리) {
    return action7();
  }

  .
  .
  .

  return (<WonBon />)
}

얼마 전에 특정 컴포넌트의 분기 처리를 했을 때, 무턱대고 if 문으로 여기저기 덕지덕지 분기 처리를 했었는데…
추가 수정 요청이 들어온 시점부터는 오히려 이 if문 때문에 뿌린대로 (고통을) 거두는 상황들이 많아져서 과거의 나를 제거하고 싶었다, 젠장. (이건 현재도 진행중이다...^^)

향후에 또 다른 업무로 분기 처리를 해야하는 상황이 있어서, 그 때는 아예 컴포넌트를 그대로 본 따고 특정 페이지나 기능 만을 위한 전용 컴포넌트로 새로 만들었다.
파일이 늘어나는 게 좀 그럴 수 있을지 모르겠지만, 개인적으론 이런 분기 처리는 나쁜 방식이 아닌 것 같다.

const AigoBungicheori = ({is분기처리}:Props) => {
  if (is분기처리) {
    return <Bungicheori />;
  }

  return (<WonBon />)
}

그래서 결국 양자택일인데 분기 처리 내용이 적다면 조건문을 통해서 분기 처리를 하고, 분기 처리가 감당 되지 않는다면 겉잡을 수 없는 부수 효과를 만들어내기 전에 컴포넌트 그 자체로 분기 처리하는 게 답일 수 있다.

  1. API가 엮여있다면 서버 개발자들과의 커뮤니케이션을 잘하자.

이건 API를 붙이는 공용 컴포넌트들에 해당된다.
회사의 규모가 커질 수록 개발자랑 도메인은 점점 늘어나고, 도메인별로 받아오는 양식도 각양각색이 되어버린다.

개인적으로 이런 부분에서 제일 고통스러웠던 건, @tanstack/query를 이용해 무한 스크롤을 만드는 부분이었는데 useInfiniteQuery를 적용해서 어느 API를 받아도 적용되는 공용 컴포넌트를 만들어놨더니 특정 API에서 무한 스크롤이 동작하지 않는다는 이슈를 받았다.

확인 결과, pageParams를 처리하는 부분에서 내가 초기에 참고했던 형태의 API와는 다른 형태로 pageParams를 보내주고 있다는 것을 확인했고, nextPage를 처리하는 부분에서 그 부분을 고려해 분기 처리를 했다.

수정 내용을 push하려던 찰나, 혹시 또 이렇게 분기 처리를 해야 하는 API가 있을까 싶어서 조사해보니 다른 API나 도메인에서도 제각기 다른 형태의 pageParams의 형태를 가지고 있다는 것을 확인해 그 역시 분기 처리를 적용하게 됐다.

어쩌면 향후 계속해서 늘어날지도 모르는 이 상황을 어떻게 타파해야할까?

혼자 내린 답은 두 가지이다.

첫 번째는 서버 개발자분들과 모여서 pageParams를 보낼 때 어떤 식으로 보낼 것인지를 약속을 정하는 것.
다른 하나는 혼자서 그 모든 pageParams를 커버칠 수 있는 유틸을 만들어내는 것이다.

둘 중의 베스트인 건 혼자서 안고 가다가 결국 감당할 수 없는 덩어리를 만드는 것보다는 서버 개발자분들과 소통해 사전에 겉잡을 수 없는 문제를 방지하여 해결하는 거라고 생각한다.

다행히, 아직 그 정도까지의 갈 정도의 양은 아니어서 나는 후자를 택했지만 플래그는 언제든지 회수하게 되는 법이니 사전에 미리 대처들을 해두자.

  1. 읽을 수 있는 코드, 고칠 수 있는 코드를 짜자

어느 배가 있다.
어느 영웅이 서사를 만들고 돌아온 배를 사람들이 기념하기 위해 보존했었는데, 시간이 지나면서 낡고 망가지는 일이 늘어나자 유지 보수를 위해 고치기 시작했다.
그러다 그게 반복되다 보니 어느 순간부터는 원본에 있던 모든 파츠가 존재하지 않게 됐다.

업무를 위해 짠 코드의 시작은 분명 나였을테지만, 유지 보수가 추가되기 시작한 시점부터는 점점 내가 짠 코드가 퇴색되더니 어느 새인가 다양한 사람의 손을 거친 코드를 맞이하게 된다.

그런 다양한 손을 거친 코드가 만약 난잡해서 이해되지 않는다면, 유지 보수가 안정적으로 유지될 수 있을까?
보통 그런 경우 몇몇 사람들은 ‘처음부터 다시 짜죠’라는 말을 할 지도 모른다.
어찌보면 그건 좋은 방법일 수 있겠지만, 다른 의미로 그동안의 코드들이 한순간에 물거품이 되어버리는 거기도 한다.

그렇다면 우리는 공용 컴포넌트 코드를 작성할 때, 어떤 식으로 접근하면서 만들어야 할까.
나는 ‘누군가에게 인수인계를 할 수 있는 코드’를 만들자고 생각하고 짜려고 한다.

몇 달 전에 코드 리뷰를 받을 때, ‘코드 흐름이 안 읽혀서 분석하느라 시간이 걸렸어요.’라는 말씀을 받은 적이 있었다.

그 당시 겹치는 변수명들로 인해 코드가 헷갈린다고 하셨던 건데, 처음에는 와닿지 않았으나 시간이 지나고 다시 내 코드를 보니 내가 봐도 헷갈려하고 있었다.

const SearchOption = {
  option: "numberCode" | "searchWord"
  keyword: string
}

그 당시 나온 예시 중 하나를 가져와봤는데 위와 같이 객체의 변수명인 SearchOption 안에 key name인 option이 있다.

이렇게 보면 되게 와닿지 않을 수 있지만, 컴포넌트 내 기능 구현 코드가 길어지면서 SearchOption.option, SearchOption, { option } 같은 식으로 여기저기서 쓰이기 시작하는 순간 진짜 추적에 혼란이 오기 시작한다.

그제서야 깨닫게 되는 거다, 지금 짠 코드가 망한 코드라는 걸.
그 후부터는 리뷰를 받고 변수명이나 key name에 고민을 했고, 아래와 같이 수정했다.

const SearchOption = {
  condition: "numberCode" | "searchWord"
  keyword: ""
}

SearchOption - condition - keyword, 이렇게 겹치지 않도록 함으로서 조금은 구분이 갈 수 있도록 했다.

또 다른 이야기를 하자면, 가끔 네이밍이 길어지는 것 때문에 button을 btn, option을 opt라고 별다줄을 시전하는 사람들이 있다.
하지만 그렇게 줄여 쓴 단어들은 누군가에게는 혼란을 가중시킬 수 있다, 그 줄임말을 몰라서 말이다.

변수명, 함수명, 메소드, 객체의 key name 등 선언을 하는 것들에는 모두 명확하게 어떤 의미를 가지고 있는지 알 수 있어야 한다.
길어도 괜찮으니까 의미를 알 수 있도록 명칭을 작성하자.

혹시 그럼에도 너무 길어지는 것 때문에 고민이 되는가?
그렇다면 과감하게 ‘한글 변수명’을 사용하는 것도 좋은 방법이라고 생각한다.

// 예를 들자면 이런거다. 아래 영어로 하기엔 긴 내용이 있다면,
const isExampleDivaricationTransactionByBlurBlurBlur = Boolean(aaaaaaaaaaaaaa.bbbbbbbbbbbbbbb.ccccccccccccccc.isExampleDivaricationTransactionByBlurBlurBlur)

// 이럴 때 한글 변수를 한 번 접목시켜보자.
const is분기처리블라블라블라예시 = Boolean(aaaaaaaaaaaaaa.bbbbbbbbbbbbbbb.ccccccccccccccc.isExampleDivaricationTransactionByBlurBlurBlur)

누군가에겐 이 방식이 거부감이 들 수 있겠지만, 너무나도 길어져서 읽기 어려운 명칭보다는 간단명료한 한글 변수명으로 가독성을 높이는 것도 좋은 방법이 될 것이다.

별다줄이든 뭐든 해서 변수명까지 과도한 추상화 같은 짓 하지 말고, 컴포넌트 내 기능을 볼 때 명확한 변수명으로 컴포넌트 내 기능들이 어떤 역할을 하는지, 값은 어떤 값들이 존재하는지를 명확하게 알 수 있도록 해주자.

저마다의 클린 코드에 대한 관점은 다 다르겠지만, 내 기준에서는 이제 누군가가 읽고 쉽게 이해할 수 있다면 그게 클린 코드라고 생각한다.

끝으로,

보통 프론트엔드가 만드는 공용 컴포넌트는 대체적으로 UI에 관련된 게 많다고 생각한다.

보통 그런 UI 컴포넌트를 만들 때, 이제 막 처음으로 그런 걸 만들어보는 사람들은 환상의 꿈을 펼치며 내가 생각하는 완벽한 컴포넌트가 누구든 가져다 써먹을 수 있도록 하는 희망편을 그리고 있을 것이다.

하지만 잊지 마라, 그 UI라도 공용이 붙는 순간 현실과의 괴리감을 서서히 느끼게 될 것이다. 오 쥐.

모두가 잘 쓸 수 있는 공용 컴포넌트를 만든다는 건 진짜 어려운 일이다.
하물며 핸들러나, 유틸리티 함수 같은 것도 그렇다.
게다가 공용 컴포넌트는 코드만 잘 만들어서 완성되는 것이 아니다.

해당 컴포넌트를 사용할 사람, 해당 컴포넌트를 접목시킬 사람이 생겨나는 순간부터 코드에 더해 ‘소통 비용’이 추가로 붙게 되고, 시간이 지나면 유지 보수 비용까지 따따블로 붙게 된다.

그러니 공용 컴포넌트를 만드는 이여, 완벽한 킹갓제네럴엠페러마제스티 만능 공용 컴포넌트 따위를 만들지 마라.
너무 완벽해지려고 하지 말고, 케이스에 따라 적재적소로 만들면서 확장성을 고려해 만들어 나가자.