현재 진행중인 knockknock 프로젝트의 알림 기능 구현을 위해 서버의 데이터를 실시간으로 스트리밍하면서 알림 데이터를 받아올 수 있는 방법에 대해 찾아봤다.
클라이언트에서 HTTP 요청이 있을 때 서버는 요청에 대한 데이터를 전송하고 즉시 종료되는 클라이언트에서만 서버로 연결되는 단방향 통신이다.
TCP/IP 또는 UDP/IP와 같은 기존의 네트워크 프로토콜을 사용하는 Socket과 달리 HTTP를 기반으로 하며, 웹 브라우저와 웹 서버 간에 웹소켓 프로토콜을 사용하여 연결을 설정하는 양방향 통신이다.
버의 데이터를 클라이언트에서 실시간으로 스트리밍할 수 있도록 연결을 유지하는 단방향 통신이다. HTTP 프로토콜 위에서 동작하며 실행 시 기존의 HTTP 연결을 유지한 상태에서 리커넥팅이나 추가설정 없이 변경된 데이터를 지속적으로 수신할 수 있다.
알림 기능 구현을 위해 React native에서 사용 가능하고 엑세스 토큰 전송을 위한 커스텀 헤더 설정이 가능한 React-native-sse를 선택했다.
import React, {useEffect} from 'react';
import EventSource, {EventSourceListener} from 'react-native-sse';
const Notifications: React.FC= () => {
//...
useEffect(() => {
// 커스텀 헤더 옵션
const option = {
method: 'GET', // GET은 Defualt
headers: {Authorization: `Bearer ${token}`},
debug: true, // 요청을 디버깅하는 옵션
};
// 종속형 배열에 빈배열을 전달하여 컴포넌트의 첫 랜더링 시 실행
const eventSource = new EventSource(streamUrl, option);
// 요청 후 응답에 따른 동작을 순수함수에 담았다.
const listener: EventSourceListener = event => {
if (event.type === 'open') {
console.log('Open SSE connection.');
} else if (event.type === 'message') {
console.log('Connection', event);
} else if (event.type === 'error') {
console.error('Connection error:', event.message);
} else if (event.type === 'exception') {
console.error('Error:', event.message, event.error);
}
};
// 이벤트리스너를 호출
eventSource.addEventListener('open', listener);
eventSource.addEventListener('message', listener);
eventSource.addEventListener('error', listener);
// 컴포넌트의 언마운트와 함께 연결이 종료된다.
return () => {
eventSource.removeAllEventListeners();
eventSource.close();
};
}, []);
}
//...
첫 연결 시도 시 Android 시뮬레이터에서 테스트 시 유효한 URL과 Token을 전달해도 401에러를 반환했다. 구글링 결과 안드로이드에 내장된 Flipper(모바일 에플리케이션 개발 시 디버깅을 위한 도구)에서 네트워크의 요청과 응답을 모니터링하는 NetworkFlipperPlugin 플러그인이 실행된다. 이 디버깅 과정 중에 SSE 요청을 가로채어 실시간 요청에 개입하여 발생한 문제로 아래의 코드를 주석처리하니 정상적으로 연결됐다.
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
if (FlipperUtils.shouldEnableFlipper(context)) {
final FlipperClient client = AndroidFlipperClient.getInstance(context);
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(CrashReporterPlugin.getInstance());
// NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
// NetworkingModule.setCustomClientBuilder(
// new NetworkingModule.CustomClientBuilder() {
// @Override
// public void apply(OkHttpClient.Builder builder) {
// builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
// }
// });
// client.addPlugin(networkFlipperPlugin);
client.start();