들어가며
웹 애플리케이션을 개발하다 보면 종종 새 창(window)을 열어 작업해야 하는 경우를 마주하게 됩니다.
특히 관리자 페이지나 기업용 웹 애플리케이션에서는 여러 창을 동시에 띄워놓고 작업하는 것이 사용자 경험 측면에서 더 효율적인 경우가 많습니다.
예를 들어 사용자 목록을 보면서 특정 사용자의 정보를 수정하거나, 주문 목록을 확인하면서 개별 주문의 상세 내용을 수정하는 등의 작업이 있죠.
이런 상황에서 중요하게 고려될만한 것은 창 간의 안정적인 데이터 통신입니다.
사용자가 새 창에서 데이터를 수정하고 나면, 그 변경사항이 원래 창에도 자연스럽게 반영되어야 하니까요.
이를 위한 구현 방식으로는 크게 window.opener를 이용하는 방식과 URL 파라미터를 이용하는 방식이 있습니다.
기술 스택
이 글의 예제에서는 다음 기술들을 사용합니다.
- React: UI 구현
- Tanstack Query: 서버 상태 관리
- React Hook Form: 폼 상태 관리
window.opener
window.opener는 브라우저에서 제공하는 기본적인 창 간 통신 방식으로, 현재 창을 열었던 부모 창에 대한 참조를 제공합니다.
이를 통해 자식 창에서 부모 창의 전역 객체나 메서드에 직접 접근할 수 있습니다.
// 예시: 부모 창의 변수에 접근
const openerWindow = window.opener;
const parentData = window.opener.someVariable;
부모 창(UserList.jsx)에서는 다음과 같이 구현합니다.
const handleEditUser = () => {
const url = `${window.location.origin}/users/edit`;
window.selectedUser = selectedUser;
openWindow(url, "Edit User");
};
/users/edit 이라는 경로로 새로운 window를 열도록 했습니다.
현재 window.selectedUser에는 selectedUser라는 데이터도 저장을 해두었구요.
selectedUser는 상태로 관리되는 데이터입니다.
새 창(EditUser.jsx)에서는 다음과 같이 접근합니다.
const {
register,
handleSubmit,
formState: { errors, isDirty },
} = useForm({
defaultValues: window.opener.selectedUser,
});
window.opener는 UserList를 의미하는데요.
이전에 UserList window에서 저장해두었던 selectedUser 데이터를 form의 기본값으로 사용하도록 설정했습니다.
제가 여기서 느꼈던 문제점은 다음과 같았습니다.
먼저, 데이터 동기화 이슈가 있습니다.
새 창에서 데이터를 수정한다고 해도 window.opener.selectedUser 값이 자동으로 업데이트 되지는 않습니다.
또한, 새로고침을 하게 되면 selectedUser 데이터는 상태 데이터이기 때문에 초기화되어 유실됩니다.
물론 이를 방지하기 위한 추가적인 상태 관리 로직을 구현할 수는 있지만, 이는 코드를 더 복잡하게 만들뿐만 아니라 근본적인 해결책이 되지 못합니다.
그리고 추가적으로 찾아보니 window 간에 직접적으로 객체를 공유하는 것은 보안상 위험할 수도 있다는 의견들도 있었습니다.
URL 파라미터
window.opener의 문제점들을 해결하기 위해 다른 방안을 검토하던 중, URL 파라미터를 활용하는 방법을 떠올렸습니다.
새 창을 열 때 사용하는 URL에 필요한 데이터를 파라미터로 전달하고, 이를 기반으로 API를 호출하여 데이터를 받아오는 방식입니다.
예를 들어, 부모 창(UserList.jsx)는 다음과 같습니다.
const handleEditUser = () => {
const url = `${window.location.origin}/users/edit?id=${selectedUser.id}`;
openWindow(url, "Edit User");
};
url에 selectedUser.id URL 파라미터를 추가했습니다.
그리고 새 창(EditUser.jsx)에서는 이 URL 파라미터를 useSearchParams를 통해 전달받습니다.
function EditUser() {
const [searchParams] = useSearchParams();
const userId = searchParams.get('id');
const { data: userData } = useQuery({
queryKey: ['user', userId],
queryFn: () => getUser(userId),
enabled: !!userId,
});
const {
register,
handleSubmit,
formState: { errors, isDirty },
} = useForm({
defaultValues: userData || {},
});
}
전달받은 userId를 이용해서 getUser API 호출을 진행합니다.
이렇게 하면 API 응답으로 받은 userData 값을 폼의 기본값으로 활용할 수 있게 되었습니다.
이전 방식처럼 window 객체를 통해 직접적인 데이터 공유를 피할 수 있고, URL을 통한 상태관리가 가능해졌습니다.
postMessage
URL 파라미터 방식을 사용하면서도 한 가지 해결해야 할 문제가 남아있었습니다.
바로 자식 창에서 데이터가 수정되었을 때, 이를 부모 창에 알려 화면을 갱신하는 것이었죠.
보통 Tanstack Query를 사용하면 데이터 캐싱과 동기화를 효율적으로 관리할 수 있습니다.
invalidateQueries를 통해 데이터가 변경되었을 때 자동으로 최신 데이터를 불러올 수 있죠.
이 갱신의 타이밍을 자식 창이 부모 창에게 알려주어야합니다.
따라서 데이터가 수정되었을 때에는 postMessage를 통해 부모 창에 알림을 보냅니다.
먼저 자식 창에서 부모 창으로 메시지를 보내는 코드를 보겠습니다.
EditUser.jsx의 코드입니다.
function EditUser() {
// ... 이전 코드 유지 ...
const onSubmit = async (data) => {
try {
await updateUser(userId, data);
// 부모 창에 메시지 전달
window.opener.postMessage(
{
type: "USER_UPDATED"
},
window.location.origin
);
// 수정 완료 후 창 닫기
window.close();
} catch (error) {
console.error('Failed to update user:', error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* 폼 필드들 */}
</form>
);
}
데이터를 수정하고 난 후에, window.opener.postMessage를 통해 부모 창에 'USER_UPDATED'라는 메시지를 전달합니다.
아래는 부모 창(UserList.jsx)의 코드입니다.
부모 창에서는 'USER_UPDATED' 메시지를 확인하고, 데이터를 갱신합니다.
function UserList() {
useEffect(() => {
const handleMessage = (event) => {
// 메시지 출처 검증
if (event.origin !== window.location.origin) {
return;
}
// 메시지 타입에 따른 처리
if (event.data.type === "USER_UPDATED") {
queryClient.invalidateQueries(["users"]);
showSuccessToast("사용자 정보가 수정되었습니다.");
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [queryClient]);
// ... 나머지 코드
}
보안을 위해 메시지의 출처(origin)을 검증하는 것이 좋다고합니다.
또한 메시지는 type, payload를 포함한 구조화된 형태로 전달합니다.
맺음말
프로젝트를 진행하면서 새 창과의 데이터 통신 방식으로 window 객체를 직접 전달, 사용하는 방법과, URL 파라미터를 사용하는 방법을 검토했습니다.
처음에는 window.opener를 통해 데이터에 접근하는 방식이 간단해보였지만, 몇 가지 고려사항들이 보였고 이 사항들을 해결하기 위해 URL 파라미터 방식을 택했습니다.
새 창을 열 때 어차피 URL을 통해 페이지를 띄우니, 여기에 필요한 데이터의 ID만 전달하면 어떨까 하는 생각이었죠.
실제 데이터는 API를 통해 가져오면 되니까요.
이를 통해 창 간의 직접적인 데이터 공유를 피하고 URL을 통한 상태 관리가 가능해졌습니다.
결과적으로 초기 구현이 URL 파라미터 방식이 조금 더 복잡할 수는 있지만 보안성 측면에서 장점을 가지고 있고, API호출을 통한 명확한 데이터 흐름을 가질 수 있었다고 생각합니다.
다음에는 이번 경험을 바탕으로 더 좋은 방식을 찾아볼 수 있기를 기대합니다.
글 읽어주셔서 감사합니다! :)
ref: