UI 개발자들 사이에서 자주 나오는 이야기 중의 하나가 "구조", "표현", "기능"을 분리하라는 말이 있습니다.
보통 html, css, js와 1:1로 매칭하여 서로 엉겨붙지 않도록 잘 나누라는 의미로 쓰이지만, css 선택자를 html의 태그네임으로 두거나 js의 선택자와 공유하여 어떤 하나의 수정이 다른 부문에 사이드 이펙트를 주고 일파만파 커지는 것을 경계하라...가 이 말의 출처에 가장 가까울 겁니다.
spa 프레임워크에서는 선택자에 대한 고민은 "기능" 면에서는 거의 사라졌고, "표현" 역시 css in js나 tailwind css의 사용이 늘면서 많이 희석되어 현재는 철 지난 화두가 되었으리라 보입니다.
그러나 여전히 html에 css와 js가 들러붙어야 하는 것은 이 언어들의 태생적인 이슈이기에 "구조"면에서는 크게 개선된 부분이 없습니다.
컴포넌트 안에 state가 jsx가 혼연일체가 되어 있거나, 중첩이 10회 이상 돌파하여 저 오른쪽 멀찍이 가버린 경우는 흔한 일이죠.
얼마전 이에 대한 한가지 아이디어가 생겨 아래와 같은 코드를 한번 구성해 보았습니다. "구조 " 컴포넌트를 만들면 "구조"와 "표현"과 "기능"의 관심사를 분리하고 코드를 계층화시킬 수 있을까...하는 호기심에서 말이죠.
<form className={style.container}>
<span>{head}</span>
<div className={style.box-wrap}>
<div className={style.box}>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<div className={style.msg_box}>
{validate_result.map(({msg}) =>
<span>{msg}</span>
)}
</div>
</div>
<div className={style.box}>
<label htmlFor={id}>{text}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<div className={style.msg_box}>
{validate_result.map(({msg}) =>
<span>{msg}</span>
)}
</div>
</div>
</div>
</form>
위의 코드는 이미지의 레이아웃을 만들 때 가장 흔하게 만들 구조를 간단하게 넣어본 것입니다.
이 코드에서 관심사는 state와 validate_result로 대표되는 "입력 필드의 기능"과 이미지대로의 레이아웃을 구성할 수 있도록 하는 "마크업과 style"입니다. 현재는 두 가지가 하나의 코드로 붙어있습니다. 이 코드를 기능 요소만 남기고 다 제거한다면 아래와 같아질 겁니다.
<form>
<span>{head}</span>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<span>{msg}</span>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<span>{msg}</span>
</form>
깔끔하게 보고 싶은 것만 추려서 볼 수 있게 되었지만, 레이아웃을 구성할 트리는 소멸되었습니다. 그런데 하나만 스펠링이 달라지면 코드의 컨텍스트가 달라질 것 같은 기분이 드네요.
<FormComposite>
<span>{head}</span>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<span>{msg}</span>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<span>{msg}</span>
</FormComposite>
form -> FormComposite로 바꾸었습니다. 컴포넌트를 만든 것인데요. 이전의 코드와 비교해본다면 없어진 트리가 FormComposite에 들어가 내재화된 것이 아닐까... 하고 갸우뚱할 수 있을 겁니다. 다만 그렇게 갸우뚱하기에는 흔적이 모자라 보입니다. 역시 무언가는 표현되어 있는 게 직관적인 인지를 위해서 더 좋을 것 같습니다.
<FormComposite>
<span>{head}</span>
<>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<>
<span>{msg}</span>
</>
</>
<>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<>
<span>{msg}</span>
</>
</>
</FormComposite>
이렇게 말이죠. 이렇게 보니 fragment가 마치 트리의 placeholder의 역할을 하는 듯 보이네요. 혹 트리의 관점에서도 요소들을 fragment로 표현하는 것이 가능하지 않을까요. 트리를 복구하며 그렇게 표현해보겠습니다.
<FormComposite
className={style.container}
tree={[
<></>,
<div className={style.box_wrap}>
<>
<></>
<></>
<div className={style.msg_box}>
<></>
</div>
</>
<>
<></>
<></>
<div className={style.msg_box}>
<></>
</div>
</>
</div>,
]}
>
<span>{head}</span>
<>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<>
<span>{msg}</span>
</>
</>
<>
<label htmlFor={id}>{label}</label>
<input type="text" id={id} name={name} value={state} onChange={({target})=>{
set_state(target.value);
set_validate(() => validator(target.value));
}}/>
<>
<span>{msg}</span>
</>
</>
</FormComposite>
정확하게 서로 대칭하는 구조를 가지게 되었습니다. 다만 이러한 방식은 중첩의 수, 구조적인 복잡도가 올라가면 코드를 리딩할 때 꽤나 피로를 안겨줄 것 같습니다. 그래서 이쯤에서 패턴화하는 방법을 적용해보겠습니다.
interface Composite {
theme ?: {[key : string] : HTMLAttributes<HTMLElement>["className"] | Array<HTMLAttributes<HTMLElement>["className"]> }
tree ?: Array<JSX.Element>
children : React.ReactNode
}
// Composite 컴포넌트의 구현 코드는 생략.
const layout = {
field : {
theme : {
msg : style.msg_box
},
tree : [
<key={"label"}></>,
<key={"field"}></>,
<div key={"message"}/>
]
}
fields : {
theme:{
wrap : style.box_wrap
box : [style.box, style.box]
},
tree:[
<key={box1}></>,
<key={box2}></>
]
},
form : {
theme:{
wrap : style.container
fields : style.box_wrap
},
tree:[
<key="head"></>
<div key={"fields"} />
]
}
}
<Composite {...layout.form}>
<span>{head}</span>
<Composite {...layout.fields}>
<Composite {...layout.field}>
...
</Composite>
<Composite {...layout.field}>
...
</Composite>
</Composite>
</Composite>
구조와 관련된 코드는 모두 약식으로 표현되었고, 특정 객체에 정보를 모두 옮겼습니다.
어쩐지 div만큼이나 Composite를 사용할 것 같네요. 그렇지만 이 실험이 무의미해 보이진 않습니다.
const layout = {
form : {
theme:{
wrap : style.container,
fields : style.box_wrap,
wrap : style.box_wrap,
msg : style.msg_box
},
tree:[
<key="head"></>
<Composite key={"fields"} tree={[
<key={box1}></>,
<key={box2}></>
]} />
]
}
}
<Composite {...layout.form}>
<span>{head}</span>
<>
<Field {...layout.field} onChange={....}>
<Field {...layout.field} onChange={....}>
</>
</Composite>
일반적으로 컴포넌트는 기능을 독립적으로 떼어내거나 혹은 그 기능을 나누어주는 역할을 위해 만듭니다. 이 경우엔 기능과는 관련이 없고, 기능만 남겨놓고 나머지를 은닉 또는 분리하는 의도로 만들었습니다. 이런 의도도 컴포넌트가 충분히 반영할 수 있다는 게 이 실험의 가장 유의미한 결과인 것 같네요.