2025-03-08
반년 전에 만든 컴포넌트를 오랜만에 확인해보니까 props가 너무 많이 늘어나 있었습니다.
이 정도면 컴포넌트를 가져다 쓸 때 어떤 prop을 써야 할지 굉장히 헷갈릴 것 같은데요…😅
UI 컴포넌트 뿐만 아니라 실무에서도 코드를 보면 굉장히 뚱뚱한 컴포넌트가 흔히 보입니다. (설마 저희 회사만 그런건 아닐거라고 믿습니다…)
도대체 왜 이렇게 뚱뚱한 컴포넌트들이 등장하게 되었을까요?
앞에서 제가 Button 컴포넌트와 Dialog 컴포넌트를 다시 설계해봤는데요.
Button 컴포넌트는 스타일만 다를 뿐 기능은 완전히 동일한 컴포넌트였고, Dialog 컴포넌트는 다양한 모양이 나올 수 있는 컴포넌트였습니다. 둘 다 기본 컴포넌트를 만든 다음 그 컴포넌트를 확장해서 다른 컴포넌트를 만드는 식으로 설계를 했습니다. 이번 Input 컴포넌트도 같은 접근 방식으로 더 깔끔한 컴포넌트로 만들 수 있습니다.
위의 경우 InputProps는 PasswordInput, TextInput, NumberInput 등등 여러 Input 을 하나의 컴포넌트로 관리하려고 했기 때문에 API가 뚱뚱해진 것으로 보입니다. API가 뚱뚱하니까 컴포넌트 내부 로직도 상당히 복잡해질 수 밖에 없습니다. 이렇게 되면 로직에 문제가 생기기도 쉽고, 새로운 요구사항에 대응하기 위해 코드를 수정해야 할 때도 엄청 힘들겠죠. 😭
그렇다면 이 뚱뚱해진 컴포넌트를 어떻게 하면 날씬한 컴포넌트로 개선할 수 있을까요? PasswordInput 을 한번 만들어보겠습니다.
우선, 위에서 봤던 InputProps에 있는 onKeyUp, onKeyDown와 같은 이벤트들은 InputHTMLAttributes 타입을 상속하면 굳이 하나하나 prop을 추가할 필요가 없습니다.
저는 모든 Input 컴포넌트의 기반이 되는 BaseInput 이라는 컴포넌트를 만들었습니다.
이 컴포넌트를 가지고 Input 컴포넌트를 다양하게 확장시키려고 합니다.
PasswordInput도 이 BaseInput을 확장해서 이렇게 만들 수 있습니다.
PasswordInput 컴포넌트는 사용하기도 더 편리합니다. 이제 사용자가 어떤 props를 써야 할지 고민할 필요가 없어졌습니다. 아주 날씬해졌습니다. 😊
컴포넌트가 뚱뚱해지는 원인은 여러 가지 원인이 있겠지만, 대부분의 경우 설계 문제 때문이라고 생각합니다.
제가 컴포넌트를 설계할 때 지키는 것은 딱 3 가지입니다.
하나의 컴포넌트가 책임지는 역할은 하나여야 합니다.
하나의 컴포넌트가 여러가지 역할을 맡게되면 코드가 너무 복잡해지기가 십상입니다.
그렇기 때문에 컴포넌트는 최대한 작게 쪼개는 것이 좋습니다.
InputArea라는 컴포넌트를 한번 보겠습니다.
이 컴포넌트는 InputArea 안에 마우스 포인터가 진입하거나 input에 포커스 되었을 때 border-color를 바꿔주는 역할을 합니다. 그래서 코드를 보면 해당 역할과 연관있는 로직밖에 없습니다.
1번 원칙과 연관된 것이지만 props는 그 컴포넌트에서 필요한 것만 내려줘야 합니다.
한 컴포넌트에서 너무 많은 props를 내려주고 있다면 설계가 잘못되었다는 시그널로 봐도 무방하다고 생각합니다. props를 많이 내려주게 되면 내부 로직이 복잡해질 수 밖에 없고, 그러면 십중팔구 문제가 생기게 되기 때문입니다.
위에서 봤던 InputArea에는 최소한의 props만 있다는 것을 확인할 수 있습니다. props가 적기 때문에 이 컴포넌트가 무슨 역할을 하는 컴포넌트인지도 쉽게 예측할 수 있습니다.
컴포넌트를 설계할 때는 성능적인 부분도 고려해야합니다. 과도한 리렌더링이 이뤄진다면 어플리케이션에서 컴포넌트를 사용했을 때 성능적인 문제를 일으킬 수 있습니다.
InputArea 컴포넌트는 사실 아래와 같이 InputArea 컴포넌트 안에 Input 컴포넌트를 자식 컴포넌트로 추가해도 상관없을 것이라는 생각을 할 수도 있습니다. 하지만 이렇게 되면 마우스 포인터가 InputArea 안을 왔다갔다 할 때마다, 즉, isHovered 상태가 변경될 때마다 Input 컴포넌트가 리렌더링됩니다.
InputArea의 isHovered 상태가 변경되었을 때 해당 컴포넌트만 리렌더링되는 것이 이상적이라고 생각합니다.
저는 그래서 InputArea 컴포넌트 안에 Input 컴포넌트를 추가하지 않고 children으로 감싸주는 식으로 설계했습니다. children은 React에서 특별한 prop이기 때문에, children으로 감싼 자식 컴포넌트는 부모 컴포넌트에서 상태가 변경되어도 리렌더링되지 않습니다! 😊 (자세하게 알고 싶으시면 이 영상을 참고해보세요!)
위의 3가지 규칙만 신경을 써도 충분히 날씬한 컴포넌트를 만들 수 있습니다.
긴 글 끝까지 읽어주셔서 감사합니다.
전체 코드가 궁금하시다면 여기서 확인하실 수 있습니다.
© 2025 Kae All Rights Reserved.