부모 컴포넌트 DOM 계층 구조 바깥에 있는 DOM 노드에 자식을 렌더링 하는 기술
// index.tsx
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(<Home/>);
// Portal.tsx
export const Portal = ({children}) => {
const root = document.getElementById('root');
return createPortal(children, root);
};
// Home.tsx
export const Home = () => {
return (
<div>
<Parent />
</div>
);
};
// Parent.tsx
export const Parent = () => {
return (
<div className="parent">
<h1>I'm parent</h1>
<Child />
</div>
);
};
// Child.tsx
export const Child = () => {
return (
<Portal>
<h2>I'm Child</h2>
</Portal>
);
};
이러한 구조의 파일이라고 가정했을 때 실제로 렌더링하면 아래와 같이 보여진다.
렌더링 완료 된 화면 및 HTML.
컴포넌트 구조 상 Parent 아래에 Child가 있어야 할 것 같지만 그렇지 않다.
우리는 React를 사용할 때 JSX 문법을 사용해 잘 모르지만, 사실 코드는 아래와 같이 정리된다.
// 변환 전
root.render(<h1>hello</h1>);
// 변환 후
root.render(createElement('h1', 'hello'));
즉 createPortal을 한다는 건 아래의 의미를 가질것이다.
root.render(createElement('div', createElement(....중략
createPortal(createElement('h2','child'), RootDOM))))))));
React의 render는 portal에 대해 어떻게 동작하길래 이런 계층 구조에서 한참 위의 DOM에 렌더하는 걸까?
React Portal의 기본적인 설명 외에 더 아래를 보면 이런 내용이 적혀있다.
portal이 DOM 트리의 어디에도 존재할 수 있다 하더라도 모든 다른 면에서 일반적인 React 자식처럼 동작합니다.
(...중략)
이것에는 이벤트 버블링도 포함되어 있습니다. portal 내부에서 발생한 이벤트는 React 트리에 포함된 상위
로 전파될 것입니다.
DOM 트리에서는 그 상위가 아니라 하더라도 말입니다. 다음의 HTML 구조를 가정해 봅시다.
그렇다는 말은 Child에서 Event bubbling이 발생했을 때 Parent에서 감지할 수 있다는 말이다.
그런데 HTML 코드로만 봐서는 그게 어떻게 가능할 수 있는지 이해가 되지 않는다.
이건 어떻게 작동하는걸지도 한번 알아보자.
- React가 Portal 객체를 만났을 때 다른 DOM에 Render하는 방법
- HTML 코드 상 다른 곳에 위치한 DOM이 기존의 위치에서 Event bubbling 되는 방법
우선 우리가 일반적으로 render에 값을 전달하는 과정부터 다시 한번 생각해보자.
우리는 render 함수에 JSX를 작성하고, 이는 createElement() 함수로 변환되어 전달된다.
그럼 createElement() 함수는 어떤 return값을 render 함수에 전달해줄까?
주석에도 나와있듯이 ReactElement를 만들어 return해 준다고 작성되어 있다.
그럼 지금까지의 과정을 아래처럼 정리할 수 있다.
이제 render 함수는 ReactElement()를 전달받는다.
그럼 ReactElement()는 어떤 return값을 가질까?
해당 코드는 자체적인 element라는 객체를 생성해 return해주고 있었다.
정리 : render()는 ReactElement()에서 만든 element를 전달받는다.
그런데 저 element 객체는 어디서 본 듯 한 모양이다.
바로 createPortal() 이다.
보면 아까 식별자로 사용되던 $$typeof의 값이 각각 REACT_ELEMENT_TYPE, REACT_PORTAL_TYPE으로 다른 것이 보여진다.
여기서 element와 달리 portal은 $$typeof 외에도 containerInfo라는 값이 추가적으로 저장되는게 보여지는데, 이 값은 아까 createPortal에서 getElementById('root') 했던 바로 그 값이다.
즉, portal 타입은 본인이 append 되어야 하는 parent(container)를 따로 가지고 있는 모습을 보인다.
이 외에 portal과 관련된 코드를 더 찾아보면 createFiberFromPortal 이라는 함수가 있고, 비슷한 네이밍의 createFiberFromElement가 존재한다.
portal, element에 따라 다른 모양의 fiber를 생성해주는 것이다.
즉, portal 을 가지고 어떤 일이 생기는지 알고 싶다면 fiber의 동작방식을 이해해야 한다는 말이다.
react는 렌더링을 진행할 때 fiber라는 것을 사용한다.
지금 react의 fiber를 전부 설명하기는 너무 방대하므로, 지금은 간단하게 렌더링 알잘딱깔센 해주는 알고리즘 으로 생각하자.
fiber의 동작방식은 크게 render와 commit 으로 나누어진다.
이 단계에서는 node들 중 생성, 삭제, 수정 등의 effect 작업들을 모은다.
이를 토대로 일종의 effect list를 만든 후 commit 단계로 넘어간다.
이 단계에서는 전달받은 effect list를 토대로 실제 dom에 적용한다.
예를 들어 effect가 삭제면 해당 DOM을 삭제하고, 생성이면 child로 추가하고 하는 식이다.
이 둘 중 우리가 궁금한 부분의 해답은 아마도 commit에 있지 않을까 싶다.
commitPlacement라는 함수를 보면 switch case 중 HostPortal이라는 case가 보여진다.
코드 내용을 보자면 parentFiber의 containerInfo값을 가져와 insertOrAppendPlacementNodeIntoContainer라는 함수로 전달한다.
아마도 이 부분이 우리가 궁금해 했던 첫번째 어떻게 다른 root DOM에 직접 추가하지? 로직 부분이라고 추측된다.
기본 컴포넌트로 보여지는 HostComponent는 parentFiber.stateNode를parent로 사용하는데 반해 parentFiber.stateNode.containerInfo 값을 parent로 사용하고 있기 때문이다.
Portal은 별도의 portal 타입을 가진 Fiber를 만든다.
그리고 자신이 append되어야 하는 parent DOM을 저장한 후 commit단계에서 저장했던 DOM으로 append 한다 (추정)
그럼 두번째로 궁금했던 event bubbling은 어떻게 진행되는 걸까?
사실 그건 방금 함께 알게 되었다.
portal은 그저 DOM에서만 다른 곳에 append 됐을 뿐, render tree에서는 여전히 코드로 작성했던 위치에 존재할 것이다. (render tree를 따로 조작하는 코드가 없었으니까)
즉 render tree와 dom tree는 같은 것이 아니고, 이벤트 버블링 처리는 render tree를 기준하기 때문에 가능한 것이다.
completeWork() 함수에는 bubbleProperties라는 함수를 호출하는데, 이는 아마도 해당 component의 event bubbling 을 세팅하는 것으로 보인다.
이는 switch case 내 모든 component case에서 호출하기 때문에, 버블링 관련해서는 portal역시 동일한 세팅을 받는 것으로 추측이 가능하다.
createPortal을 하면 일반 element와 다른 타입의 fiber를 생성하고 portal의 root DOM을 저장한다.
commit 단계에서 portal fiber일 경우 저장되어 있는 root DOM에 append를 진행한다.
event bubbling의 경우 DOM Tree와는 별개이므로, 컴포넌트의 DOM Tree 위치와 관계 없이 React의 render tree와 동일하게 진행된다.
출처.
https://velog.io/@himprover/3%EB%85%84%EC%B0%A8%EA%B0%80-React-portal-%EB%AA%A8%EB%A5%B8-%EC%8D%B0