리액트 컴포넌트들을 특정 방식으로 행동하게 만들기
리액트는 공식문서에서 cloneElement()
, isValidElement()
, 그리고React.Children
이라는 API
들을 소개한다. 리액트로 개발하면서 이것들을 사용할 일은 거의 없지만, 이것들은 종종 유용하고 각종 라이브러리에서 실제로 사용된다. 예를 들어 react-router
에서도 이것들을 사용하고 있다.
먼저 간단한 예로 시작해보자.
이 Codesandbox는 리액트 경험이 적은 지인이 dooboo-ui
컴포넌트 스타일링을 배우는 것을 도와주기 위해 만들어졌다. 원래 Button
컴포넌트는 disabled
와 outlined
라는 prop
을 받고 각 경우에 대해 다른 스타일을 보여준다.
이를 위해서는 button.js
에서 상태관리를 해주거나, App.js
에서 상태를 관리하고 이를 exampleButton
으로 넘겨줘야 할 것이다. 하지만 나는 button.js
에서 스타일링 외 요소들을 최대한 제거하고 싶었고, 이 목적을 cloneElement
를 통해 달성할 수 있었다.
exampleButton
을 cloneElement
의 첫 번째 인자로, disabled
, outlined
를 가진 객체를 두 번째 인자로 넘겼다. 이렇게 생성된 리액트 엘리먼트는 exampleButton
에 disabled
, outlined
prop
이 추가된 것과 같다. 즉, exampleButton
은 disabled
, outlined
에 대해 전혀 신경 쓰지 않아도 된다.
ref
가 유지되느냐에 있다. 관련 내용은 공식문서를 참고하면 좋다. 또, React.createElement의 반환값이 일반 객체이고, 이것을 렌더하는 책임은 렌더러에게 있다는 것을 알고 있으면 React.cloneElement의 이해에 도움이 된다. 관련 내용은 UI 런타임으로서의 리액트를 참고하면 좋다.그런데 이것이 컴포넌트들을 특정 방식으로 행동하게 만들기와 무슨 상관인가? 이것은 별 의미 없는 기교같은 것 아닌가?
꼭 그렇지는 않다. 다음 예시에서 이 글의 제목이 무슨 의미인지 이해할 수 있을 것이다. 최근 나는 다음과 같은 편집 가능한 아이템 목록을 만들고 있었다.
이것은 별로 어려운 작업은 아니었지만, 필드 개수가 조금만 많아져도 코드가 복잡해졌고, 비슷한 작업을 반복하고 있다고 느껴졌다. 예를 들어, 각 줄이 editing
상태에 있는지 그렇지 않은지 관리하려면 useState
을 이용하여 [isEditing, setIsEditing]
을 얻어야 한다.
그런데 줄이 여러 개면 어떨까? 이런 경우 코드 중복이나 복잡도는 재쳐두더라도 네이밍이 어려워진다. 단지 두 줄뿐인 위 사진의 예시에서도, [isEditingSaleInformations, setIsEditingSaleInformations]
, [isEditingStockNumber, setIsEditingStockNumber]
등으로 이름을 짓는 작업은 꽤나 피곤하다. 결정적으로는, 뭔가 좋지 않은 냄새(code smell
)가 난다. 나는 이런 것을 구조 자체를 바꿔야 하는 신호로 받아들인다.
아래 Codesandbox는 Example1
과 Example2
로 이루어져 있다. Example1
는 일반적인 구현이고, Example2
는 Editable
을 정의하여 Example1
을 리팩터링한 것이다.
Editable
의 구현은 간단하다. 각 인스턴스는 자신만의 isEditing
상태를 가지고, 이를 적절히 업데이트하여 children
에게 prop
으로 제공한다. 따라서 상당한 양의 코드 중복이 제거되며, 네이밍 문제에서도 해방된다.
물론 이러한 구현에 언제나 장점만 있는 것은 아니다. 예를 들어 isEditing
상태가 감추어진다는 것이 그것이다. 이는 Editable
에서는 장점이 되지만, 곧 소개할 Selectable
처럼 감추어진 상태 값에 접근해야 할 때는 단점이 될 수 있다. (Selectable
에서 나는 이것을 이벤트 헨들러를 넘기는 것으로 해결했다.)
Editable
은 재미있는 구현이고, 나는 앞으로 비슷한 일을 할 때 이를 고려할 것이다. 그런데 여기서 끝내기는 아쉽다. 나는 종종 거리를 두고 내가 하는 일이 어떤 의미가 있는지 생각해보기를 즐긴다. 구체적인 구현과 추상을 오가는 것이다.
Editable
이 하는 일의 핵심은 무엇일까? 이것이 무엇을 해주기에 내가 편안함을 느낄까? 나는 나름의 대답을 재사용성에 대해 생각하면서 얻었다.
리액트 renderProp
은 렌더링 로직을, hook
은 상태 관련 로직을, Presentational
컴포넌트는 뷰를 재사용 하는 것이다. 그리고, Editable
에서 사용한 패턴은 prop
업데이트 로직을 재사용 하는 것이다.
이것을 이해하면, 더 복잡한 것도 시도해볼 수 있다. 예를 들어 아래 Codesandbox에서 정의한 Selectable
은 prop
업데이트 로직을 singleChoice
와 multipleChoice
라는 추상으로 제공하고 있다.
코드가 Editable
보다 복잡해졌지만, 핵심은 같다. Selectable
안에서 children
에게 어떤 props
를 넘겨줄지 결정하고, 그것을 React.Children
와 React.cloneElement
를 이용해 제공한다.
Selectable
과 Editable
의 차이는 관리하는 상태를 노출하느냐에 있다. Editable
을 사용하는 쪽은 isEditing
값이 필요하지 않을 수도 있지만, Selectable
을 사용하는 쪽은 아마 무슨 아이템이 선택되었는지 알고 싶을 것이다. 이는 Selectable
에 이벤트 헨들러를 넘기는 것으로 얻어진다.
Selectable
은 자신에게 onPress
가 제공되었다면 children
으로 받은 요소들이 pressed
될 때마다 선택된 요소(들)로 onPress
를 호출한다.
앞서 살펴본 예시들은 모두 prop
업데이트 로직을 재사용하는 형태였다. 나는 이것들이 가장 유용한 Use case
라고 생각한다. 하지만 조금 다른 형태로 응용할 수도 있다. 다음 예시는 Submit
했을 때 다음 필드로 넘어가도록 하는 Submittable
인데, 여기서 Submittable
은 prop
업데이트 로직보다는 prop
제공 로직을 재사용하기 위해 사용되었다고 할 수 있다.
Example1
는 일반적인 구현이고, Example2
는 Submittable
을 정의하여 Example1
을 리팩터링한 것이다. form1
에서 엔터를 누르면 form2
로, form2
에서 엔터를 누르면 form3
로 넘어간다.
Submittable
으로 EditText
들을 감싸는 것만으로, 각 필드는 다음 필드와 연결되었다. 각 필드에 올바른 inputRef
와 onSubmitEditing
을 제공하는 로직은 Submittable
에 의해 재사용된다. 따라서 불필요한 반복과 실수에서 벗어날 수 있다.
물론 이러한 접근에는 분명한 한계점들이 있다. 이것은 children
에 대한 암묵적인 가정을 포함하고 있으며, Redux
같은 상태관리 도구를 도입하면 더 쉽게 해결할 수 있는 문제를 어려운 기교로 해결한다는 느낌도 있다. 그렇지만 훌륭한 해결책으로 발전할 가능성이 있고, 결정적으로 재미있다!
실제로 나는 최근 상태관리 도구를 사용하지 않는 회사 프로젝트에서 많은 항목이 있는 편집 가능한 필드들을 Editable
으로 간결하게 해결했다. Selectable
은 dooboo-ui
에 Pull Request
를 보냈다가 해당 프로젝트에서는 크게 유용하지 않은 것 같아 close
했었는데, 이 글을 쓰다 보니 다시 시도해봐야겠다는 생각이 든다.
- 이 글의 예제에 사용된
dooboo-ui
는ios
,Android
,Web
에서 사용 가능한 초기 단계의React Native UI Framework
이다. 관심 있는 이들의 기여를 기다린다. - 이 글에서 소개하는 패턴은
Ruby
의Enumerable mixin
과duck type
의 영향을 받았다.checked
와onPress
가props
에 존재한다면, 그것은Selectable
인 것이다.