[리액트] createPortal과 createRoot
카테고리: react
태그: deep dive
🚀주제
포탈을 만든다? 이게 무슨뜻일까?
메이플스토리 히든포탈처럼, 리액트의 숨겨진 두가지 보석, createPortal
과 createRoot
에 대해 알아보도록 하겠다.
🚀createRoot
리액트 공식홈페이지에 가보면 다음과 같은 코드를 볼 수 있다.
npx로 만들어도, vite로 만들어도, RCA로 만들어도 모두 동일한 뿌리를 얻게된다. 많은 분들이 알고있겠지만, React는 본인이 웹에서보여지게 될지, 모바일에서 보여지게될 지 모른다. 우리가 리액트의 마법이라고 생각되는 대부분의 기능들은 사실상 React-Dom 혹은 ReactNative와 같이 렌더링해주는 라이브러리에 들어있는 것이다.
무튼, 통상적으로 앱을 만들 때, 이처럼 React-Dom을 이용해서 만든다. 다음 틀-코드를 한번 살펴보자.
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
위와 같은 방법으로 만드는것을 보았는가? 아마 최근에 리액트를 공부하신 분들은 처음보는 코드일 것이다. (필자의 이야기다)
ReactDom.render()
은 React 16 이전의 기존 렌더링 방식을 사용한다. 이 방식은에서는 옛날 리액트 틀리액트 방식인 callStack Architecture을 이용한 동기식 처리를 통해 렌더링한다. 따라서 작업처리중 userInteraction을 처리하지 못해 사용성이 떨어지게 될 수 있다. 렌더링도 LIFO방식인 것이다.
당연하게도, ReactDom.createRoot.render()
을 이용하면 우리에게 익숙한 fiber Architecture을 이용한 비동기식 처리가 가능하다. react18부터는 createRoot를 사용하지 않으면, 공부하고오라고 화낸다…
무튼 16버전부터 도입된 fiber Architecture가 성숙해졌고, 18버전부터는 createRoot
를 통한 concurrent렌더링이 안정적으로 지원되게 된 것이다. 최근에는 (공홈에 나와있듯)모두가 사용하는 아래와 같은 방법을 쓴다.
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
🚀createPortal
포탈이란 무엇일까? 여기에서 저기로 가는것이다.
사용법은 다음과 같다.
//루트
<html>
<body>
<div id="app-root"></div>
<div id="모달이있을곳"></div>
</body>
</html>
//모달
function myModal(){
return createPortal(
this.props.children, //첫번째인자
document.getElementById("모달이있을곳") //두번째인자
);
}
다음 영상 처럼 정말 모달이있을곳으로 이동시키는 것이다
하지만 공식문서(링크 )에서는 조금 다른 방법을 이용한다
루트에 만들어둔 <div id="모달이있을곳"></div>
에 바로 달아주는게 아니라, “모달이 있을곳”에 child div를 하나 더 만들어 두고 그곳에 createPortal을 사용한다. 코드내용은 위 링크에 codePen과 함께 첨부되어있다. 굳이 굳이 새로운 child를 이용하는 이유가 뭘까?
바로 위처럼 특정 Dom의 child로 들어가는게 위험할 수 있기 때문이다. 다른 모달들이 있을수도 있고, (다른 누군가의 코드로)우리가 예상치 못한 Dom들이 존재할 수도 있는 것이다. 따라서 리액트 공홈에서는 다음과 같은 방식으로 작성한 것이다.
이젠 모달이있을곳(div)에게로의 직접적인 추가가 아니라, 각각의 div children에 추가됨으로 더 안전하게 접근을 할 수 있는 것이다. 이와같은 방식으로 각 모달이 겹치지 않고 제각각의 상태와 이벤트를 처리할 수 있다.
🚀딥다이브
우리는 createRoot와 createPortal에 대해 알아보았다. 그렇다면 위와 같은 api들이 없을때는 어땠을까? 둘의 차이점과 공통점은 무엇일까? 도대체 어떻게 작동하는 것일까? 많은 부분들이 궁금할 것이다. 안궁금 할 수도 있다. 하지만 필자가 궁금했었던 내용들을 공유해보고자 한다.
🚀createPortal 좀더 자세히 보기
다음은 createPortal
내부 구현체이다
export function createPortal(
children: ReactNodeList,
containerInfo: any,
implementation: any,
key: ?string = null,
): ReactPortal {
return {
$$typeof: REACT_PORTAL_TYPE,
key: key == null ? null : '' + key,
children,
containerInfo,
implementation,
};
}
함수는 이처럼 간단하게 되어있다. 렌더링하고자 하는 자식노드를 받아서, ReactPortal객체를 반환한다. (일반적인 react element들처럼 선택적으로 키를 받을 수 있는 모습도 보인다)
라이브러리에서는 생각보다 별게 없었다. 조금 눈에 띄는 점은 Iframe을 사용할 때 createPortal을 사용한다는 것이다.
코드보기(클릭)👇
class IframePortal extends React.Component {
iframeRef = null;
handleRef = ref => {
if (ref !== this.iframeRef) {
this.iframeRef = ref;
if (ref) {
if (ref.contentDocument && this.props.head) {
ref.contentDocument.head.innerHTML = this.props.head;
}
setTimeout(() => {
this.forceUpdate();
});
}
}
};
render() {
const ref = this.iframeRef;
let portal = null;
if (ref && ref.contentDocument) {
portal = ReactDOM.createPortal(
this.props.children,
ref.contentDocument.body
);
}
return (
<div>
<iframe
title="Iframe portal"
style=
ref={this.handleRef}
/>
{portal}
</div>
);
}
}
이처럼 Iframe을 이용하여 외부 컨텐츠들(유튜브, 광고 등)을 삽입할 때, 리액트 내부에서 createPortal을 사용하여 렌더링해준다는 사실을 알 수 있다. 그러면… 이걸 또 어디에 쓸까? 그걸 넘어서 이걸 왜 쓸까? createPortal의 핵심이 무엇인지에 대한 질문으로 이어진다.
필자가 생각하는 createPortal의 핵심은 다음과 같다.
실제 DOM노드에 렌더링하면서도 react컴포넌트 트리에서는 본인의 고향(?)에 남아 본연의 역할을 수행
이게 무슨뜻일까?
<App>
<Header />
<Content>
<Modal /> //위로 올라가고 싶은 모달
</Content>
<Footer />
</App>
<Modal/>
은 컨텐츠 내부에 있지만, createPortal을 이용하여 원하는 위치에 렌더링 시킬 수 있다. 하지만, 비록 모달 컴포넌트가 고향(?)을 떠났을지라도, 여전히 `Contents`의 자식으로 남아있고 이벤트버블링도 Content로 전달된다.
조금 더 압축을 하자면, createPortal은 그저, “렌더링위치만” 변경시켜줄 뿐인 것이다. 이처럼 특정 위치에 있어야 할 컴포넌트를 그저 원하는 위치로 이동
시켜줄 뿐이다. 포탈이 무엇을 옮겨주듯, 그저 렌더링 위치를 옮겨준것일 뿐인 것이다.
🚀createRoot 좀 더 자세히 보기
다음은 createRoote()의 내부 라이브러리를 입맛대로 요약해 본 코드이다.
코드보기(클릭)👇
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
){
// 1. 유효한 컨테이너인지 확인
checkValidContainer(container);
// 2. 옵션을 적용
const {
isStrictMode,
concurrentUpdatesByDefaultOverride,
} = applyOptions(options);
// 3. 컨테이너 생성
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
// 4. 컨테이너와 DOM 연결 및 이벤트 리스너 추가
markContainerAsRoot(root.current, container); //(이제부터 너가 루트임)
const rootContainerElement = getRootContainerElement(container);
// 5. ReactDOMRoot 객체 생성 및 반환
return new ReactDOMRoot(root);
}
그냥 뿌리가 될 부분을 받아서 옵션을 적용해 주는 모습이다. 딱히 인상적이거나 주목할만한점은 없는 것 같다. 굳이 하나를 꼽자면, 코드 내에 “ConcurrentRoot”타입의 컨테이너를 만들어 주는 부분이다.
🚀돔접근
마지막으로 돔접근에 대해 조금 더 살펴보고 글을 마치도록 하겠다.
그렇다면 createPortal이 생기기 전의 돔접근 방법에는 어떤게 있을까?
얼마 전 스크롤링 관련된 함수를 구현할 때, useRef
와 getElementById
중 어떤걸 사용할지 고민했던 기억이 있다.
둘다 DOM요소에 대한 참조를 얻는 방식이지만 조금 다르다. useRef
도 Hook이며 값이 변경되어도 리렌더링이 되지 않는 변수를 저장하기 위해 사용된다. getElementById
는 모두가 아는 DOM을 찾아주는 DOM API이다. 리액트에서는 컴포넌트 내부에서는 useRef를 사용하는 것을 권장하지만, 공식문서의 코드들에서도 getElementById
가 떡하니 들어가 있는 모습을 종종 볼 수 있는것으로 보아, 실 사용에서는 크게 차이가 없지 않을까 싶다.
관련된 틀-메소드를 하나 소개하고 끝내도록 하겠다.
focusInput() {
const inputElement = findDOMNode(this.inputRef.current);
inputElement.focus();
}
findDomNode
라는 메소드는 무려 리액트 0.13v
부터 존재해왔던 메소드다. 지금은 ref를 이용해 완전히 대체할 수 있고, 성능도 구리고 문제도 많기 때문에 권장되지 않는 사용법이라고 한다. 곧 deprecated될 예정이라고 애초에 쓰지 말라고 리액트에서 경고하고 있다. (링크)
아래처럼 직접 돔을 조작 할 수 있다. (돔 선택 후 속성 직접 바꾸기)
옛날 옛적에는 이와 같은 함수들을 이용해 createPortal
을 수동으로 구현했다기도 하지만(카더라 통신), 뭐 이제는 몰라도 되는 메소드일 것 같다.
Dom접근을 할 때에는 권장되는 방식인 useRef
와 권장된다는 말은 없지만 공홈에서도 자주 쓰는 DomAPI들
을 사용해서 접근하면 될 것 같다.
🚀마무리
우리는 리액트의 숨겨진(?)기능 createPortal과 createRoot를 살펴보았다. 프로젝트를 셋업할 때, 외부 컨텐츠들을 볼 때 사용되지만 직접적으로 찾아보고 자주 사용하는 함수들은 아닐 것이다.
하지만 18버전 이후로 안정적으로 concurrent mode를 지원해주는 createRoot
과 컴포넌트 위치를 유지하며 다른곳에 렌더링 시켜줄 수 있는 createPortal
을 알고있다면, 조금 더 나은 코드를 작성하는데 도움이 될 것이다. 남다른 실력자들은 조약돌 속의 보석을 알고있는 사람들이기 때문이다. (사실 영영 쓸일이 없을수도…?)
댓글 남기기