아니 React portal 도 몰라?

황주현·2023년 3월 1일
69

React

목록 보기
1/1
post-thumbnail
post-custom-banner

❗ (주의) 본 게시글의 내용은 검증되지 않았으며, 틀린 부분이 많을 확률이 높습니다.


들어가며

... 그래가지고 제 생각에는 root 계층에 모달을 구현할 때 React Portal을 사용하면 될 것 같은데 전역 상태를 사용하고자 한 이유가 있으신가요?

큰일났다.
React Portal이 뭐였는지 기억이 나지 않았다..

바로 얼마 전에 있었던 일이다..ㅠㅠ

하지만 결국 내가 부족한 것이기에
오늘은 별 다른 사족 없이 React Portal을 뜯어보고자 한다....

모르는걸 인정하는 것만 중요한 것이 아닌, 알고자 하는 것도 중요하기 때문이다. (ㅠㅠ)



Portal?

솔직히 지나가는 닭이 들어도 뭔가 이동시켜줄 것 같은 기능이다. (goto 같은)
그래도 모르는 걸 아는 것 처럼 넘겨짚고 대답할 수 없었다.

이제는 조금 더 당당해지기 위해 이참에 한번 제대로 알아보자.

기능 간단 요약

아마 나 말고는 다 알고 있을 것 같지만 그래도 간단하게 요약해보자면

부모 컴포넌트 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.page.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가 있어야 할 것 같지만 그렇지 않다.


궁금증

어떻게 다른 DOM에 따로 Render?

우리는 React를 사용할 때 JSX 문법을 사용해 잘 모르지만, 사실 코드는 아래와 같이 정리된다.

// 변환 전
root.render(<h1>hello</h1>);
             
// 변환 후 
root.render(createElement('h1', 'hello'));

createPortal을 한다는 건 아래의 의미를 가질것이다.

root.render(createElement('div', createElement(....중략
    createPortal(createElement('h2','child'), RootDOM))))))));

Reactrenderportal에 대해 어떻게 동작하길래 이런 계층 구조에서 한참 위의 DOM에 렌더하는 걸까?



어떻게 DOM Tree와 다른 event bubbling?

React Portal 공식 Document 을 읽다보니 궁금한 점이 추가적으로 생겼다.
React Portal의 기본적인 설명 외에 더 아래를 보면 이런 내용이 적혀있다.

portal이 DOM 트리의 어디에도 존재할 수 있다 하더라도 모든 다른 면에서 일반적인 React 자식처럼 동작합니다.

(...중략)

이것에는 이벤트 버블링도 포함되어 있습니다. portal 내부에서 발생한 이벤트는 React 트리에 포함된 상위로 전파될 것입니다. DOM 트리에서는 그 상위가 아니라 하더라도 말입니다. 다음의 HTML 구조를 가정해 봅시다.

어라? 그렇다는 말은 Child에서 Event bubbling이 발생했을 때 Parent에서 감지할 수 있다는 말이다.

그런데 HTML 코드로만 봐서는 그게 어떻게 가능할 수 있는지 이해가 되지 않는다.
이건 어떻게 작동하는걸지도 한번 알아보자.


동작 원리

우리는 지금 궁금한 점이 두 가지 생겼다.

  1. ReactPortal 객체를 만났을 때 다른 DOMRender하는 방법
  2. HTML 코드 상 다른 곳에 위치한 DOM이 기존의 위치에서 Event bubbling 되는 방법

이를 알아내는 것을 이번 게시글의 목표로 잡자.

뜯어보자

내가 못 찾은걸 수 있지만, React와 달리 portal은 구현방식이나 구동방식 등이 잘 정리된 문서나 아티클을 찾을 수 없었다.

나는 그럴때는 일단 소스코드를 열어서 확인해본다.
다행히도 개발이라는 것은 구현방식이 궁금할 때 소스코드를 열어서 확인을 할 수(는) 있다는 것이다.

createPortal

내가 사용했던 createPortalcreatePortal$1을 export한 이름이었다. 한번 따라가보자.
(vscode라면 컨트롤 누르고 클릭하면 가진다)


createPortal$1에 도착했다.
아무래도 실제 createPortal의 구현부 라기보다는 처음 선언될 때 validation을 확인하기 위한 함수로 보여진다.

입력받은 container(=target)DOM element가 맞는지 확인을 진행하고 createPortal 함수를 실행시킨다. 그럼 진짜 createPortal로 가보자.


짠~ 잉? 생각보다 별거 없네.. 라고 생각 되는 코드이다.
그도 그럴것이 이 코드는 portal을 동작시키는 부분이 아닌, 생성하는 부분이기 때문이다.

결국 React가 하나하나 렌더링 하는 중 portal 타입의 자식이 존재한다는 것이다.
아까 말한 createElement와 같은 결의 createPortal인게 아닐까 추측된다.

헉 그럼 어떡하죠. 이제 더 못따라가는 거 아닌가요? 😥

자... 한번 함수를 다시 보자.

return 해주는 값 중 $$typeof: REACT_PORTAL_TYPE 이라는 부분이 보인다.
누가봐도 특별해보이는 이 값은, 주석에도 친절히 React Portal을 식별하는 특별한 값이다~ 라고 적혀있다.

세상에 필요없는 코드는 없듯이 식별하는 과정이 없는데 식별자가 있을 이유도 없다.
즉, 어딘가에서는 $$typeof === REACT_PORTAL_TYPE 과 같은 로직이 존재한다는 것이다.

그리고 그 코드가 우리가 지금 궁금해하는 portal의 동작 방식을 가지고 있을 것이라고 예상할 수 있다.

그럼 REACT_PORTAL_TYPE으로 한번 다시 찾아보자.


여기는 아니고...

역시 예상대로 portal 타입을 확인하는 로직이 있다.
함수 이름이 createChild 인 것을 보아 JSX로 받은 값으로 자식들을 만드는 부분으로 보여진다.

그럼 이제 createFiberFromPortal 함수를 볼 차례인데.... Fiber.....

우선 createChild를 호출한 부분을 먼저 찾아보자.
Fiber까지 들어가면 한번에 이해할 범주를 넘어설 것 같다.

이곳이 createChild를 호출한 곳으로 보여지는 reconcileChildrenArray 함수 이다.
한번 호출한 호출부를 보면...

더 알아보고자 이후에 수 많은 함수를 따라가보았지만, 결국 React 코드의 양에 압도되고 말았다.

여기서 나는 이 방식으로 분석하는데에는 한계가 있다고 생각하고, 다시 렌더링 과정부터 정리해보기로 했다.


다시 돌아가서

React Render 에게는 무슨 값이 전달되나?

우선 우리가 일반적으로 render에 값을 전달하는 과정부터 다시 한번 생각해보자.

우리는 render 함수에 JSX를 작성하고, 이는 createElement() 함수로 변환되어 전달된다.

그럼 createElement() 함수는 어떤 return값을 render 함수에 전달해줄까?

주석에도 나와있듯이 ReactElement를 만들어 return해 준다고 작성되어 있다.
그럼 지금까지의 과정을 아래처럼 정리할 수 있다.

이제 render 함수는 ReactElement()를 전달받는다.
그럼 ReactElement()는 어떤 return값을 가질까?

해당 코드는 자체적인 element라는 객체를 생성해 return해주고 있었다.

정리 : render()ReactElement()에서 만든 element를 전달받는다.


createPortal을 다시 한번 생각해보자

그런데 저 element 객체는 어디서 본 듯 한 모양이다.
바로 createPortal() 이다.

보면 아까 식별자로 사용되던 $$typeof의 값이 각각 REACT_ELEMENT_TYPE, REACT_PORTAL_TYPE으로 다른 것이 보여진다.

여기서 element와 달리 portal$$typeof 외에도 containerInfo라는 값이 추가적으로 저장되는게 보여지는데, 이 값은 아까 createPortal에서 getElementById('root') 했던 바로 그 값이다.

즉, portal 타입은 본인이 append 되어야 하는 parent(container)를 따로 가지고 있는 모습을 보인다.


fiber?

이 외에 portal과 관련된 코드를 더 찾아보면 createFiberFromPortal 이라는 함수가 있고, 비슷한 네이밍의 createFiberFromElement가 존재한다.

portal, element에 따라 다른 모양의 fiber를 생성해주는 것이다.

즉, portal 을 가지고 어떤 일이 생기는지 알고 싶다면 fiber의 동작방식을 이해해야 한다는 말이다.


render의 동작방식 (fiber)

react는 렌더링을 진행할 때 fiber라는 것을 사용한다.

