섬세한 ISFP의 코드 가독성 개선 경험에서 가독성 향상을 위해 사용한 ObserverComponent을 통해 전역상태관리 라이브러리 의존 낮추기
요즘 개발을 할 때 편리함과 가독성이라는 이유로 recoil을 애용하고 있는데, 끝없이 늘어나는 atom과 리코일 import문을 보면서 전역상태관리 라이브러리에 너무 의존하고 있다는 생각이 들었다. 그러던 중 섬세한 ISFP의 코드 가독성 개선 경험에서 ObserverComponent를 보게 되었는데 영상에서 나온 패턴에 어떤 장점을 가지고 있는지 궁금했기도 했고, 전역상태관리를 어느정도 대체할 수 있을거라 생각되어 직접 구현해보았다.
Observer Component는 이벤트핸들러를 정의한 뒤, 하위 컴포넌트의 Observed Component에서 발생하는 이벤트 버블링을 캡쳐하여 정의한 이벤트핸들러를 실행하는 패턴이다.
여기서 HOC(higher-order-components)는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수이며, 현대 React에서는 주로 사용되지는 않는다고 한다. higherOrderComponent
직접 만들며 느낀 장점은 이벤트 버블링을 이용하기 때문에 props를 전달하지 않아도 된다는 점, 단점은 최상위 컴포넌트에서 이벤트를 정의, 관리 번거로움이 있다는 것이 있었다.
타입스크립트를 사용하였고, 영상에서 모든 코드가 나오지 않기 때문에 임의로 작성/수정한 코드가 많다.
export default function ObserberComponent<P extends JSX.IntrinsicAttributes>(
Component: React.ComponentType<P>,
handleEvent: (event: { [key: string]: string } | string) => void
) {
function Observer(props: P) {
return (
<div
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
const target = ((e.target as HTMLElement)?.closest("[data-click-log]") as HTMLElement) || null;
if (!target) return;
handleEvent(extractParams({ el: target, paramTarget: "data-click-log", paramLabel: "data-click-param" }));
}}
>
<Component {...props} />
</div>
);
}
return Observer;
}
interface ExtractParams {
el: HTMLElement;
paramTarget: string;
paramLabel: string;
depth?: number;
}
/** @description i번째의 부모 엘리먼트까지 paramTarget 있는지 체크한 뒤 paramLabel으로 구별하여 객체를 반환한다. */
const extractParams = ({ el, paramTarget, paramLabel, depth }: ExtractParams): { [key: string]: string } => {
if (!el) return {};
let paramEl = el;
const params = {};
const param = `[${paramTarget}]`;
for (let i = 0; i < (depth || 3); i++) {
const paramsEl = paramEl.closest(param);
if (!paramEl || !paramEl?.parentElement) break;
const targetAttribute = paramsEl?.getAttribute(paramTarget);
const labelAttribute = paramsEl?.getAttribute(paramLabel);
if (!targetAttribute || !labelAttribute) break;
Object.assign(params, { [labelAttribute]: targetAttribute });
paramEl = paramsEl?.parentElement!;
}
return params;
};
const ObservedFirstPage = ObserberComponent(FirstPage, (arg) => console.log(arg));
function App() {
return (
<div>
<ObservedFirstPage />
</div>
);
}
export default App;
export default function FirstPage() {
return (
<div data-click-param="First Page Param" data-click-log="First Page Log">
FirstPage
<button type="button">Fist Page Button</button>
<SecondPage />
</div>
);
}
버튼 클릭 시 { First Page Param ****: "First Page Log" }
console 출력
export default function SecondPage() {
return (
<div data-click-param="Second Page Param" data-click-log="Second Page Log">
SecondPage
<button type="button">Second Page Button</button>
</div>
);
}
버튼 클릭 시 {Second Page Param: 'Second Page Log', First Page Param: 'First Page Log'}
console 출력