FE | postMessage를 활용해 Cross-Origin 환경에서 안전하게 데이터 전달하기

안녕하세요, 부트텐트 프론트엔드 개발자 코디입니다.
이번 글에서는 부트텐트의 파트너 교육기관 운영 담당자가, 등록 중인 교육과정 공고나, 관리·운영 중인 여러 데이터를 실제 서비스 페이지에서 미리 혹은, 다시 확인할 수 있도록 만드는 과정에서 마주한 문제 해결 경험을 공유해보고자 합니다.
겉보기엔 단순히 “작성 중이거나, 등록 요청을 마친 데이터를 미리 보여주는 기능”처럼 보일 수 있지만, 막상 작업을 시작해보니 서로 다른 도메인(Origin) 간의 안전한 데이터 전달, 보안, 그리고 자연스러운 UX 등 생각보다 다양한 기술적 고민이 필요했습니다.
이 글에서는
- 왜 이런 미리보기 기능이 꼭 필요했는지
- 직접 시도해본 다양한 방법과 그 한계
- 그리고 cross-origin 환경에서 postMessage라는 해법을 찾아가며 문제를 풀어나간 과정까지
자세히 기록해보았습니다. 비슷한 고민을 하거나, “작은 미리보기 기능” 하나에도 숨어 있는 다양한 기술적·운영적 고민이 궁금한 분들께 조금이나마 도움이 되길 바라며 이 회고를 시작합니다.
1. 문제배경: 미리보기 기능이 필요했던 이유
등록 요청 폼에 입력한 텍스트나 이미지 등 각종 데이터가 실제 서비스 화면에서는 어떻게 보일지 미리 확인할 수 있을지에대한 요청이 종종 있습니다. 이 미리보기 기능은 특히 두 가지 상황에서 꼭 필요했습니다.
- 요청 혹은 세팅 폼에 모든 입력을 마친 뒤 실제 서비스 화면에서 어떻게 보일지 즉시 미리 확인하고 싶은 경우
- 교육과정 등록같은 경우, 요청을 한 뒤 검수·반영이 완료되기 전까지 영업일 기준 최대 2일 정도 검수 시간이 소요되는데, 이 기간 동안에도 언제든 요청한 공고(또는 등록한 데이터)의 실제 화면을 미리 확인하고 싶은 경우
지금까지는 이런 요청이 들어올 때마다 운영팀에서 임시로 데이터를 추가 생성하고, 데이터를 복사해 임시로 미리보기 화면을 만들어드리는 수작업을 반복해왔습니다. 이 방식은 매번 테스트 데이터를 복제해야 해서 번거로울 뿐만 아니라, 담당자 입장에서도 직접 테스트·미리보기 환경이 제공되지 않는 점이 불편하다는 피드백이 많았습니다.
그래서 반복되는 운영 수고를 줄이고, 파트너 담당자님들께서 직접 교육과정 등록 내용을 포함한 여러 관리 데이터를 언제든 서비스 화면에서 미리 확인할 수 있는 환경을 만드는 것이 필요하다고 생각하게 되었습니다.
2. 시도와 한계
문제를 해결하기 위해 여러 가지 방법을 먼저 고민해보았습니다.
가장 먼저 떠오른 것은 URL 파라미터를 활용해 특정 데이터(예: 교육과정 공고의 campId)를 넘기는 방식입니다.
예를 들어,
https://boottent.com/preview?campId=abc123
와 같은 형태로 id 값을 넘겨주고, 미리보기(프리뷰) 페이지에서 해당 id로 데이터를 조회해 미리보기 화면을 그려주는 구조입니다.
// (예시)
// 리스트에서 campId로 프리뷰 페이지 이동
window.open(`https://boottent.com/preview?campId=${campId}`);
하지만 이 방식에는 치명적인 단점이 있었습니다. Id가 URL에 그대로 노출되기 때문에, 누구나 id값만 만 알면 임의로 URL을 조작해서 프리뷰 페이지에 접근해 보여지면 안될 데이터(공고 내용 등)에 접근하게된다던가 다른 파트너사의 데이터가 보여질 수 있는 보안 이슈가 있었습니다.
이 문제를 우회하기 위해, secret 쿼리스트링(우리만 아는 약속된 암호/토큰)을 URL에 포함해 암호화해서 넘기는 방안도 고민했습니다.
// (예시)
// 암호화된 secret을 추가로 넘기기
window.open(`https://boottent.com/preview?campId=${campId}&secret=${encrypt(secretKey, campId)}`);
하지만 이 방법 역시 “결국 URL만 알면 누구나 접근 가능”하다는 구조적 한계를 벗어나지 못했습니다. secret 값이 유출되거나, URL이 복사되면 파트너(관리) 페이지를 통하지 않고도 언제든 미리보기 화면을 열 수 있다는 점도 문제였습니다.
보안적인 부분을 고려했을때 아래 두 가지 요구사항이 이번 개발의 중요한 포인트였습니다.
- “누구나 URL만 알면 미리보기를 볼 수 없도록, 반드시 파트너(관리) 페이지를 통해서만 진입하게 만들 것”
- “등록 요청 전·후 모두, 실제 서비스에 노출되는 화면을 그대로 미리보기로 확인할 수 있을 것”
결국, 요구사항을 만족시키지 못해 다시 근본적으로 방법을 고민하게 되었습니다.
3. 고민의 전환: postMessage
예전에 제 개발자 지인과 나눴던 대화가 떠올랐습니다. 그때 그분이 postMessage라는 브라우저 API를 활용해
“창을 여러 개 띄운 서비스에서 서로 다른 도메인 간에도, 민감한 데이터를 안전하게 주고받았던 경험”을 이야기해준 적이 있었죠. 그분이 회사에서도 결제창 같은 외부 팝업 띄울 때, 창 간에 데이터 노출 없이 값을 넘겨줘야 할 일이 많았는데, postMessage를 쓰면 도메인이 달라도, 혹은 새 탭 사이에 데이터를 전달할 수 있었다고 얘기해주신 적이 있습니다.
그때는 대수롭지 않게 넘겼지만, 이번에 프론트엔드 단에서의 비슷한 데이터 전달 문제를 마주하다 보니 “postMessage가 이런 환경에서 데이터를 안전하게 넘기는 데 적절한 방법이 아닐까?” 생각하게 되었습니다.
1) postMessage란?
postMessage는 브라우저에서 서로 다른 윈도우(예: 부모-자식, 새 창, iframe 등) 간에 안전하게 데이터를 주고받을 수 있게 해주는 표준 API입니다.
기본적으로 웹은 “동일 출처 정책(same-origin policy)”에 의해, 다른 도메인/포트/프로토콜을 가진 창이나 탭, iframe 간에는 localStorage, 쿠키, JS 변수 등 직접적인 데이터 공유가 허용되지 않습니다.
하지만 실무에서는
- 결제창, 인증창, 파일 첨부 등 “별도 팝업/새탭”을 띄우고
- 부모 창에서 데이터를 넘겨주거나
- 결과를 자식/팝업 창에서 다시 부모에게 알려줘야 하는 상황
이 있습니다.
이럴 때 window.postMessage()와 수신 측의 window.addEventListener("message", handler) 구조를 이용하면 브라우저가 보안 레이어를 유지하면서도, 명시적으로 안전하게 데이터를 전달할 수 있습니다.
2) postMessage의 주요 특징
- 도메인이 달라도 메시지 전송이 가능함 (cross-origin 지원)
- 반드시 “보낼 곳(targetOrigin)”을 명시해야 함
- 수신 측에서 “누가 보냈는지(event.origin)”를 체크할 수 있음
- 민감한 정보/상태를 노출 없이 창 간에 실시간으로 주고받기 용이
3) postMessage는 어떤 상황에서 유용한가?
- 인증/결제/인증코드 등 “임시 팝업”으로 안전하게 정보 주고받기
- 부모-iframe, 새 탭, 또는 micro frontend 등 다양한 앱 통합 시
- 외부 위젯/내장 서비스와 데이터 연결이 필요할 때
- 도메인이 다른 여러 웹앱이 협업해야 하는 환경
4) postMessage 사용 시 신경써야 할 부분
- event.origin 체크는 필수!
- 타이밍 관리
- 팝업 차단 이슈
- 전달할 데이터의 크기와 민감도
5) postMessage의 한계와 부적합한 상황
- 대용량 데이터 교환에는 부적합
- 장기간 상태 동기화에는 약함
- 동시에 여러 메시지 송수신 관리 어려움
- 브라우저별, 환경별 미묘한 제약
- 한 번만 접근 가능한 보안성 요구 시
4. postMessage가 왜 적합했는가?
이번 교육과정 공고 미리보기 문제는
- “파트너(관리) 페이지”와
- “실제 서비스 미리보기 페이지”가
서로 다른 도메인(cross-origin) 환경에 있다는 점이 가장 큰 특이점이었습니다.
이 때문에
- localStorage, 쿠키, JS 변수 등으로는 아예 데이터를 공유할 수 없었고,
- campId, secret 등 URL 파라미터 기반 데이터 전달은 보안이나 접근통제 측면에서 구조적으로 취약했습니다.
이런 상황에서 postMessage는
- 부모(파트너) → 자식(미리보기) 새 창/탭으로 한 번만, 안전하게 데이터를 직접 전송할 수 있고,
- 수신 창에서는 신뢰할 수 있는 도메인에서 온 메시지만 받도록 체크할 수 있어서,
- 사용자 액션(버튼 클릭) → window.open → 메시지 전송이라는 자연스러운 UX 흐름도 만들 수 있다는 점에서
적합한 해결방법처럼 보였습니다. 특히,
- 프리뷰 페이지가 파트너 페이지를 통해서만 진입하도록 제어할 수 있고,
- 직접 URL로 접근하거나 새로고침해도 데이터가 없으면 화면을 보여주지 않아 보안과 UX 모두를 만족할 수 있었습니다.
5. 실제 구현 과정
이번 교육과정 공고 미리보기 기능의 핵심 구조는
“파트너(관리) 페이지에서 교육과정 데이터를 준비한 뒤, 새 창(미리보기 페이지)으로 안전하게 전달”하는 것이었습니다.
이 과정을 단계별로 어떻게 고민했고, 각각 어떤 코드를 작성했는지 구체적으로 기록해봅니다.
5-1. 전체 데이터 흐름 설계
- 파트너(관리) 페이지에서
- 미리보기(서비스) 페이지에서
5-2. 실제 코드 예시 및 고민
1) 파트너(관리) 페이지에서 데이터 준비와 창 띄우기
// 예시 함수: 미리보기 버튼 클릭 시 실행
const handlePreview = async (campId?: string) => {
// 교육과정 데이터 패칭 (단건 또는 전체)
const snaps = await fetchGetCampSnapshot(campId);
// 실제 배포 시에는 https://boottent.com 으로 맞춤
const TARGET_ORIGIN = "http://localhost:3001";
const previewUrl = `${TARGET_ORIGIN}/preview`;
// 팝업 차단 방지를 위해 반드시 사용자 액션 직후 window.open!
const newWindow = window.open(previewUrl, "_blank");
if (!newWindow) {
alert("팝업이 차단되었습니다! 팝업 허용 후 다시 시도해주세요.");
return;
}
// 타이밍 이슈 방지: 새 창이 완전히 열린 뒤에 postMessage 전달
setTimeout(() => {
newWindow.postMessage(
{ type: "campSnapshot", payload: snaps },
TARGET_ORIGIN // 반드시 타겟 origin을 명시
);
}, 800); // 실제로는 500~1000ms 사이가 안정적
};
- window.open은 사용자 액션(클릭 등)과 완전히 붙여서 실행해야 팝업 차단에 막히지 않습니다.
- 새 창 로딩 전에 postMessage를 너무 빨리 보내면 수신이 안 될 수 있으니, 약간의 delay(setTimeout)도 함께 적용했습니다.
- postMessage의 두 번째 인자는 반드시 “수신자(새 창)의 origin”으로 정확히 맞춰야만 메시지가 정상적으로 전달됩니다.
2) 미리보기 페이지에서 메시지 수신 및 데이터 활용
이번에는 실무에서 여러 종류의 미리보기나 데이터 수신이 필요한 상황에도 쉽게 적용할 수 있도록, 로직을 훅(hook)으로 먼저 분리해 설계했습니다. 메시지 수신, 로딩/타임아웃/보안 체크 등 반복되는 패턴을 usePostMessageData라는 커스텀 훅으로 만들어 재사용성을 높였습니다.
// 공통 메시지 수신 로직 – usePostMessageData 훅
export function usePostMessageData<T>({
expectedOrigin,
messageType,
timeout = 3000,
}: {
expectedOrigin: string;
messageType: string;
timeout?: number;
}) {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [timeoutExpired, setTimeoutExpired] = useState(false);
const handleMessage = useCallback(
(event: MessageEvent) => {
if (event.origin !== expectedOrigin) return;
if (typeof event.data === "object" && event.data?.type === messageType) {
setData(event.data.payload);
setIsLoading(false);
setTimeoutExpired(false);
}
},
[expectedOrigin, messageType],
);
useEffect(() => {
let timeoutId = setTimeout(() => {
setTimeoutExpired(true);
setIsLoading(false);
}, timeout);
window.addEventListener("message", handleMessage as EventListener);
return () => {
clearTimeout(timeoutId);
window.removeEventListener("message", handleMessage as EventListener);
};
}, [handleMessage, timeout]);
return { data, isLoading, timeoutExpired };
}
- expectedOrigin, messageType 등 메시지의 출처와 유형을 인자로 받아, 다양한 미리보기/실시간 데이터 교환 케이스에 손쉽게 확장할 수 있습니다.
- 보안(event.origin 체크), 타임아웃, 상태관리까지 모두 포함되어 있어서, 실질적인 UI 컴포넌트에서는 데이터 활용에만 집중할 수 있게 해줍니다.
export default function CampPreviewPage() {
const { alert } = usePortal();
const hasAlerted = useRef(false);
const {
data: camp,
isLoading,
timeoutExpired,
} = usePostMessageData<CampDetailElement>({
expectedOrigin: PARTNER_ORIGIN,
messageType: CAMP_SNAP_MSG_TYPE
});
useEffect(() => {
if (camp && !hasAlerted.current) {
alert({
title: "알림",
description: (
<div>
<span>요청주신 교육과정이 등록된 모습을 볼 수 있는 미리보기 페이지입니다.<br />
등록요청 폼에 작성해주신 내용들을 확인하실 수 있습니다.</span>
<ul>
<li>본 페이지는 부트텐트 파트너 페이지를 통해 진입하셨을 때에만 확인할 수 있습니다.</li>
<li>새로고침이나 직접 접근 시 미리보기를 볼 수 없습니다.</li>
</ul>
</div>
),
size: "md",
});
hasAlerted.current = true;
}
}, [camp, alert]);
if (isLoading)
return (
<div className="flex w-full flex-col items-center gap-10 py-20">
<span className="text-grey-600">교육과정을 불러오는 중입니다...🔍</span>
<LoadingSpinner type="horizon" />
</div>
);
if (timeoutExpired && !camp) return <NotFound />;
if (!camp) return <NotFound />;
return <CampDetailMain camp={camp} />;
}
- 보안: 훅 내부에서 event.origin을 반드시 체크하므로, 내 서비스에서만 온 데이터만 수신합니다.
- 타임아웃 UX: 메시지가 지정 시간 내에 오지 않으면 자동으로 로딩을 멈추고 404 안내를 띄웁니다.
- 알림(UX): 데이터 수신 후 최초 진입 시 안내 메시지도 한 번만 노출되도록 useRef로 중복 방지 로직도 적용했습니다.
- 코드 유지보수: 여러 종류의 미리보기나 실시간 데이터 교환이 필요한 페이지에서 이 훅만 재사용하면, 비즈니스 로직/UX 설계에만 집중할 수 있다는 점이 큰 장점이었습니다.
이렇게 hook 중심의 설계로 실무에서의 반복 구현 부담을 줄이고, 새로운 미리보기/실시간 데이터 요구에도 빠르고 안정적으로 대응할 수 있게 되었습니다.
6. 회고 및 마무리
이번 교육과정 공고(및 관리 데이터) 미리보기 기능을 개발하며 postMessage라는 브라우저 API를 처음 제대로 활용해보았습니다. 작은 부분까지 신경 쓸 것이 생각보다 많았지만, 덕분에 “한 번만, 안전하게, 필요한 정보만” 교환할 수 있는 구조를 만들 수 있었고, 운영팀의 수작업도 줄어들고, 파트너분들께도 더 나은 미리보기 경험을 제공할 수 있게 되었다고 생각합니다.
특히 이번 경험에서 느낀 가장 큰 보람은 여러 개발자들과의 일상적인 기술 대화, 그리고 커뮤니티에서 무심코 주워 들었던 다양한 경험들이, 실제로 내 현업 문제를 마주했을 때 자연스럽게 떠오르고, 해결책이 되어준다는 점입니다. 앞으로도 다양한 네트워킹 활동을 통해 많은 대화를 나누고, 더 넓은 시야로 문제를 바라보고 해결할 수 있는 개발자로 성장하고 싶다는 다짐을 하게 되었습니다.
마지막으로, 운영의 불편함이나 비효율을 “어쩔 수 없는 일”로 넘기지 않고, 작은 불편이라도 개발적으로 개선할 방법을 고민하고, 실제 효율로 연결하는 경험이 쌓이는 것이 개발자로서 느끼는 가장 큰 성장 포인트라는 점을 다시 한 번 느꼈습니다. 앞으로도 기술과 운영의 경계를 넘나들며 더 안전하고, 편리하며, 모두가 즐겁게 쓸 수 있는 서비스를 만들어가고 싶다는 다짐으로 이 회고를 마무리합니다.
긴글 읽어주셔서 감사합니다.
고민이 있으시거나 대화하고싶으신 분께서는 아래 1:1 오픈채팅 링크로 놀러오세요!
편하게 오셔서 부트캠프고민, 취업고민, 인생고민 같이 나눠봐요.