iframe 과 postMessage로 다른 페이지에 데이터 주고 받기

ChanghyeonO·2024년 12월 6일

앞서

이번에 Next15를 사용해서 신규 어드민 페이지 제작을 시작했는데, 레거시 어드민 페이지에서도 동일한 자료가 필요해서 해당 자료를 별도 페이지로 분리 후 연결해주기로 했다.

먼저 Next 프로젝트를 (이하 신규 어드민) 레거시, 즉 Jquery와 JS로 제작 된 프로젝트 (이하 레거시 어드민) 내에 Next 프로젝트를 연결해주어야 하고, 레거시 어드민 -> 신규 어드민으로 POST 요청에 필요한 body 값을 함께 전달해야 한다.

나는 여기서 iframe과 함께 postMessage를 사용하기로 했다.

레거시 어드민 연결하기

먼저 iframe으로 기존 레거시 어드민 내부에 신규 어드민 자료를 심어보자.

<iframe id="아이디값" src="http://localhost:3000/배포 주소"></iframe>

기존 레거시 페이지에 위 같은 주소를 src로 연결해주었다.
(보안 상 기본 로컬 주소로 대체)

그리고 해당 자료를 연결하기 위한 별도 스크립트 파일을 만들어 해당 html head에 연결해주었고,
스크립트는 하단과 같이 작성했다. (보안 상 민감할 수 있는 정보는 전부 제외하거나 변경)

// URL에서 쿼리 파라미터 추출
const extractQueryParams = () => {
	const searchParams = new URLSearchParams(window.location.search);
	return {
		xxxId: searchParams.get('xxxId'), 
		xxxSeq: searchParams.get('xxxSeq'),
		xxxCode: searchParams.get("xxxCode"),
	};
};

// iframe에 데이터 전달
const initializeIframeCommunication = async () => {
	const params = extractQueryParams();

	// iframe 요소 가져오기
	const iframeElement = document.getElementById('아까 iframe id');
	if (!iframeElement) {
		console.error('iframe을 찾을 수 없습니다');
		return;
	}

	// CHILD_READY 메시지 수신 핸들러
	const handleChildReady = (event) => {
		// 허용된 origin인지 확인
		if (event.origin !== 'http://localhost:3000') return;

		// CHILD_READY 메시지를 받으면 데이터 전송
		if (event.data === 'CHILD_READY') {
			iframeElement.contentWindow.postMessage(params, 'http://localhost:3000');
		}
	};

	// 메시지 수신 이벤트 리스너 등록
	window.addEventListener('message', handleChildReady);

	// iframe 로드 완료 시 로그 출력
	iframeElement.addEventListener('load', () => {
		console.log('iframe 로드 완료, CHILD_READY를 기다리는 중...');
	});

	// 이벤트 리스너 정리
	return () => window.removeEventListener('message', handleChildReady);
};

// DOMContentLoaded 이벤트가 발생하면 초기화 함수 실행
document.addEventListener('DOMContentLoaded', initializeIframeCommunication);

설명을 붙이자면 내가 필요한 정보는 레거시 어드민의 쿼리 파라미터에서 추출했고,
해당 정보를 iframe 요소로 전달한다.

그리고 내가 데이터를 전달하려고 하는 url이 내가 지정한 url인지 확인하는 로직을 추가했고,

iframeElement.contentWindow.postMessage(params, 'http://localhost:3000'); 

를 통해 해당 url로 추출한 쿼리 파라미터 데이터를 전달했다.

그리고 나중에 추가로 설명 하겠지만, iframe 로드가 완료되고 나서 데이터를 보낼 수 있도록,
신규 어드민에서 CHILD_READY 라는 신호를 받아오도록 했다.

위처럼 하면 레거시 어드민 페이지는 준비가 끝났다.

신규 어드민 연결하기

이제 신규 어드민에서 세팅을 해주면 되는데 코드는 하단처럼 작성했다.

  const { params, setParams } = useXxxStore();

  const allowedOrigins = [
    process.env.NEXT_PUBLIC_TEST_LEGACY_ADMIN_URL,
  ];

  const handleMessage = (event: MessageEvent) => {
    if (!allowedOrigins.includes(event.origin)) return;

    if (
      event.data?.xxxId &&
      event.data?.xxxSeq &&
      event.data?.xxxCode &&
    ) {
      setParams(event.data);
    }
  };

  useEffect(() => {
    // 컴포넌트 마운트시 부모에게 준비완료 메세지 전송
    window.parent.postMessage('CHILD_READY', 'http://localhost:8080');

    // 메시지 리스너 등록
    window.addEventListener('message', handleMessage);

    // 클린업
    return () => window.removeEventListener('message', handleMessage);
  }, []);

여기서는 컴포넌트가 로드되면 부모 창 (레거시 어드민) 에 준비 완료(CHILD_READY) 메시지를 보낸다.
그리고 부모 창이 postMessage로 데이터를 보낸 걸 받아오는데, 메세지의 출처가 조건을 만족할 경우 (allowedOrgins를 통한 출처 검증으로 외부 공격 방지) 데이터를 전역 상태로 저장했다.
참고로 신규 어드민에서는 Zustand를 전역상태관리 라이브러리로 사용하고 있다.
그리고 컴포넌트가 언마운트 됐을 경우 메세지 리스너를 정리하여 불필요한 이벤트를 방지해줬다.

결과물은 보여줄 수 없지만,,,, 위처럼 할 경우 정상적으로 연결되는 걸 확인 할 수 있었다.

그리고 여기서 CHILD_READY를 신규 어드민에서 레거시 어드민으로 보내준 이유는 타이밍 문제 때문이었는데,
iframe으로 신규 어드민 로드가 끝나기 전에 postMessage로 데이터 전송을 시도하는 경우가 생겼고, 그럴 경우 신규 어드민에서 해당 데이터를 받아오지 못하는 일이 발생했기 때문이다.
때문에 iframe 페이지 로드가 끝나고 신규 어드민 페이지에서 "나 받을 준비 끝났음 ㅎㅎ" 하고 레거시 페이지로 준비 완료 시그널을 보내주는 방식으로 해결했다.

회고

최근 반복적인 업무가 많아 다소 지루하게 느껴졌는데, 난이도가 높진 않지만 색다른 경험을 해서 무척 즐거웠다. 특히, 이번에 iframe을 사용해본 것이 인상적이었다. 과거 개발을 시작한 지 얼마 안 되었을 때 유튜브를 연결해본 게 전부였는데, 이번 프로젝트에 처음 알게된 개념과 함께 적용을 해본게 신기하고 뿌듯하다.

더 기쁜 건 이번 프로젝트를 통해 실 사용자가 데이터를 비교하는 과정이 훨씬 편리해졌다고 피드백을 받은 것이다. 이런 이야기를 들으니 뿌듯함이 두 배가 됐다. 이런 순간들 때문에 개발을 하는 게 아닐까 싶다. 오늘 또 한 번, 개발자로 일하길 잘했다고 느꼈다.

profile
현 2년차 개발자 -> 기관사로 이직 준비 중

0개의 댓글