가끔 코딩을 하다 보면, 특정 요소를 현재 컴포넌트나 HTML 구조에 속하지 않은 곳에다가 요소를 배치하고 싶은 때가 있다.
예를 들면 모달(modal) 창이라던지, 드롭다운 메뉴 등등, 어떤 요소와 상호작용하는 요소를 문서 흐름에 배치하지 않고 body나 다른 요소에 붙이고 싶은 경우가 있을 수 있겠다.
그럴 때 사용할 수 있는 것이 바로 svelte-portal 되시겠다!
svelte-portal은 위와 같은 상황은 물론, 별도 라이브러리를 사용할 때 HTML 문서를 받도록 되어 있는 라이브러리에서도 사용할 수 있다.
예를 들어 팝업창을 쉽고 이쁘게 만들어 주는 sweetalert2 같은 라이브러리에서 바디 영역의 HTML을 문자열로 작성하거나 HTMLElement 객체를 만들지 않고 별도의 문서 흐름에 작성해 둔 것을 끌어와 쓸 수 있도록 해준다.
약간 svelte의 {#snippet ...} 문법과 쓰임새가 비슷하다.
Svelte-Portal
설치 방법
npm install svelte-portal이번 예제를 위해 sweetalert2를 svelte 코드에 불러와보자. 설치가 되어 있지 않다면 설치하도록 하자.
npm install sweetalert2svelte-portal과 sweetalert2의 불러온다.
<script lang="ts">
import Portal from "svelte-portal";
import Swal from "sweetalert2";
</script>
<main></main>
<style>
</style>간단하게 화면에 팝업창을 띄우는 버튼과 팝업창에 들어갈 내용을 작성해보았다.
...
<main>
<button>이 버튼을 누르면 아래 내용이 팝업창에 뜨게 될 것입니다.</button>
<hr />
<div class="popup_inner">
<p>안녕하세요.</p>
<p>이 내용은 팝업창에 들어갈 내용입니다.</p>
</div>
</main>
<style>
.popup_inner {
background: #0001;
padding: 1em;
border-radius: 8px;
}
</style>그 다음에 버튼을 클릭하면 호출할 함수를 작성하고 안에다 sweetalert2의 Swal.fire() 함수를 작성해 팝업창을 띄우도록 해보자.
...
function buttonAction() {
Swal.fire({
title: "팝업창 제목",
});
}
</script>
<main>
<button onclick={() => buttonAction()}>
이 버튼을 누르면 아래 내용이 팝업창에 뜨게 될 것입니다.
</button>
...버튼을 누르면 팝업창이 잘 나온다.
.popup_inner의 컨텐츠를 팝업창 내부로 옮기는 방법은 두가지가 있다.
1) 셀렉터를 이용하는 방법 (class, id)
셀렉터를 이용해 팝업창으로 내용을 옮기기 위해서는 우선 팝업창이 뜨고 사라지는 여부를 별도로 변수로 처리해줘야 한다.
그 다음에 Swal 내부에서 willOpen() 콜백과 didClose() 콜백에서 각각 변수를 업데이트해줘야 안정적으로 사용할 수 있다.
그리고 customClass 옵션을 추가하고, 내부에 htmlContainer 키의 값을 사용하고자 하는 클래스명으로 넣어준다.
let popupOpen = $state(false);
function buttonAction() {
Swal.fire({
title: "팝업창 제목",
willOpen(popup) { // 팝업창이 표시될 준비가 마쳐진 직후 실행된다.
popupOpen = true;
},
didClose() { // 팝업창이 사라지고 나서 실행된다.
popupOpen = false;
},
customClass: {
htmlContainer: "my-popup"
}
});
}마지막으로, 팝업창 내용으로 삼을 부분을 조건문과 Portal로 감싸준다. Portal에는 target 프롭스를 추가하고 htmlContainer에서 지정해뒀던 클래스명을 셀렉터 형태로 넣어준다.
<main>
<button onclick={() => buttonAction()}>
이 버튼을 누르면 아래 내용이 팝업창에 뜨게 될 것입니다.
</button>
<hr />
{#if popupOpen}
<Portal target=".my-popup">
<div class="popup_inner">
<p>안녕하세요.</p>
<p>이 내용은 팝업창에 들어갈 내용입니다.</p>
</div>
</Portal>
{/if}
</main>그러면 이제 문서 흐름에서는 popupOpen 변수로 걸어둔 조건문에 의해 팝업창 내용은 사라지게 된다.
그리고 버튼을 누르면,
이렇게 팝업창 내용이 실제 팝업 안에 들어가는 것을 볼 수 있다.
2) Swal 팝업 컨테이너 요소를 변수에 담아 사용하는 방법
사실 이 방법이 조금 더 프로그래밍적으로 맞다고 볼 수 있다. 는 느낌이 든다. 1번 방법을 사용하면 모종의 이유로 Portal이 팝업창의 클래스를 찾아내지 못해 오류를 발생시킬 여지가 있기 때문이다.
그래서 이 방법을 사용하면 명시적으로 변수에 컨테이너 요소를 넣어주고 빼줌으로써 그런 문제를 해결한다.
let popup: HTMLElement | undefined | null = $state();
function buttonAction() {
const swalPopup = Swal;
swalPopup.fire({
title: "팝업창 제목",
didClose() {
popup = undefined;
}, // willOpen() 부분과 customClass는 필요 없다.
});
popup = swalPopup.getHtmlContainer();
}
</script>
<main>
...
{#if popup}
<Portal target={popup}>
<div class="popup_inner">
<p>안녕하세요.</p>
<p>이 내용은 팝업창에 들어갈 내용입니다.</p>
</div>
</Portal>
{/if}
</main>
...먼저 팝업 컨테이너를 담아줄 변수를 popup과 같이 만들어 준다. 타입스크립트로 인해 코드가 약간 길어졌는데, 빼고 보면 이렇다.
let popup = $state();그리고 버튼 함수에서는 먼저 Swal 을 별도로 swalPopup과 같이 변수에 할당해야 한다.
그 다음에 swalPopup으로부터 팝업창을 호출한 다음, 바로 swalPopup.getHtmlContainer() 함수(swalPopup = Swal)를 popup 변수에 담아주도록 작성한다.
마지막으로 팝업창 내용이 있는 부분은 1번과 동일하게 조건문으로 감싸주되, popup의 존재 여부로 판단하도록 해주고, target 프롭스는 popup 변수로 지정해주면 된다.
동일하게 팝업창이 잘 나타난다.
마치며
svelte-portal을 잘 활용하면 플로팅 요소를 body에 걸어서 혹여나 부모 요소에 transform이나 overflow CSS 속성이 적용되어 있다고 하더라도 position: fixed 설정이 잘 유지되도록 해줄 수 있다. 그러면서 컴포넌트 내부에서의 바인딩과 상태를 통한 요소 컨트롤도 물론 가능하다.
아직 Svelte가 대한민국에서는 불모지 같은 존재인지라 이것에 대한 내용이 없길래 경력도 없지만 최초로(!) 관련 팁을 작성해 보았다.
앞으로 본인이 Svelte를 사용하면서 배워가는 지식을 꾸준히 올려보려고 하는데 많관부!