Project Templates
Social Network
소셜 네트워크를 위한 실시간 채팅
52 분
소개 이 튜토리얼에서는 back4app을 사용하여 소셜 네트워크 애플리케이션을 위한 실시간 메시징 시스템을 구현하는 방법을 배웁니다 사용자가 메시지를 즉시 보내고 받을 수 있으며, 입력 중 표시를 보고 대화를 관리할 수 있는 완전한 채팅 기능을 구축할 것입니다 이는 매력적인 소셜 플랫폼에 필수적인 기능입니다 back4app은 parse server를 기반으로 구축된 backend as a service (baas) 플랫폼으로, live query 기능을 통해 강력한 실시간 기능을 제공합니다 back4app의 실시간 인프라를 사용하면 복잡한 websocket 서버를 관리하거나 확장 문제를 걱정하지 않고도 반응형 메시징 시스템을 만들 수 있습니다 이 튜토리얼이 끝나면 back4gram이라는 소셜 네트워크 애플리케이션에서 사용되는 것과 유사한 완전한 기능의 메시징 시스템을 만들게 됩니다 대화 생성, 실시간 메시지 교환, 입력 중 표시 및 사용자 검색을 구현하여 사용자에게 원활한 커뮤니케이션 경험을 제공할 것입니다 back4gram 프로젝트 찾기 여기 전체 코드는 소셜 네트워크 샘플 프로젝트 back4app으로 구축되었습니다 전제 조건 이 튜토리얼을 완료하려면 다음이 필요합니다 back4app 계정 back4app com https //www back4app com 에서 무료 계정에 가입할 수 있습니다 parse javascript sdk가 초기화된 back4app 프로젝트가 설정되어 있어야 합니다 로컬 머신에 node js가 설치되어 있어야 합니다 javascript, react js 및 back4app/parse server에 대한 기본 지식이 필요합니다 이미 구현된 인증 시스템이 필요합니다 아직 설정하지 않았다면, 우리의 인증 시스템 튜토리얼 https //www back4app com/docs/react/authentication tutorial 을 확인하세요 react 훅 및 컴포넌트 생명주기에 대한 친숙함이 필요합니다 1단계 – back4app의 실시간 기능 이해하기 코딩을 시작하기 전에, back4app이 live query 기능을 통해 실시간 기능을 어떻게 제공하는지 이해해 봅시다 live query 설명 live query는 클라이언트가 쿼리에 구독하고 해당 쿼리에 일치하는 객체가 생성, 업데이트 또는 삭제될 때 업데이트를 받을 수 있도록 하는 parse server의 기능입니다 이는 채팅 시스템과 같은 실시간 애플리케이션을 구축하는 데 완벽합니다 live query가 작동하는 방식은 다음과 같습니다 클라이언트는 특정 쿼리에 구독합니다 (예 "대화 x의 모든 메시지") 서버에서 일치하는 객체가 생성, 업데이트 또는 삭제될 때 live query는 자동으로 모든 구독 클라이언트에게 알림을 보냅니다 클라이언트는 이러한 이벤트에 응답하여 ui를 업데이트할 수 있습니다 메시징 시스템의 맥락에서, 이는 다음을 의미합니다 새 메시지가 전송되면, 해당 대화의 모든 사용자가 즉시 수신합니다 사용자가 입력을 시작하면, 다른 사용자는 실시간으로 입력 표시기를 볼 수 있습니다 메시지가 읽히면, 모든 참가자에 대해 읽음 확인이 업데이트될 수 있습니다 back4app에서 라이브 쿼리 설정하기 라이브 쿼리를 사용하려면 back4app 대시보드에서 활성화해야 합니다 back4app 대시보드에 로그인합니다 서버 설정 > 웹 호스팅 및 라이브 쿼리로 이동합니다 라이브 쿼리를 활성화합니다 라이브 쿼리와 함께 사용할 클래스를 추가합니다 (우리의 경우, "message"와 "typingstatus") 다음으로, 클라이언트 측 코드에서 live query 클라이언트를 초기화해야 합니다 // initialize parse with your back4app credentials parse initialize("your app id", "your javascript key"); parse serverurl = "https //parseapi back4app com/"; // initialize live query parse livequeryserverurl = "wss\ //your app id back4app io"; 이제 메시징 시스템을 위한 데이터베이스 스키마 설계로 넘어갑니다 back4gram 프로젝트 여기에서 전체 코드를 확인하세요 소셜 네트워크 샘플 프로젝트 는 back4app으로 구축되었습니다 2단계 – 데이터베이스 스키마 설계 메시징 시스템을 위해 여러 parse 클래스가 필요합니다 대화 두 명 이상의 사용자 간의 채팅을 나타냅니다 메시지 대화 내 개별 메시지를 나타냅니다 타이핑 상태 사용자가 대화 중에 타이핑할 때를 추적합니다 이 클래스들을 back4app에서 생성해 봅시다 대화 클래스 대화 클래스는 다음 필드를 가집니다 참여자 (사용자에 대한 포인터 배열) 대화에 참여하는 사용자들 마지막 메시지 (문자열) 가장 최근 메시지의 내용 업데이트 시간 (날짜) 레코드가 변경될 때 parse에 의해 자동으로 업데이트됨 메시지 클래스 메시지 클래스는 다음을 가집니다 대화 (대화에 대한 포인터) 이 메시지가 속한 대화 보낸 사람 (사용자에 대한 포인터) 메시지를 보낸 사용자 내용 (문자열) 메시지의 내용 생성 시간 (날짜) 메시지가 전송될 때 parse에 의해 자동으로 생성됨 타이핑 상태 클래스 타이핑 상태 클래스는 타이핑 지표를 추적합니다 사용자 (사용자 포인터) 타이핑 중인 사용자 대화 (대화 포인터) 타이핑이 발생하는 대화 타이핑 중 (부울) 사용자가 현재 타이핑 중인지 여부 이제 이 메시징 시스템을 구현하기 위해 프로젝트 구조를 설정해 보겠습니다 back4gram 프로젝트 여기에서 전체 코드를 찾으세요 소셜 네트워크 샘플 프로젝트 는 back4app으로 구축되었습니다 3단계 – 프로젝트 구조 설정 메시징 시스템을 위한 코드를 정리해봅시다 필수 구성 요소에 집중할 것입니다 src/ ├── components/ │ └── ui/ │ ├── toaster js │ └── avatar js ├── pages/ │ └── messagespage js ├── app js └── parseconfig js 간단한 toaster js 를 알림을 위해 만들어봅시다 // src/components/ui/toaster js export const toaster = { create ({ title, description, type }) => { // in a real app, you would use a proper toast notification component console log(`\[${type touppercase()}] ${title} ${description}`); alert(`${title} ${description}`); } }; 그리고 우리의 app js 를 메시지 경로를 포함하도록 업데이트합시다 // src/app js import react from 'react'; import { browserrouter as router, routes, route } from 'react router dom'; import messagespage from ' /pages/messagespage'; // import other pages function app() { return ( \<router> \<routes> \<route path="/messages" element={\<messagespage />} /> {/ other routes /} \</routes> \</router> ); } export default app; 이제 주요 메시징 기능을 구현해봅시다 back4gram 프로젝트 여기에서 전체 코드를 찾으세요 소셜 네트워크 샘플 프로젝트 는 back4app으로 구축되었습니다 4단계 – 메시지 페이지 구성 요소 만들기 우리의 messagespage js 구성 요소는 우리의 메시징 시스템의 핵심이 될 것입니다 이 구성 요소는 대화 목록 표시 사용자가 메시지를 보고 보낼 수 있도록 허용 타이핑 표시기 표시 사용자를 검색하여 새로운 대화를 시작할 수 있도록 활성화 이것을 단계별로 구축해 봅시다 // src/pages/messagespage js import react, { usestate, useeffect, useref } from 'react'; import { box, flex, input, button, vstack, text, avatar, heading, spinner, center, hstack, divider, } from '@chakra ui/react'; import { usenavigate } from 'react router dom'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function messagespage() { const \[message, setmessage] = usestate(''); const \[istyping, setistyping] = usestate(false); const \[isloading, setisloading] = usestate(true); const \[issending, setissending] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); const \[conversations, setconversations] = usestate(\[]); const \[activeconversation, setactiveconversation] = usestate(null); const \[messages, setmessages] = usestate(\[]); const \[users, setusers] = usestate(\[]); const \[searchquery, setsearchquery] = usestate(''); const \[showusersearch, setshowusersearch] = usestate(false); const \[processedmessageids, setprocessedmessageids] = usestate(new set()); const \[otherusertyping, setotherusertyping] = usestate(false); const messagesendref = useref(null); const livequerysubscription = useref(null); const typingstatussubscription = useref(null); const typingtimerref = useref(null); const navigate = usenavigate(); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { console log('checking authentication '); const user = await parse user current(); if (!user) { console log('no user found, redirecting to login'); navigate('/login'); return; } console log('user authenticated ', user id, user get('username')); setcurrentuser(user); fetchconversations(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); // clean up subscriptions when component unmounts return () => { if (livequerysubscription current) { livequerysubscription current unsubscribe(); } if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); } if (typingtimerref current) { cleartimeout(typingtimerref current); } }; }, \[navigate]); // we'll implement the following functions next const fetchconversations = async (user) => { // coming up next }; const resetmessagestate = () => { // coming up next }; const fetchmessages = async (conversationid) => { // coming up next }; const setuplivequery = async (conversationid) => { // coming up next }; const setuptypingstatussubscription = async (conversationid) => { // coming up next }; const updatetypingstatus = async (istyping) => { // coming up next }; const sendmessage = async () => { // coming up next }; const searchusers = async (query) => { // coming up next }; const startconversation = async (userid) => { // coming up next }; // format time for messages const formatmessagetime = (date) => { return new date(date) tolocaletimestring(\[], { hour '2 digit', minute '2 digit' }); }; // format date for conversation list const formatconversationdate = (date) => { const now = new date(); const messagedate = new date(date); // if today, show time if (messagedate todatestring() === now\ todatestring()) { return messagedate tolocaletimestring(\[], { hour '2 digit', minute '2 digit' }); } // if this year, show month and day if (messagedate getfullyear() === now\ getfullyear()) { return messagedate tolocaledatestring(\[], { month 'short', day 'numeric' }); } // otherwise show full date return messagedate tolocaledatestring(); }; return ( \<flex h="100vh"> {/ we'll implement the ui next /} \</flex> ); } export default messagespage; 이제 우리의 메시징 시스템에 필요한 각 기능을 구현해 봅시다 back4gram 프로젝트 찾기 여기 전체 코드를 소셜 네트워크 샘플 프로젝트 back4app으로 구축된 5단계 – 사용자 대화 가져오기 먼저, 현재 사용자의 모든 대화를 로드하는 fetchconversations 함수를 구현해 보겠습니다 const fetchconversations = async (user) => { setisloading(true); try { console log('fetching conversations for user ', user id); // query conversations where the current user is a participant const query = new parse query('conversation'); query equalto('participants', user); query include('participants'); query descending('updatedat'); const results = await query find(); console log('found conversations ', results length); // format conversations const formattedconversations = results map(conv => { const participants = conv get('participants'); // find the other participant (not the current user) const otherparticipant = participants find(p => p id !== user id); if (!otherparticipant) { console warn('could not find other participant in conversation ', conv id); return null; } return { id conv id, user { id otherparticipant id, username otherparticipant get('username'), avatar otherparticipant get('avatar') ? otherparticipant get('avatar') url() null }, lastmessage conv get('lastmessage') || '', updatedat conv get('updatedat') }; }) filter(boolean); // remove any null entries console log('formatted conversations ', formattedconversations); setconversations(formattedconversations); // if there are conversations, set the first one as active if (formattedconversations length > 0) { setactiveconversation(formattedconversations\[0]); fetchmessages(formattedconversations\[0] id); } } catch (error) { console error('error fetching conversations ', error); toaster create({ title 'error', description 'failed to load conversations', type 'error', }); } finally { setisloading(false); } }; 이 함수는 현재 사용자가 참여자인 대화에 대한 conversation 클래스를 쿼리합니다 결과를 ui에 표시할 수 있도록 형식화합니다 사용 가능한 경우 첫 번째 대화를 활성화로 설정합니다 back4gram 프로젝트 찾기 여기 전체 코드는 소셜 네트워크 샘플 프로젝트 back4app으로 구축되었습니다 6단계 – 메시지 처리 및 실시간 업데이트 이제 메시지를 가져오고 live query를 사용하여 실시간 업데이트를 설정하는 기능을 구현해 보겠습니다 const resetmessagestate = () => { console log('resetting message state'); setmessages(\[]); setprocessedmessageids(new set()); setotherusertyping(false); if (livequerysubscription current) { livequerysubscription current unsubscribe(); livequerysubscription current = null; console log('unsubscribed from live query in resetmessagestate'); } if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); typingstatussubscription current = null; console log('unsubscribed from typing status subscription in resetmessagestate'); } }; const fetchmessages = async (conversationid) => { // reset message state to avoid any lingering messages or subscriptions resetmessagestate(); try { // query messages for this conversation const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); query ascending('createdat'); const results = await query find(); // format messages const formattedmessages = results map(msg => ({ id msg id, text msg get('text'), sender { id msg get('sender') id, username msg get('sender') get('username') }, createdat msg get('createdat') })); // initialize the set of processed message ids const messageids = new set(formattedmessages map(msg => msg id)); // set state after processing all messages setmessages(formattedmessages); setprocessedmessageids(messageids); // set up live query subscription for new messages setuplivequery(conversationid); // set up typing status subscription setuptypingstatussubscription(conversationid); // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); } catch (error) { console error('error fetching messages ', error); toaster create({ title 'error', description 'failed to load messages', type 'error', }); } }; 이제 실시간 메시지 업데이트를 위한 live query 구독을 구현해 보겠습니다 const setuplivequery = async (conversationid) => { // capture the current user in a closure to avoid null reference later const captureduser = currentuser; // unsubscribe from previous subscription if exists if (livequerysubscription current) { livequerysubscription current unsubscribe(); console log('unsubscribed from previous live query'); } try { console log('setting up live query for conversation ', conversationid); // create a query that will be used for the subscription const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); console log('created query for message class with conversation id ', conversationid); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened for conversation ', conversationid); }); // handle new messages livequerysubscription current on('create', (message) => { console log('new message received via live query ', message id); // check if we've already processed this message if (processedmessageids has(message id)) { console log('skipping duplicate message ', message id); return; } // format the new message const newmessage = { id message id, text message get('text'), sender { id message get('sender') id, username message get('sender') get('username') }, createdat message get('createdat') }; console log('formatted new message ', newmessage); // add the message id to the set of processed ids setprocessedmessageids(previds => { const newids = new set(previds); newids add(message id); return newids; }); // add the new message to the messages state setmessages(prevmessages => { // check if the message is already in the list (additional duplicate check) if (prevmessages some(msg => msg id === message id)) { console log('message already in list, not adding again ', message id); return prevmessages; } return \[ prevmessages, newmessage]; }); // use the captured user to avoid null reference if (captureduser && message get('sender') id !== captureduser id) { setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === conversationid) { return { conv, lastmessage message get('text'), updatedat message get('createdat') }; } return conv; }); }); } else { // if user check fails, update without the check setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === conversationid) { return { conv, lastmessage message get('text'), updatedat message get('createdat') }; } return conv; }); }); } // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); // handle subscription close livequerysubscription current on('close', () => { console log('live query connection closed for conversation ', conversationid); }); } catch (error) { console error('error setting up live query ', error); toaster create({ title 'error', description 'failed to set up real time messaging', type 'error', }); } }; 이 라이브 쿼리 설정 현재 대화에서 새로운 메시지에 대한 구독을 생성합니다 실시간으로 들어오는 메시지를 처리합니다 새로운 메시지가 도착할 때 ui를 업데이트합니다 최신 메시지와 타임스탬프를 사용하여 대화 목록을 업데이트합니다 중복 메시지를 관리합니다(라이브 쿼리를 통해 전송 및 수신할 때 발생할 수 있음) back4gram 프로젝트 찾기 여기 전체 코드는 소셜 네트워크 샘플 프로젝트 back4app으로 구축되었습니다 7단계 – 타이핑 인디케이터 구현 사용자 경험을 향상시키기 위해 실시간 타이핑 인디케이터를 추가해 보겠습니다 const setuptypingstatussubscription = async (conversationid) => { // cancel previous subscription, if it exists if (typingstatussubscription current) { typingstatussubscription current unsubscribe(); console log('unsubscribed from previous typing status subscription'); } try { console log('setting up typing status subscription for conversation ', conversationid); // create a query for the typingstatus class const query = new parse query('typingstatus'); const conversation = new parse object('conversation'); conversation id = conversationid; // filter by conversation query equalto('conversation', conversation); // don't include the current user's status query notequalto('user', currentuser); // subscribe to the query typingstatussubscription current = await query subscribe(); console log('successfully subscribed to typing status'); // handle create events typingstatussubscription current on('create', (status) => { console log('typing status created ', status get('istyping')); setotherusertyping(status get('istyping')); }); // handle update events typingstatussubscription current on('update', (status) => { console log('typing status updated ', status get('istyping')); setotherusertyping(status get('istyping')); }); // handle errors typingstatussubscription current on('error', (error) => { console error('typing status subscription error ', error); }); } catch (error) { console error('error setting up typing status subscription ', error); } }; const updatetypingstatus = async (istyping) => { if (!activeconversation || !currentuser) return; try { // check if a typing status already exists for this user and conversation const query = new parse query('typingstatus'); const conversation = new parse object('conversation'); conversation id = activeconversation id; query equalto('user', currentuser); query equalto('conversation', conversation); const existingstatus = await query first(); if (existingstatus) { // update existing status existingstatus set('istyping', istyping); await existingstatus save(); } else { // create a new status const typingstatus = parse object extend('typingstatus'); const newstatus = new typingstatus(); newstatus set('user', currentuser); newstatus set('conversation', conversation); newstatus set('istyping', istyping); await newstatus save(); } } catch (error) { console error('error updating typing status ', error); } }; 이 구현 대화 중 다른 사용자의 입력 상태 업데이트를 구독합니다 다른 사람이 입력할 때 ui를 업데이트합니다 현재 사용자가 입력할 때 입력 상태 업데이트를 보냅니다 back4gram 프로젝트 찾기 여기 전체 코드를 소셜 네트워크 샘플 프로젝트 back4app으로 구축된 8단계 – 메시지 전송 이제 메시지 전송 기능을 구현해 보겠습니다 const sendmessage = async () => { if (!message trim() || !activeconversation) return; const messagetext = message trim(); // store the message text setissending(true); setmessage(''); // clear input immediately to prevent double sending setistyping(false); // clear typing status locally updatetypingstatus(false); // clear typing status on server // clear typing timer if (typingtimerref current) { cleartimeout(typingtimerref current); } try { // create message const message = parse object extend('message'); const newmessage = new message(); // set conversation pointer const conversation = new parse object('conversation'); conversation id = activeconversation id; newmessage set('conversation', conversation); newmessage set('sender', currentuser); newmessage set('text', messagetext); // save the message const savedmessage = await newmessage save(); console log('message saved ', savedmessage id); // add the message id to the set of processed ids to prevent duplication // when it comes back through live query setprocessedmessageids(previds => { const newids = new set(previds); newids add(savedmessage id); return newids; }); // check if this is a new conversation (no messages yet) // if it is, add the message to the ui immediately if (messages length === 0) { console log('first message in conversation, adding to ui immediately'); const formattedmessage = { id savedmessage id, text messagetext, sender { id currentuser id, username currentuser get('username') }, createdat new date() }; setmessages(\[formattedmessage]); // scroll to bottom of messages settimeout(() => { if (messagesendref current) { messagesendref current scrollintoview({ behavior 'smooth' }); } }, 100); } // update conversation's lastmessage const conversationobj = await new parse query('conversation') get(activeconversation id); conversationobj set('lastmessage', messagetext); await conversationobj save(); // update the conversation in the list setconversations(prevconversations => { return prevconversations map(conv => { if (conv id === activeconversation id) { return { conv, lastmessage messagetext, updatedat new date() }; } return conv; }); }); } catch (error) { console error('error sending message ', error); toaster create({ title 'error', description 'failed to send message', type 'error', }); // if there's an error, put the message back in the input setmessage(messagetext); } finally { setissending(false); } }; 이 함수 back4app 데이터베이스에 새 메시지를 생성하고 저장합니다 대화의 마지막 메시지와 타임스탬프를 업데이트합니다 live query를 통해 메시지가 다시 올 때 중복 방지를 처리합니다 발신자에게 즉각적인 피드백을 제공합니다 back4gram 프로젝트 여기에서 전체 코드를 찾으세요 소셜 네트워크 샘플 프로젝트 은 back4app으로 구축되었습니다 9단계 – 사용자 검색 및 새로운 대화 시작하기 사용자를 검색하고 새로운 대화를 시작하는 기능을 구현해 봅시다 const searchusers = async (query) => { if (!query trim()) { setusers(\[]); return; } try { console log('searching for users with query ', query); // create a query for the user class const userquery = new parse query(parse user); // search for username containing the query string (case insensitive) userquery contains('username', query tolowercase()); // don't include the current user in results userquery notequalto('objectid', currentuser id); // limit to 10 results userquery limit(10); // execute the query const results = await userquery find(); console log('user search results ', results length); // format users const formattedusers = results map(user => ({ id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null })); console log('formatted users ', formattedusers); setusers(formattedusers); } catch (error) { console error('error searching users ', error); toaster create({ title 'error', description 'failed to search users', type 'error', }); } }; const startconversation = async (userid) => { console log('starting conversation with user id ', userid); try { // check if a conversation already exists with this user const query = new parse query('conversation'); // create pointers to both users const currentuserpointer = parse user current(); const otheruserpointer = new parse user(); otheruserpointer id = userid; // find conversations where both users are participants query containsall('participants', \[currentuserpointer, otheruserpointer]); const existingconv = await query first(); if (existingconv) { console log('found existing conversation ', existingconv id); // get the other user object const userquery = new parse query(parse user); const otheruser = await userquery get(userid); // format the conversation const conversation = { id existingconv id, user { id otheruser id, username otheruser get('username'), avatar otheruser get('avatar') ? otheruser get('avatar') url() null }, lastmessage existingconv get('lastmessage') || '', updatedat existingconv get('updatedat') }; // set as active conversation setactiveconversation(conversation); // fetch messages and set up live query await fetchmessages(conversation id); } else { console log('creating new conversation with user ', userid); // get the other user object const userquery = new parse query(parse user); const otheruser = await userquery get(userid); // create a new conversation const conversation = parse object extend('conversation'); const newconversation = new conversation(); // set participants newconversation set('participants', \[currentuserpointer, otheruserpointer]); newconversation set('lastmessage', ''); // save the conversation const savedconv = await newconversation save(); console log('new conversation created ', savedconv id); // format the conversation const conversation = { id savedconv id, user { id otheruser id, username otheruser get('username'), avatar otheruser get('avatar') ? otheruser get('avatar') url() null }, lastmessage '', updatedat savedconv get('updatedat') }; // add to conversations list setconversations(prev => \[conversation, prev]); // set as active conversation and reset message state setactiveconversation(conversation); resetmessagestate(); // set up live query for the new conversation await setuplivequery(savedconv id); // set up typing status subscription await setuptypingstatussubscription(savedconv id); // reset search setshowusersearch(false); setsearchquery(''); setusers(\[]); } } catch (error) { console error('error starting conversation ', error); toaster create({ title 'error', description 'failed to start conversation', type 'error', }); } }; 이 코드는 우리의 메시징 시스템을 위한 두 가지 중요한 기능을 제공합니다 사용자 검색 이 searchusers 기능은 사용자가 사용자 이름으로 다른 사용자를 찾을 수 있게 해줍니다 이 기능은 back4app의 user 클래스를 쿼리하고, 현재 사용자를 결과에서 제외하며, 데이터를 표시할 수 있도록 형식화합니다 대화 생성 이 startconversation 기능은 기존 대화를 찾고 새로운 대화를 생성하는 두 가지 작업을 처리합니다 이 기능은 사용자 간에 이미 대화가 존재하는지 확인합니다 하나가 존재하면 해당 대화와 메시지를 로드합니다 대화가 존재하지 않으면 새로 생성합니다 새 메시지 및 입력 상태에 대한 라이브 쿼리 구독을 설정합니다 이 기능들이 구현되면 사용자는 다른 사용자를 쉽게 찾고 그들과 대화를 시작할 수 있습니다 이는 소셜 네트워크 메시징 시스템에 필수적입니다 back4gram 프로젝트 여기에서 전체 코드를 찾을 수 있습니다 소셜 네트워크 샘플 프로젝트 는 back4app으로 구축되었습니다 10단계 – 채팅 ui 구축 이제 모든 기능을 구현했으니, 메시징 시스템을 위한 사용자 인터페이스를 만들어 보겠습니다 ui는 다음으로 구성됩니다 모든 대화를 나열하는 사이드바 사용자를 찾기 위한 검색 인터페이스 메시지를 보여주는 채팅 창 타이핑 표시 지원이 있는 메시지 입력란 우리의 messagespage 컴포넌트에 대한 jsx는 다음과 같습니다 return ( \<flex h="100vh"> {/ chat list sidebar /} \<box w="300px" borderrightwidth="1px" bordercolor="gray 600" p={4} bg="gray 800"> \<heading size="md" mb={4}> messages \</heading> \<flex mb={4}> \<input placeholder="search users " value={searchquery} onchange={(e) => { const value = e target value; setsearchquery(value); if (value trim() length > 0) { searchusers(value); } else { setusers(\[]); } }} mr={2} /> \<button onclick={() => { setshowusersearch(!showusersearch); if (!showusersearch) { setsearchquery(''); setusers(\[]); } }} \> {showusersearch ? 'cancel' 'new'} \</button> \</flex> {isloading ? ( \<center py={10}> \<spinner size="lg" /> \</center> ) showusersearch ? ( // user search results \<vstack align="stretch" spacing={2}> {users length > 0 ? ( users map(user => ( \<box key={user id} p={3} borderradius="md" hover={{ bg "gray 700" }} cursor="pointer" onclick={() => startconversation(user id)} \> \<hstack> \<avatar size="sm" name={user username} src={user avatar} /> \<text>{user username}\</text> \</hstack> \</box> )) ) searchquery ? ( \<text color="gray 400" textalign="center">no users found\</text> ) ( \<text color="gray 400" textalign="center">search for users to message\</text> )} \</vstack> ) ( // conversations list \<vstack align="stretch" spacing={0}> {conversations length > 0 ? ( conversations map(conv => ( \<box key={conv id} p={3} borderradius="md" bg={activeconversation? id === conv id ? "gray 700" "transparent"} hover={{ bg "gray 700" }} cursor="pointer" onclick={() => { if (activeconversation? id !== conv id) { setactiveconversation(conv); fetchmessages(conv id); } }} \> \<hstack> \<avatar size="sm" name={conv user username} src={conv user avatar} /> \<box flex="1" overflow="hidden"> \<flex justify="space between" align="center"> \<text fontweight="bold" nooflines={1}>{conv user username}\</text> \<text fontsize="xs" color="gray 400"> {formatconversationdate(conv updatedat)} \</text> \</flex> \<text fontsize="sm" color="gray 400" nooflines={1}> {conv lastmessage || 'no messages yet'} \</text> \</box> \</hstack> \</box> )) ) ( \<text color="gray 400" textalign="center" mt={8}> no conversations yet start a new one! \</text> )} \</vstack> )} \</box> {/ chat window /} \<box flex={1} p={0} display="flex" flexdirection="column" bg="gray 900"> {activeconversation ? ( <> {/ chat header /} \<flex align="center" p={4} borderbottomwidth="1px" bordercolor="gray 700"> \<avatar size="sm" mr={2} name={activeconversation user username} src={activeconversation user avatar} /> \<text fontweight="bold">{activeconversation user username}\</text> {otherusertyping && ( \<text ml={2} color="gray 400" fontsize="sm"> is typing \</text> )} \</flex> {/ messages /} \<box flex={1} p={4} overflowy="auto" display="flex" flexdirection="column" \> {messages length > 0 ? ( messages map((msg) => ( \<box key={msg id} alignself={msg sender id === currentuser id ? "flex end" "flex start"} bg={msg sender id === currentuser id ? "blue 500" "gray 700"} color={msg sender id === currentuser id ? "white" "white"} p={3} borderradius="lg" maxw="70%" mb={2} \> \<text>{msg text}\</text> \<text fontsize="xs" color={msg sender id === currentuser id ? "blue 100" "gray 400"} textalign="right" mt={1}> {formatmessagetime(msg createdat)} \</text> \</box> )) ) ( \<center flex={1}> \<text color="gray 400"> no messages yet say hello! \</text> \</center> )} \<div ref={messagesendref} /> \</box> {/ message input /} \<flex p={4} bordertopwidth="1px" bordercolor="gray 700"> \<input placeholder="type a message " value={message} onchange={(e) => { setmessage(e target value); // set typing status to true locally setistyping(true); // update typing status on server updatetypingstatus(true); // clear any existing timer if (typingtimerref current) { cleartimeout(typingtimerref current); } // set a new timer to turn off typing status after 2 seconds of inactivity typingtimerref current = settimeout(() => { setistyping(false); updatetypingstatus(false); }, 2000); }} mr={2} onkeypress={(e) => { if (e key === 'enter' && !e shiftkey) { e preventdefault(); sendmessage(); // also clear typing status when sending a message setistyping(false); updatetypingstatus(false); if (typingtimerref current) { cleartimeout(typingtimerref current); } } }} /> \<button colorscheme="blue" onclick={sendmessage} isloading={issending} disabled={!message trim() || issending} \> send \</button> \</flex> \</> ) ( \<center flex={1}> \<vstack> \<text fontsize="xl" fontweight="bold">welcome to messages\</text> \<text color="gray 400"> select a conversation or start a new one \</text> \</vstack> \</center> )} \</box> \</flex> ); ui를 주요 구성 요소로 나눠봅시다 대화 사이드바 왼쪽 사이드바에는 모든 대화 목록이 표시됩니다 각 대화에는 다른 사용자의 이름, 아바타 및 마지막 메시지가 표시됩니다 사용자는 대화를 클릭하여 채팅을 보고 계속할 수 있습니다 상단의 검색창을 통해 다른 사용자를 찾아 새 대화를 시작할 수 있습니다 메시지 표시 주요 영역에는 활성 대화의 메시지가 표시됩니다 현재 사용자의 메시지는 오른쪽에 파란색 배경으로 표시됩니다 다른 사용자의 메시지는 왼쪽에 회색 배경으로 표시됩니다 각 메시지는 메시지 텍스트와 전송 시간을 표시합니다 새 메시지가 도착하면 메시지 목록이 자동으로 아래로 스크롤됩니다 메시지 입력 하단에는 메시지를 입력하고 전송할 수 있는 입력 영역이 있습니다 사용자는 enter 키를 눌러 메시지를 보낼 수 있습니다 전송 버튼을 눌러서도 메시지를 보낼 수 있습니다 입력 영역에 입력하면 자동으로 입력 표시가 활성화됩니다 대화가 선택될 때까지 입력이 비활성화됩니다 타이핑 표시기 다른 사용자가 타이핑할 때, "타이핑 중 " 표시기가 나타납니다 표시기는 typingstatus 라이브 쿼리 구독을 사용하여 실시간으로 업데이트됩니다 이 ui는 사용자가 인기 있는 메시징 애플리케이션에서 기대할 수 있는 것과 유사한 깔끔하고 직관적인 메시징 인터페이스를 제공합니다 back4gram 프로젝트 여기에서 전체 코드를 찾으세요 소셜 네트워크 샘플 프로젝트 는 back4app으로 구축되었습니다 11단계 – 라이브 쿼리 연결 테스트 우리의 라이브 쿼리 연결이 제대로 작동하는지 확인하기 위해, 컴포넌트가 마운트될 때 연결을 검증하는 테스트 함수를 추가해 보겠습니다 // add this at the beginning of the component to test live query connection useeffect(() => { // test live query connection const testlivequery = async () => { try { console log('testing live query connection '); console log('live query url ', parse livequeryserverurl); const query = new parse query('message'); console log('created test query for message class'); const subscription = await query subscribe(); console log('live query subscription successful!'); subscription on('open', () => { console log('live query connection opened successfully'); }); subscription on('create', (object) => { console log('live query create event received ', object id); }); subscription on('error', (error) => { console error('live query error ', error); }); // unsubscribe after a few seconds to test settimeout(() => { subscription unsubscribe(); console log('unsubscribed from test live query'); }, 10000); } catch (error) { console error('error testing live query ', error); } }; testlivequery(); }, \[]); 이 테스트 함수는 메시지 클래스에 대한 구독을 생성하려고 시도합니다 연결 이벤트에 대한 이벤트 핸들러를 설정합니다 각 단계의 결과를 기록합니다 10초 후에 자동으로 구독 해제됩니다 콘솔 로그를 통해 다음을 확인할 수 있습니다 라이브 쿼리 url이 올바르게 구성되었습니다 연결이 설정될 수 있습니다 이벤트가 제대로 수신되고 있습니다 라이브 쿼리 연결에 문제가 발생하면 다음을 확인하십시오 back4app 라이브 쿼리가 귀하의 앱에 대해 활성화되어 있습니다 라이브 쿼리 url이 parse 초기화에서 올바르게 설정되어 있습니다 메시지 클래스가 back4app의 라이브 쿼리 클래스 목록에 추가되었습니다 귀하의 네트워크가 websocket 연결을 허용합니다 back4gram 프로젝트 찾기 여기 전체 코드를 위한 소셜 네트워크 샘플 프로젝트 back4app으로 구축되었습니다 12단계 – back4app이 실시간 메시징을 어떻게 가능하게 하는지 이해하기 우리가 완전한 메시징 시스템을 구축했으니, back4app의 기능이 실시간 메시징을 어떻게 가능하게 하는지 더 깊이 살펴보겠습니다 실시간 쿼리 인프라 back4app의 실시간 쿼리 기능은 websockets를 기반으로 하며, 이는 클라이언트와 서버 간의 지속적인 연결을 제공합니다 이는 전통적인 http 요청/응답 모델과 근본적으로 다릅니다 전통적인 rest api 클라이언트가 요청을 보냄 → 서버가 응답함 → 연결 종료 websockets/실시간 쿼리 클라이언트가 지속적인 연결을 설정함 → 서버가 언제든지 업데이트를 푸시할 수 있음 이 지속적인 연결이 우리 메시징 시스템의 실시간 기능을 가능하게 합니다 새로운 메시지가 생성되면, back4app은 서버를 폴링할 필요 없이 모든 구독 클라이언트에게 자동으로 그 업데이트를 푸시합니다 파스 서버의 구독 모델 파스 서버의 구독 모델은 쿼리 기반입니다 이는 다음을 의미합니다 특정 쿼리에 구독합니다 (예 "대화 x의 모든 메시지") 해당 쿼리에 맞는 객체에 대해서만 업데이트를 받습니다 다양한 유형의 이벤트를 받을 수 있습니다 ( 생성 , 업데이트 , 삭제 , 등등) 이 쿼리 기반 접근 방식은 정밀한 구독을 가능하게 하므로 매우 강력합니다 우리의 메시징 시스템에서는 활성 대화에 대한 메시지만 구독하고 있으며, 이는 모든 메시지를 구독하는 것보다 훨씬 더 효율적입니다 데이터베이스 고려사항 back4app와 함께 라이브 쿼리를 사용할 때 몇 가지 중요한 데이터베이스 고려사항이 있습니다 색인 생성 구독 쿼리에서 사용되는 필드가 더 나은 성능을 위해 색인화되도록 하세요 데이터 크기 객체를 작게 유지하여 페이로드 크기를 줄이고 성능을 향상시키세요 확장성 실시간 쿼리 연결은 리소스를 사용하므로 애플리케이션을 확장할 때 이를 고려하세요 최적의 성능을 위해 고려해야 할 사항 메시지 클래스의 대화 필드에 색인 생성하기 대형 객체를 포함하기보다는 포인터 사용하기 메모리 누수를 피하기 위해 구독의 생명주기 관리하기 보안 고려사항 실시간 메시징을 구현할 때 보안은 매우 중요합니다 acls and clps 액세스 제어 목록 및 클래스 수준 권한을 사용하여 데이터를 보호합니다 connection authentication 인증된 사용자만 라이브 쿼리 연결을 설정할 수 있어야 합니다 data validation 클라우드 코드를 사용하여 서버 측에서 메시지 내용을 검증합니다 rate limiting 남용을 방지하기 위해 속도 제한을 구현합니다 예를 들어, 메시지가 저장되기 전에 클라우드 코드를 사용하여 검증하고 정리할 수 있습니다 // in cloud code parse cloud beforesave("message", async (request) => { const message = request object; const text = message get("text"); // check if message is empty if (!text || text trim() length === 0) { throw new error("message cannot be empty"); } // sanitize message text message set("text", sanitizehtml(text)); // rate limiting check if user is sending too many messages const sender = message get("sender"); const query = new parse query("message"); query equalto("sender", sender); query greaterthan("createdat", new date(new date() 60000)); // last minute const count = await query count({usemasterkey true}); if (count > 10) { throw new error("you are sending messages too quickly please slow down "); } }); back4gram 프로젝트 찾기 여기 완전한 코드를 위한 소셜 네트워크 샘플 프로젝트 back4app으로 구축되었습니다 13단계 – 메시징 시스템 테스트 및 최적화 메시징 시스템이 올바르게 작동하는지 확인하려면 철저히 테스트해야 합니다 기본 기능 테스트 메시지 전송 메시지를 전송하고 수신할 수 있는지 확인합니다 대화 생성 새로운 대화를 생성하는 테스트 사용자 검색 사용자를 찾고 대화를 시작할 수 있는지 확인합니다 ui 업데이트 입력 표시기, 메시지 목록 및 대화 목록이 올바르게 업데이트되는지 확인합니다 다중 사용자 테스트 메시징 시스템을 제대로 테스트하려면 여러 사용자와 함께 테스트해야 합니다 두 개의 브라우저 창 또는 탭을 엽니다 각각 다른 사용자로 로그인합니다 그들 사이에 대화를 시작합니다 메시지를 주고받습니다 타이핑 표시기 및 실시간 업데이트를 테스트합니다 성능 최적화 더 큰 애플리케이션의 경우, 다음 최적화를 고려하십시오 페이지 매김 메시지를 한 번에 모두가 아닌 배치로 로드합니다 연결 관리 활성 대화에 대해서만 라이브 쿼리 연결을 설정합니다 효율적인 쿼리 타겟 쿼리를 사용하고 필요한 필드만 포함합니다 캐싱 자주 접근하는 데이터에 대해 클라이언트 측 캐싱을 구현합니다 메시지 로드를 위한 페이지 매김 구현의 예는 다음과 같습니다 const fetchmessages = async (conversationid, limit = 20, skip = 0) => { try { const query = new parse query('message'); const conversation = new parse object('conversation'); conversation id = conversationid; query equalto('conversation', conversation); query include('sender'); query descending('createdat'); query limit(limit); query skip(skip); const results = await query find(); // process results return { messages formattedmessages, hasmore results length === limit }; } catch (error) { console error('error fetching messages ', error); throw error; } }; 오류 처리 및 복구 강력한 오류 처리는 실시간 애플리케이션에 매우 중요합니다 연결 실패 websocket 실패에 대한 재연결 로직 구현 메시지 실패 실패한 메시지 전송을 처리하고 재시도 옵션 제공 상태 복구 연결 중단 후 애플리케이션 상태 복구 예를 들어, 연결 모니터링을 추가할 수 있습니다 // monitor live query connection status parse livequery on('open', () => { console log('live query connection established'); // re establish subscriptions if needed }); parse livequery on('close', () => { console log('live query connection closed'); // show connection status to user }); parse livequery on('error', (error) => { console error('live query error ', error); // handle error, potentially reconnect }); 결론 이 튜토리얼에서는 back4app을 사용하여 소셜 네트워크를 위한 포괄적인 실시간 메시징 시스템을 구축했습니다 다음을 구현했습니다 사용자 간 메시징 사용자가 서로를 찾고 메시지를 보낼 수 있는 시스템을 생성했습니다 실시간 업데이트 back4app의 live query를 사용하여 메시지를 즉시 전달했습니다 타이핑 표시기 실시간 타이핑 알림으로 사용자 경험을 향상시켰습니다 대화 관리 여러 대화를 보고 관리할 수 있는 ui를 구축했습니다 back4app의 live query 기능은 복잡한 websocket 서버나 확장 문제를 관리하지 않고도 이를 가능하게 하는 실시간 인프라를 제공했습니다 parse server의 내장 구독 모델을 활용하여 반응적이고 효율적인 메시징 시스템을 만들 수 있었습니다 당신이 구축한 메시징 시스템은 추가 기능으로 향상시킬 수 있는 견고한 기반을 제공합니다 다음 단계 읽음 확인 메시지 읽음 상태 추적 구현 그룹 대화 시스템을 확장하여 여러 참가자를 지원 미디어 공유 이미지, 비디오 및 기타 첨부 파일 지원 추가 메시지 반응 사용자가 이모지로 메시지에 반응할 수 있도록 허용 메시지 삭제 메시지를 삭제하거나 편집할 수 있는 기능 구현 고급 검색 대화 내에서 검색 기능 추가 메시징 시스템을 포함한 back4gram 소셜 네트워크 애플리케이션의 전체 코드는 github 저장소 https //github com/templates back4app/back4gram back4app의 데이터베이스, api, 클라우드 기능, 파일 저장소, 사용자 관리 및 실시간 기능의 조합은 기능이 풍부한 소셜 네트워크 애플리케이션을 구축하는 데 탁월한 선택이 됩니다 back4app을 사용하면 복잡한 백엔드 인프라를 관리하는 대신 훌륭한 사용자 경험을 만드는 데 집중할 수 있습니다