Skip to content

Commit 2bf7898

Browse files
authored
Merge pull request #265 from DDD-Community/feat/app-bridge
feat: App 과 Web 간의 이벤트기반 통신모듈 구현
2 parents 496d3a4 + acaf3af commit 2bf7898

File tree

31 files changed

+6089
-74
lines changed

31 files changed

+6089
-74
lines changed
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# 06-1. WebView 토큰 전달 (웹 사이드)
2+
3+
## 목표
4+
5+
React Native 앱과 웹 간의 토큰 동기화를 구현합니다.
6+
7+
## 배경
8+
9+
GrowIt 앱은 **하이브리드 앱** 구조입니다:
10+
- 로그인/회원가입: React Native (네이티브)
11+
- 메인 서비스: WebView로 웹 앱 로드
12+
13+
이 구조에서 **토큰 동기화**가 필요합니다:
14+
1. 앱에서 로그인 → 웹에 토큰 전달
15+
2. 웹에서 토큰 갱신 → 앱에 알림
16+
3. 웹에서 로그아웃 → 앱에 알림
17+
18+
## 원리: postMessage 통신
19+
20+
### 왜 postMessage인가?
21+
22+
| 방식 | 보안 | 양방향 | 설명 |
23+
|------|------|--------|------|
24+
| URL 파라미터 ||| 토큰이 URL에 노출됨 |
25+
| Cookie ||| 도메인 제약, 보안 설정 복잡 |
26+
| **postMessage** ||| 안전한 양방향 통신 |
27+
28+
### 통신 흐름
29+
30+
```
31+
┌─────────────────────────────────────────────────────────────┐
32+
│ 앱 (React Native) │
33+
│ ┌─────────────────────────────────────────────────────────┐ │
34+
│ │ WebView │ │
35+
│ │ ┌─────────────────────────────────────────────────────┐ │ │
36+
│ │ │ 웹 (Next.js) │ │ │
37+
│ │ │ │ │ │
38+
│ │ │ 1. 페이지 로드 완료 │ │ │
39+
│ │ │ └─▶ postMessage('READY') ──────────────────┐ │ │ │
40+
│ │ │ │ │ │ │
41+
│ │ │ 2. 앱에서 토큰 수신 ◀──┘ │ │ │
42+
│ │ │ └─▶ window.addEventListener('message') │ │ │
43+
│ │ │ └─▶ localStorage에 저장 │ │ │
44+
│ │ │ │ │ │
45+
│ │ │ 3. 토큰 갱신 시 │ │ │
46+
│ │ │ └─▶ postMessage('TOKEN_REFRESHED') ────────┐ │ │ │
47+
│ │ │ │ │ │ │
48+
│ │ │ 4. 로그아웃 시 ◀──┘ │ │ │
49+
│ │ │ └─▶ postMessage('LOGOUT') │ │ │
50+
│ │ └─────────────────────────────────────────────────────┘ │ │
51+
│ └─────────────────────────────────────────────────────────┘ │
52+
└─────────────────────────────────────────────────────────────┘
53+
```
54+
55+
## 메시지 프로토콜
56+
57+
### 메시지 타입
58+
59+
| 방향 | 타입 | 페이로드 | 설명 |
60+
|------|------|----------|------|
61+
| 웹 → 앱 | `READY` | - | 웹 로드 완료, 토큰 요청 |
62+
| 앱 → 웹 | `AUTH_TOKEN` | `{ accessToken, refreshToken }` | 앱에서 토큰 전달 |
63+
| 웹 → 앱 | `TOKEN_REFRESHED` | `{ accessToken, refreshToken }` | 웹에서 토큰 갱신됨 |
64+
| 웹 → 앱 | `LOGOUT` | - | 로그아웃 요청 |
65+
66+
### 메시지 구조
67+
68+
```typescript
69+
interface AppMessage {
70+
type: 'READY' | 'AUTH_TOKEN' | 'TOKEN_REFRESHED' | 'LOGOUT';
71+
payload?: {
72+
accessToken?: string;
73+
refreshToken?: string;
74+
};
75+
}
76+
```
77+
78+
## 작업 절차
79+
80+
### 1. 앱 환경 감지 유틸리티
81+
82+
앱 내 WebView에서 실행 중인지 감지하는 함수입니다.
83+
84+
#### src/utils/appBridge.ts
85+
86+
```typescript
87+
/**
88+
* React Native WebView 환경인지 확인
89+
* WebView에서 실행될 때만 window.ReactNativeWebView가 존재함
90+
*/
91+
export const isInApp = (): boolean => {
92+
return (
93+
typeof window !== 'undefined' && window.ReactNativeWebView !== undefined
94+
);
95+
};
96+
97+
/**
98+
* 앱에 메시지 전송
99+
* @param type - 메시지 타입
100+
* @param payload - 전달할 데이터 (optional)
101+
*/
102+
export const sendToApp = (type: string, payload?: unknown): void => {
103+
if (!isInApp()) return;
104+
105+
window.ReactNativeWebView.postMessage(JSON.stringify({ type, payload }));
106+
};
107+
108+
/**
109+
* 앱에서 메시지 수신 리스너 등록
110+
* @param callback - 메시지 수신 시 호출할 콜백
111+
* @returns cleanup 함수
112+
*/
113+
export const onAppMessage = (
114+
callback: (message: { type: string; payload?: unknown }) => void,
115+
): (() => void) => {
116+
const handler = (event: MessageEvent) => {
117+
// 앱에서 전송한 메시지만 처리
118+
if (typeof event.data === 'object' && event.data.type) {
119+
callback(event.data);
120+
}
121+
};
122+
123+
window.addEventListener('message', handler);
124+
return () => window.removeEventListener('message', handler);
125+
};
126+
```
127+
128+
### 2. TypeScript 타입 선언
129+
130+
#### src/types/global.d.ts
131+
132+
```typescript
133+
interface Window {
134+
ReactNativeWebView?: {
135+
postMessage: (message: string) => void;
136+
};
137+
}
138+
```
139+
140+
### 3. 앱 인증 연동 Hook
141+
142+
앱과 토큰을 주고받는 핵심 로직입니다.
143+
144+
#### src/hooks/useAppAuth.ts
145+
146+
```typescript
147+
import { useEffect, useCallback } from 'react';
148+
import { isInApp, sendToApp, onAppMessage } from '@/utils/appBridge';
149+
150+
interface TokenPayload {
151+
accessToken: string;
152+
refreshToken: string;
153+
}
154+
155+
/**
156+
* 앱에서 토큰을 수신하고 저장하는 Hook
157+
* 앱 환경에서만 동작하며, 브라우저에서는 무시됨
158+
*/
159+
export const useAppAuth = (
160+
onTokenReceived?: (tokens: TokenPayload) => void,
161+
) => {
162+
useEffect(() => {
163+
// 브라우저 환경에서는 실행하지 않음
164+
if (!isInApp()) return;
165+
166+
// 앱에 준비 완료 알림 → 앱이 토큰을 전송해줌
167+
sendToApp('READY');
168+
169+
// 앱에서 토큰 수신 리스너
170+
const unsubscribe = onAppMessage((message) => {
171+
if (message.type === 'AUTH_TOKEN' && message.payload) {
172+
const { accessToken, refreshToken } = message.payload as TokenPayload;
173+
174+
// localStorage에 저장 (기존 웹 인증 로직과 호환)
175+
localStorage.setItem('accessToken', accessToken);
176+
localStorage.setItem('refreshToken', refreshToken);
177+
178+
// 콜백 호출 (상태 업데이트 등)
179+
onTokenReceived?.({ accessToken, refreshToken });
180+
181+
// 다른 컴포넌트에 알림 (optional)
182+
window.dispatchEvent(new Event('auth-updated'));
183+
}
184+
});
185+
186+
return unsubscribe;
187+
}, [onTokenReceived]);
188+
};
189+
190+
/**
191+
* 토큰 갱신 시 앱에 알림
192+
* API 인터셉터에서 토큰 갱신 후 호출
193+
*/
194+
export const notifyTokenRefresh = (
195+
accessToken: string,
196+
refreshToken: string,
197+
): void => {
198+
if (!isInApp()) return;
199+
200+
sendToApp('TOKEN_REFRESHED', { accessToken, refreshToken });
201+
};
202+
203+
/**
204+
* 로그아웃 시 앱에 알림
205+
* 로그아웃 로직에서 호출
206+
*/
207+
export const notifyLogout = (): void => {
208+
if (!isInApp()) return;
209+
210+
sendToApp('LOGOUT');
211+
};
212+
```
213+
214+
### 4. 루트 레이아웃에 Hook 적용
215+
216+
앱 진입 시 토큰을 수신하도록 설정합니다.
217+
218+
#### src/app/layout.tsx (또는 _app.tsx)
219+
220+
```typescript
221+
'use client';
222+
223+
import { useAppAuth } from '@/hooks/useAppAuth';
224+
225+
function AppAuthProvider({ children }: { children: React.ReactNode }) {
226+
useAppAuth((tokens) => {
227+
console.log('앱에서 토큰 수신:', tokens.accessToken.slice(0, 20) + '...');
228+
// 필요시 전역 상태 업데이트 (Zustand, Redux 등)
229+
});
230+
231+
return <>{children}</>;
232+
}
233+
234+
export default function RootLayout({
235+
children,
236+
}: {
237+
children: React.ReactNode;
238+
}) {
239+
return (
240+
<html>
241+
<body>
242+
<AppAuthProvider>{children}</AppAuthProvider>
243+
</body>
244+
</html>
245+
);
246+
}
247+
```
248+
249+
### 5. API 인터셉터에서 토큰 갱신 알림
250+
251+
토큰 갱신 시 앱에도 새 토큰을 알려줍니다.
252+
253+
#### src/lib/api.ts (또는 axios 인터셉터)
254+
255+
```typescript
256+
import { notifyTokenRefresh } from '@/hooks/useAppAuth';
257+
258+
// 토큰 갱신 로직 (기존 코드에 추가)
259+
const refreshToken = async () => {
260+
const response = await fetch('/api/auth/refresh', {
261+
method: 'POST',
262+
body: JSON.stringify({
263+
refreshToken: localStorage.getItem('refreshToken'),
264+
}),
265+
});
266+
267+
const { accessToken, refreshToken: newRefreshToken } = await response.json();
268+
269+
// localStorage 업데이트
270+
localStorage.setItem('accessToken', accessToken);
271+
localStorage.setItem('refreshToken', newRefreshToken);
272+
273+
// 앱에 알림 (추가)
274+
notifyTokenRefresh(accessToken, newRefreshToken);
275+
276+
return accessToken;
277+
};
278+
```
279+
280+
### 6. 로그아웃 시 앱 알림
281+
282+
로그아웃 로직에 앱 알림을 추가합니다.
283+
284+
#### src/hooks/useLogout.ts (또는 로그아웃 함수)
285+
286+
```typescript
287+
import { notifyLogout } from '@/hooks/useAppAuth';
288+
289+
export const logout = () => {
290+
// 기존 로그아웃 로직
291+
localStorage.removeItem('accessToken');
292+
localStorage.removeItem('refreshToken');
293+
294+
// 앱에 알림 (추가)
295+
notifyLogout();
296+
297+
// 리다이렉트 등
298+
window.location.href = '/login';
299+
};
300+
```
301+
302+
## 테스트 방법
303+
304+
### 1. 앱 환경 시뮬레이션 (개발용)
305+
306+
브라우저 콘솔에서 테스트:
307+
308+
```javascript
309+
// ReactNativeWebView 모킹
310+
window.ReactNativeWebView = {
311+
postMessage: (msg) => console.log('앱으로 전송:', JSON.parse(msg)),
312+
};
313+
314+
// 앱에서 토큰 전송 시뮬레이션
315+
window.dispatchEvent(
316+
new MessageEvent('message', {
317+
data: {
318+
type: 'AUTH_TOKEN',
319+
payload: {
320+
accessToken: 'test-access-token',
321+
refreshToken: 'test-refresh-token',
322+
},
323+
},
324+
}),
325+
);
326+
```
327+
328+
### 2. 실제 앱에서 테스트
329+
330+
1. 앱에서 로그인 후 WebView 진입
331+
2. 개발자 도구에서 localStorage 확인
332+
3. `accessToken`, `refreshToken`이 저장되었는지 확인
333+
334+
## 완료 조건
335+
336+
- [ ] `src/utils/appBridge.ts` 구현
337+
- [ ] `src/types/global.d.ts` 타입 선언 추가
338+
- [ ] `src/hooks/useAppAuth.ts` Hook 구현
339+
- [ ] 루트 레이아웃에 `useAppAuth` 적용
340+
- [ ] API 인터셉터에 `notifyTokenRefresh` 추가
341+
- [ ] 로그아웃 로직에 `notifyLogout` 추가
342+
- [ ] 앱과 연동 테스트
343+
344+
## 주의사항
345+
346+
1. **브라우저 환경 호환**: `isInApp()` 체크로 브라우저에서도 정상 동작
347+
2. **기존 로직 유지**: localStorage 기반 인증 로직은 그대로 유지
348+
3. **SSR 주의**: `window` 접근은 클라이언트에서만 (useEffect 내부)
349+
350+
## 참고
351+
352+
- 앱 사이드 문서: [06-webview-토큰-전달.md](./06-webview-토큰-전달.md)

0 commit comments

Comments
 (0)