
커스템 엘리먼츠
1. 서론
앞서 2편을 통해 웹팩 세팅을 하며 모든 파일을 하나의 js파일로 번들링할 수 있게 되었다.
그래서 마지막 rendering에 관한 로직만 추가하면 마지막 스텝이 마무리가 된다!
기존에 회사에서 사용하던 컴포넌트집합들 중 일부를 하나의 js파일로 번들링하여 외부에서 해당 커스텀엘리먼츠를 선언하고 script태그를 통해 번들링 된 파일을 받아오기만 하면 요구사항을 구현할 수 있게 될 것 같았다.
2. 방식
일단 기본적으로 React는 가상의돔인 ReactDOM을 사용하고 기존 ReactDOM.render()메서드는 지정된 node엘리먼트에 번들링 된 결과물을 연결하게 되는 방식이다. 그래서 기존의 render메서드를 좀 손볼 필요가 있어 여러 자료들을 서치하였다.
좋은 샘플을 발견하였고 구체적인 작동방식 설명이전에 간단한 설명을 크게 세가지 단계로 나뉘어서 이야기할 수 있다.
- 사용할 커스텀태그를 새롭게 document에 만든다
- ReactDOM.render메서드를 활용해 루트컴포넌트를 document에 새로만든 커스텀태그에 가상돔으로 렌더시킨다.
- 외부 html의 script에서 React에 미리 선언된 이벤트를 사용하고 콜백함수로 덮어씌울 수 있어야하므로 dispatchEvent를 사용하였다.
2.1 1단계
- CompProps는 컴포넌트가 가지게 될 props이다
- document.createElement를 통해 document에 tag이름을 가진 커스텀엘리먼트를 미리 만들어둔다
- 기존 컴포넌트에서 사용하는 react portal을 위해 modal이라는 id를 가진 div도 만들어준다.
- nodes라는 변수에 커스텀엘리먼트로 만든(여러번 선언할 수도 있으니까)태그를 가져와서 배열에 담아준다.
- 이어서 2단계!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// custom tags
function render(tag: string, Comp: React.FC<CompProps>) {
document.createElement(tag);
//for modal
const modal = document.createElement("div");
modal.setAttribute("id", "modal");
document.body.appendChild(modal);
//
const nodes: Element[] = Array.from(document.getElementsByTagName(tag));
nodes.map((node, i) => renderNode(tag, Comp, node, i));
return Comp;
}
render("test-home", App);
2.2 2단계
- Array.prototype.slice.call()을 이용하여 node.attributes에 존재하는 객체{key:value}들의 을 카피한 배열로 반환해준다.(이렇게하면 얕은복사가 진행되므로 node에 직접 추가가 가능 해진다.)
- 커스텀앨리먼츠에 선언한 props를 리액트에서 활용하기 위해 props라는 변수에 임의로 {key:value}에 해당하는 것을 선언해주고 node.attributes의 속성으로부터 props로 선언한 값을 가져온다.
- 커스텀엘리먼츠가 class이름으로 받은 이름을 리액트에서 className으로 사용하기 위해 처리를 별도로 해준다.
- 가상돔에 해당 컴포넌트와 props를 렌더하고 1단계에서 만든 커스텀엘리먼트에 렌더가 된다.
- 여기까지하면 커스텀이벤츠는 사용하지 않고 엘리먼트로 필요한 컴포넌트를 렌더할 수 있게된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface IAttrs {
[key: string]: string;
}
interface IProps extends CompProps {
[key: string]: string | undefined;
}
function renderNode(
tag: string,
Comp: React.FC<CompProps>,
node: Element,
i: number
) {
let attrs: IAttrs[] = Array.prototype.slice.call(node.attributes);
let props: IProps = {
key: `${tag}-${i}`,
};
attrs.map((attr) => {
return (props[attr.name] = attr.value);
});
if (!!props.class) {
props.className = props.class;
delete props.class;
}
ReactDOM.render(<Comp {...props} />, node);
}
2.2 3단계
이제 커스텀한 이벤트를 외부에서 필요한 로직으로 활용할 수 있게만 만들어주면 된다.
- js에서 지원하는 CustomeEvent 클래스를 활용해 컴포넌트 내부에서 미리 선언해주고
- 커스텀이벤트에서 props로 받을 id에 해당 이벤트를 걸어준다
- dispatchEvent 메서드를 활용하여 해당 커스텀 이벤트를 컴포넌트 내부에서 필요한 부분에 실행해준다.
- 외부 html에서 해당 이벤트에 eventListener를 걸고 콜백함수로 필요한 로직을 실행하면 된다. 아래는 컴포넌트에서 선언하여 필요한곳에 배치한 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from "react";
import { HomeCompProps } from "../../model/types";
import "../../styles/sass.scss";
const Home: React.FC<HomeCompProps> = (props) => {
const wrapper = document.getElementById(props["id"]);
const newCustomEvent = new CustomEvent("onOpen");
const openModal = () => {
wrapper?.dispatchEvent(newCustomEvent);
console.log(props["id"]);
};
return (
<div className="testWrapper">
<section>
<img
onClick={openModal}
className="img-home"
src="https://"
alt="home-file-loader"
/>
</section>
</div>
);
};
export default Home;
아래는 외부 html에서 필요한 로직을 콜백으로 구현한 것이다.
1
2
3
4
5
6
7
8
9
<body>
<test-home id="test" user-id="test"></test-home>
<script src="https://s3.blahblah.test.js"></script>
<script type="text/javascript" defer>
const dtime = document.getElementById("dtime");
dtime.addEventListener("onOpen", () => alert("index.html script alert")); // 기존 onOpen 실행 event 그대로 유지
</script>
</body>
3. 결론
커스텀엘리먼트를 만들기 위해 다시한번 DOM, Node, VirtualDOM에 대해 많이 찾아보게 된 것 같다. 기존에 회사의 모든 프로젝트들이 SPA로 되어있어 웹팩을 해야지 해야지.. 하던 생각이 있었는데 좋은기회에 기본적인 것을 공부할 수 있게되어 개인적으로는 매우 보람차다!
아무쪼록 회사 내부의 사정이 잘 해결되어 커스텀엘리먼츠를 더욱 활용하며 조금 더 깊은 공부를 할 수 있는 기회가 많아지면 좋겠다 :)