웹뷰 웹-앱 인터페이스 과정 쉽게 이해하기
이 글은 웹-앱 인터페이스에 익숙하지 않은 리액트 개발자를 대상으로 하며, 이해를 돕기 위해 간소화된 예제 코드를 사용하고 있습니다. 실제 사용 환경에서는 일반적으로 더 복잡한 로직과 추상화가 적용되므로, 본 글은 실제 코드 구현 전에 웹-앱 인터페이스의 흐름을 이해하는 데 참고용으로 읽어주시기 바랍니다.
웹뷰를 개발하다보면 웹뷰를 사용할 때 앱과 통신(인터페이스)을 해야 할 필요가 있습니다. 이제 서버와 통신을 하는 방법은 익숙한데, 앱과는 도대체 어떻게 통신하는 걸까요?
웹뷰에서의 웹-앱 인터페이스 과정을 이해할 때, window.navigator.appVersion
혹은 서버-클라이언트와 같은 과정을 생각하신다면 코드가 매우 까다로워 보일 수 있습니다. window.navigator.appVersion
처럼 app.navigator.appVersion
같이 앱의 정보를 직접 가져오면 좋을텐데 말이죠.
이제 axios
, fetch
와 같은 인터페이스 방식은 잠깐 잊고, 웹-앱 통신은 어떻게 해야 하는지 천천히 알아보도록 합시다.
단방향 구조의 웹-앱 인터페이스
웹-앱 인터페이스 과정을 이해하려면 핵심적인 키워드가 있습니다. 바로 WebView는 본질적으로 이벤트 기반의 단방향 통신 구조를 가지고 있다는 것인데요.
fetch로 값을 요청하고 data로 응답을 처리하는 서버-클라이언트 인터페이스와 달리, 웹뷰는 웹에서 앱으로 메시지를 보내면 끝입니다. 반대로 앱에서도 웹으로 메시지를 전달하는 것으로 인터페이스가 끝나게 됩니다. 때문에 웹이나 앱의 요청에 맞춰 이벤트를 실행하기 위해서는 메시지를 받을 준비를 해두어야 합니다.
예시1. 웹뷰에서 앱 설정창 열기
예를 들어 웹뷰에서 앱의 설정창을 열어야 한다고 생각해봅시다. window.open('app-setting')
과 같이 하면 좋겠지만, 웹과 앱은 독립된 환경에서 실행되기 때문에 앱에 직접 접근할 수 없습니다. 따라서 앱에서 설정창을 여는 로직을 구현하고, 특정 메시지가 오면 해당 로직을 실행하도록 해야합니다.
// React Native 기준
import { Linking, Platform } from 'react-native';
const MyWebView = () => {
const onMessage = (event) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === 'OPEN_SETTINGS') {
Linking.openURL('app-settings:');
}
};
앱에서 설정창을 열 준비를 마치고 빌드를 완료했다면, 웹뷰에서는 data.type === OEPN_SETTINGS
를 담은 메시지를 보내주면 됩니다. 이때, string 형식으로밖에 값을 전달할 수 없기 때문에 JSON.stringify를 사용하며, 리액트 네이티브에서도 JSON.parse
와 같은 로직을 거칩니다.
function openAppSettings() {
// iOS용
if (window.webkit?.messageHandlers?.ReactNativeWebView) {
window.webkit.messageHandlers.ReactNativeWebView.postMessage(
JSON.stringify({ type: 'OPEN_SETTINGS' })
);
}
}
축하합니다🎉 이제 웹뷰에서도 앱의 설정창을 열 수 있게 되었습니다. openAppSettings()
함수를 실행하면 앱의 설정창이 열리게 됩니다.
예시2. 스마트폰의 사진을 웹으로 전달하기
이제 앱에서 웹으로 데이터를 보내보도록 합시다. 리뷰 기능의 웹뷰를 개발할 때, 핸드폰의 사진을 웹뷰에서 첨부하는 상황을 생각해봅시다. 이미지를 요청하고 바로 사용하면 좋겠지만, 웹-앱 통신은 단방향 구조를 가지고 있습니다. 따라서 다음과 같은 절차를 거쳐야합니다.
단방향 메시지 구현하는 요청-응답 구조
1️⃣ 앱: 사진을 선택하면 선택한 사진을 웹으로 보내도록 하는 "REQUEST_PICTURE" 로직 등록
2️⃣ 웹 → 앱: type === "REQUEST_PICTURE" 메시지를 전달하여 사진을 요청
3️⃣ 앱: 메시지를 받으면 사진 선택기(갤러리/카메라)를 열어 사용자가 사진을 선택
4️⃣ 웹: 앱에서 보내는 메시지를 받을 수 있도록 event listener를 등록해두고 대기
5️⃣ 앱 → 웹: 선택된 사진을 base64 문자열로 변환하여 웹으로 전달
6️⃣ 웹: 받은 메시지의 type === "PICTURE"일 경우 base64 → Blob으로 변환하여 리뷰 첨부에 사용
// 리액트(웹)
export default function ReviewForm() {
const handleFileSelect = () => { // 2️⃣
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "REQUEST_PICTURE" }));
}
React.useEffect(() => {
const listener = (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data.type === "PICTURE") { // 6️⃣
console.log("📷 이미지 도착:", data.payload); // base64 string
// base64 문자열로 전달받은 데이터를 Blob 객체로 변환하여 FormData 로 업로드
}
};
// 앱의 메시지를 받기 위한 이벤트 리스너 등록 4️⃣
window.addEventListener("message", listener);
return () => window.removeEventListener("message", listener);
}, []);
// 리액트 네이티브(앱)
export default function ReviewWebView() {
const webviewRef = useRef(null);
const handleMessage = async (event) => {
const { data } = event.nativeEvent;
const message = JSON.parse(data);
if (message.type === "REQUEST_PICTURE") { // 3️⃣
const result = await ImagePicker.launchImageLibraryAsync({ // 5️⃣
base64: true,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
});
const base64 = result.base64;
// 웹뷰에 base64 이미지 전달
webviewRef.current?.postMessage(JSON.stringify({
type: "PICTURE_DATA",
payload: `data:image/jpeg;base64,${base64}`
}));
}
};
웹-앱 인터페이스 과정 총 정리
어느정도 웹-앱 인터페이스에 친숙해지셨나요? 아직도 조금 어렵다면 딱 2가지만 기억하시기 바랍니다.
- 웹-앱 인터페이스는 단방향 통신 구조를 가진다.
- 따라서 직접 요청-응답 구조를 구현해주어야 한다. 웹은 앱에서, 앱은 웹에서 메시지를 받을 준비를 해야한다.
이제 웹-앱 인터페이스에 대해 어느정도 친숙해지셨다면, 보일러 플레이트를 참고하여 직접 구현해보시기 바랍니다. 이후에는 개발 환경을 위한 앱 이벤트 모킹이나, 앱의 버전 혹은 OS를 고려한 구조 등도 구현해보셔도 좋습니다!
ㅤ
참고하기 좋은 github repository