지금 reactfiber를 전부 설명하기는 너무 방대하므로, 지금은 간단하게 렌더링 알잘딱깔센 해주는 알고리즘 으로 생각하자.

fiber의 동작방식은 크게 rendercommit 으로 나누어진다.

render

이 단계에서는 node들 중 생성, 삭제, 수정 등의 effect 작업들을 모은다.
이를 토대로 일종의 effect list를 만든 후 commit 단계로 넘어간다.

commit

이 단계에서는 전달받은 effect list를 토대로 실제 dom에 적용한다.
예를 들어 effect삭제면 해당 DOM을 삭제하고, 생성이면 child로 추가하고 하는 식이다.


이 둘 중 우리가 궁금한 부분의 해답은 아마도 commit에 있지 않을까 싶다.


commit 에서 portal을 만나면

commitPlacement라는 함수를 보면 switch caseHostPortal이라는 case가 보여진다.

코드 내용을 보자면 parentFibercontainerInfo값을 가져와 insertOrAppendPlacementNodeIntoContainer라는 함수로 전달한다.

아마도 이 부분이 우리가 궁금해 했던 첫번째 어떻게 다른 root DOM에 직접 추가하지? 로직 부분이라고 추측된다.

기본 컴포넌트로 보여지는 HostComponentparentFiber.stateNodeparent로 사용하는데 반해 parentFiber.stateNode.containerInfo 값을 parent로 사용하고 있기 때문이다.

라고 생각했는데 막상 구현부를 보니 tag===HostPortal일 때 동작하는 게 없다..

이후로 더 찾아보았지만 아직 내 수준으로는 도저히 세부 동작 방식을 알아내지 못했다.
똑똑하신 분이 있다면 이 비밀을 꼭 알려주면 좋겠다..


이벤트 버블링 처리 방법

결국 세부 구현 방식은 알아내지 못했지만 대략적인 동작 방식을 정리해 볼 수 있었다.

Portal은 별도의 portal 타입을 가진 Fiber를 만든다.

그리고 자신이 append되어야 하는 parent DOM을 저장한 후 commit단계에서 저장했던 DOM으로 append 한다 (추정)

그럼 두번째로 궁금했던 event bubbling은 어떻게 진행되는 걸까?
사실 그건 방금 함께 알게 되었다.

portal은 그저 DOM에서만 다른 곳에 append 됐을 뿐, render tree에서는 여전히 코드로 작성했던 위치에 존재할 것이다. (render tree를 따로 조작하는 코드가 없었으니까)

render treedom tree는 같은 것이 아니고, 이벤트 버블링 처리는 render tree를 기준하기 때문에 가능한 것이다.

completeWork() 함수에는 bubbleProperties라는 함수를 호출하는데, 이는 아마도 해당 componentevent bubbling 을 세팅하는 것으로 보인다.

이는 switch case 내 모든 component case에서 호출하기 때문에, 버블링 관련해서는 portal역시 동일한 세팅을 받는 것으로 추측이 가능하다.


정리

결국 실제 어떤 방식으로 Portal을 구현, 동작하는지는 알게되지 못하고 그저 이렇게 동작하는 것 같네~ 수준에 그치게 되었다.

아무래도 React Fiber의 전체 동작 방식을 모두 알지 못하기에 더 어려웠던 게 아닐까 싶다. 추후에 더 공부하고 이 주제에 대해 다시 알아보고 싶다.


  • createPortal을 하면 일반 element와 다른 타입의 fiber를 생성하고 portalroot DOM을 저장한다.
  • commit 단계에서 portal fiber일 경우 저장되어 있는 root DOM에 append를 진행한다.
  • event bubbling의 경우 DOM Tree와는 별개이므로, 컴포넌트의 DOM Tree 위치와 관계 없이 Reactrender tree와 동일하게 진행된다.

+ 읽어주셔서 감사합니다.
+ 오타, 내용 지적, 피드백을 환영합니다. 많이 해주실 수록 제 성장의 밑거름이 됩니다.
profile
반갑습니다. 프론트엔드 개발자 황주현 입니다. 🤗
post-custom-banner

4개의 댓글

comment-user-thumbnail
2023년 3월 9일

와우 고퀄.. 잘봤습니다

1개의 답글
comment-user-thumbnail
2023년 3월 11일

굿

1개의 답글