paint-brush
Next.js 및 SignalR: 손쉬운 소켓 통합 및 문제 해결~에 의해@chilledcowfan
5,096 판독값
5,096 판독값

Next.js 및 SignalR: 손쉬운 소켓 통합 및 문제 해결

~에 의해 Anton Burduzha8m2023/09/05
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

수많은 프로젝트에서는 데이터를 다시 가져오지 않고도 변경 사항에 대한 인터페이스의 즉각적인 반응을 제공하기 위해 웹 소켓 통합이 필요합니다. 우리는 더 나은 개발 경험을 위해 API를 제공하는 타사 라이브러리에 대해 이야기하거나 비교하지 않을 것입니다. 내 목표는 `@microsoft/signalr`을 NextJ와 빠르게 통합하는 방법을 보여주는 것입니다.
featured image - Next.js 및 SignalR: 손쉬운 소켓 통합 및 문제 해결
Anton Burduzha HackerNoon profile picture

소수이지만 여전히 엄청난 수의 프로젝트에서는 데이터를 다시 가져오지 않고도 변경 사항에 대한 인터페이스의 즉각적인 반응을 제공하기 위해 웹 소켓 통합이 필요합니다.


이는 필수적인 것이므로 더 나은 개발 경험을 위해 API를 제공하는 타사 라이브러리에 대해 이야기하거나 비교하지 않을 것입니다.


내 목표는 @microsoft/signalr NextJ와 빠르게 통합하는 방법을 보여주는 것입니다. 그리고 개발 중에 직면한 문제를 해결하는 방법.


모든 사람이 이미 NextJS 프로젝트를 로컬에 설치하고 배포했기를 바랍니다. 제 경우에는 버전이 13.2.4 입니다. 좀 더 중요한 라이브러리를 추가해 보겠습니다. 데이터 가져오기를 위한 swr (버전 2.1.5 )과 로컬 캐시 및 @microsoft/signalr (버전 7.0.5 ) - 웹 소켓용 API에 대한 추가 작업을 수행해 보겠습니다.


 npm install --save @microsoft/signalr swr


REST API에서 초기 데이터를 가져오기 위해 간단한 fetcher 기능과 useChatData 라는 새 후크를 만드는 것부터 시작해 보겠습니다. 채팅 메시지 목록, 오류 및 로드 상태를 감지하는 필드, 캐시된 데이터를 변경할 수 있는 mutate 메서드 목록을 반환합니다.


 // hooks/useChatData.ts import useSWR from 'swr'; type Message = { content: string; createdAt: Date; id: string; }; async function fetcher<TResponse>(url: string, config: RequestInit): Promise<TResponse> { const response = await fetch(url, config); if (!response.ok) { throw response; } return await response.json(); } export const useChatData = () => { const { data, error, isLoading, mutate } = useSWR<Message[]>('OUR_API_URL', fetcher); return { data: data || [], isLoading, isError: error, mutate, }; };


예상대로 작동하는지 테스트하기 위해 페이지 구성 요소를 업데이트해 보겠습니다. 상단에서 후크를 가져오고 아래 스니펫과 같이 여기에서 데이터를 추출합니다. 작동하면 렌더링된 데이터가 표시됩니다. 보시다시피 아주 간단합니다.


 // pages/chat.ts import { useChatData } from 'hooks/useChatData'; const Chat: NextPage = () => { const { data } = useChatData(); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };


다음 단계에서는 미래 페이지를 웹 소켓에 연결하고, NewMessage 이벤트를 포착하고, 새 메시지로 캐시를 업데이트해야 합니다. 나는 별도의 파일에 소켓 서비스를 구축하는 것부터 시작할 것을 제안합니다.


SignalR 문서의 예제에 따르면 이벤트를 추가로 수신하기 위해 연결 인스턴스를 만들어야 합니다. 또한 중복을 방지하기 위한 연결 개체와 연결을 시작/중지하는 두 개의 도우미를 추가했습니다.


 // api/socket.ts import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; let connections = {} as { [key: string]: { type: string; connection: HubConnection; started: boolean } }; function createConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj) { console.log('SOCKET: Registering on server events ', messageType); const connection = new HubConnectionBuilder() .withUrl('API_URL', { logger: LogLevel.Information, withCredentials: false, }) .withAutomaticReconnect() .build(); connections[messageType] = { type: messageType, connection: connection, started: false, }; return connection; } else { return connections[messageType].connection; } } function startConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj.started) { connectionObj.connection.start().catch(err => console.error('SOCKET: ', err.toString())); connectionObj.started = true; } } function stopConnection(messageType: string) { const connectionObj = connections[messageType]; if (connectionObj) { console.log('SOCKET: Stoping connection ', messageType); connectionObj.connection.stop(); connectionObj.started = false; } } function registerOnServerEvents( messageType: string, callback: (payload: Message) => void, ) { try { const connection = createConnection(messageType); connection.on('NewIncomingMessage', (payload: Message) => { callback(payload); }); connection.onclose(() => stopConnection(messageType)); startConnection(messageType); } catch (error) { console.error('SOCKET: ', error); } } export const socketService = { registerOnServerEvents, stopConnection, };


이제 우리 페이지는 코드 조각과 같이 보일 수 있습니다. 메시지 목록과 함께 data 가져오고 추출하여 렌더링합니다. 또한 위의 useEffect NewMessage 이벤트를 등록하고 연결을 생성하며 백엔드를 수신합니다.


이벤트가 트리거되면 후크의 mutate 메소드가 기존 목록을 새 객체로 업데이트합니다.


 // pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };


제 생각에는 괜찮아 보이고 작동하며 피드에 새 메시지가 어떻게 나타나는지 볼 수 있습니다. 명확하고 이해하기 쉽기 때문에 채팅이 포함된 기본 예를 선택했습니다. 그리고 물론 자신의 논리에 따라 적용합니다.

작은 보너스

버전 중 하나( @microsoft/signalr )를 사용하면 중복 문제가 발생했습니다. 이는 종속성 배열인 useEffect 에 연결되었습니다. 종속성이 변경될 때마다 connection.on(event, callback); 콜백을 캐시하고 계속해서 트리거했습니다.


 useEffect(() => { // data equals [] by default (registerOnServerEvents 1 run), // but after initial data fetching it changes (registerOnServerEvents 2 run) // each event changes data and triggers runnning of registerOnServerEvents socketService.registerOnServerEvents( 'NewMessage', // callback cached (payload: Message) => { // mutate called multiple times on each data change mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); // after getting 3 messages events, we had got 4 messages rendered lol


우리가 찾은 가장 빠르고 안정적인 솔루션은 React ref 내부에 데이터 복사본을 보관하고 향후 업데이트를 위해 useEffect 내부에서 사용하는 것이었습니다.


 // pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); const messagesRef = useRef<Message[]>([]); useEffect(() => { messagesRef.current = chatData; }, [chatData]); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { const messagesCopy = messagesRef.current.slice(); mutate(() => [...messagesCopy, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };


현재 우리는 이미 필요한 수정 사항이 있는 것으로 보이는 새 버전의 @microsoft/signalr 사용하고 있습니다. 그러나 어쨌든 누군가가 이 솔루션이 유용하다고 생각하고 이 해결 방법을 사용한다면 저는 기쁠 것입니다. 결론적으로 SignalR에 대한 내 경험은 매우 긍정적이고 설치에는 특정 종속성이나 설정이 필요하지 않았으며 잘 작동하고 우리의 요구 사항을 충족한다고 말하고 싶습니다.