❗ (주의) 본 게시글의 내용은 검증되지 않았으며, 틀린 부분이 많을 확률이 높습니다.
... 그래가지고 제 생각에는
root
계층에 모달을 구현할 때React Portal
을 사용하면 될 것 같은데전역 상태
를 사용하고자 한 이유가 있으신가요?
큰일났다.
React Portal
이 뭐였는지 기억이 나지 않았다..
바로 얼마 전에 있었던 일이다..ㅠㅠ
하지만 결국 내가 부족한 것이기에
오늘은 별 다른 사족 없이 React 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
가 있어야 할 것 같지만 그렇지 않다.
우리는 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
공식 Document 을 읽다보니 궁금한 점이 추가적으로 생겼다.
React Portal
의 기본적인 설명 외에 더 아래를 보면 이런 내용이 적혀있다.
portal이 DOM 트리의 어디에도 존재할 수 있다 하더라도 모든 다른 면에서 일반적인 React 자식처럼 동작합니다.
(...중략)
이것에는 이벤트 버블링도 포함되어 있습니다. portal 내부에서 발생한 이벤트는 React 트리에 포함된 상위로 전파될 것입니다. DOM 트리에서는 그 상위가 아니라 하더라도 말입니다. 다음의 HTML 구조를 가정해 봅시다.
어라? 그렇다는 말은 Child
에서 Event bubbling
이 발생했을 때 Parent
에서 감지할 수 있다는 말이다.
그런데 HTML 코드로만 봐서는 그게 어떻게 가능할 수 있는지 이해가 되지 않는다.
이건 어떻게 작동하는걸지도 한번 알아보자.
우리는 지금 궁금한 점이 두 가지 생겼다.
React
가Portal
객체를 만났을 때 다른DOM
에Render
하는 방법- HTML 코드 상 다른 곳에 위치한
DOM
이 기존의 위치에서Event bubbling
되는 방법
이를 알아내는 것을 이번 게시글의 목표로 잡자.
내가 못 찾은걸 수 있지만, React
와 달리 portal
은 구현방식이나 구동방식 등이 잘 정리된 문서나 아티클을 찾을 수 없었다.
나는 그럴때는 일단 소스코드를 열어서 확인해본다.
다행히도 개발이라는 것은 구현방식이 궁금할 때 소스코드를 열어서 확인을 할 수(는) 있다는 것이다.
내가 사용했던 createPortal
은 createPortal$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
코드의 양에 압도되고 말았다.
여기서 나는 이 방식으로 분석하는데에는 한계가 있다고 생각하고, 다시 렌더링 과정부터 정리해보기로 했다.
우선 우리가 일반적으로 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
로 사용하고 있기 때문이다.
라고 생각했는데 막상 구현부를 보니 tag===HostPortal
일 때 동작하는 게 없다..
이후로 더 찾아보았지만 아직 내 수준으로는 도저히 세부 동작 방식을 알아내지 못했다.
똑똑하신 분이 있다면 이 비밀을 꼭 알려주면 좋겠다..
결국 세부 구현 방식은 알아내지 못했지만 대략적인 동작 방식을 정리해 볼 수 있었다.
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
역시 동일한 세팅을 받는 것으로 추측이 가능하다.
결국 실제 어떤 방식으로 Portal
을 구현, 동작하는지는 알게되지 못하고 그저 이렇게 동작하는 것 같네~ 수준에 그치게 되었다.
아무래도 React Fiber
의 전체 동작 방식을 모두 알지 못하기에 더 어려웠던 게 아닐까 싶다. 추후에 더 공부하고 이 주제에 대해 다시 알아보고 싶다.
createPortal
을 하면 일반 element
와 다른 타입의 fiber
를 생성하고 portal
의 root DOM
을 저장한다.commit
단계에서 portal fiber
일 경우 저장되어 있는 root DOM
에 append를 진행한다.event bubbling
의 경우 DOM Tree
와는 별개이므로, 컴포넌트의 DOM Tree
위치와 관계 없이 React
의 render tree
와 동일하게 진행된다.+ 읽어주셔서 감사합니다.
+ 오타, 내용 지적, 피드백을 환영합니다. 많이 해주실 수록 제 성장의 밑거름이 됩니다.
와우 고퀄.. 잘봤습니다