Project Templates
Social Network
How to build a backend for a social network?
48 분
소개 이 가이드에서는 서비스로서의 백엔드 플랫폼 이 백엔드는 사용자 인증, 데이터 관리, 실시간 통신 등을 처리하도록 설계되며, 보안과 효율성을 보장합니다 이 튜토리얼이 끝나면, 당신의 프로젝트는 다음과 같을 것입니다 소셜 네트워크 개요 팁 전체 소스 코드는 github https //github com/templates back4app/back4gram 에서 확인하세요 주요 내용 간소화된 백엔드 관리 back4app의 관리형 백엔드를 활용하여 사용자 인증, 데이터 저장 및 실시간 기능을 간소화합니다 실시간 상호작용 livequery를 활용하여 동적 메시징 및 즉각적인 콘텐츠 업데이트를 가능하게 합니다 포괄적인 사용자 및 콘텐츠 처리 사용자 프로필, 게시물, 댓글 및 사회적 상호작용을 위한 강력한 시스템을 구축합니다 확장성 및 성능 쿼리 최적화, 캐싱 및 배치 작업을 적용하여 네트워크가 성장함에 따라 높은 성능을 유지합니다 고급 통합 더 풍부한 사용자 경험을 위해 클라우드 기능, 백그라운드 작업 및 푸시 알림으로 백엔드를 강화합니다 전제 조건 시작하기 전에 다음을 확인하세요 back4app 계정 무료로 등록하려면 back4app com https //www back4app com/ node js 및 npm 에서 node js (v14 x 이상) 설치 nodejs org https //nodejs org/ javascript 및 react에 대한 기본 지식 코드 편집기 visual studio code 또는 sublime text와 같은 편집기를 사용하세요 1단계 — back4app 백엔드 구성하기 back4app에서 새 프로젝트 만들기 back4app 대시보드에 로그인하세요 클릭하세요 "새 앱 만들기" 앱의 이름을 정하세요 (예 "back4social") 그리고 가장 가까운 서버 지역을 선택하세요 프로젝트가 생성되면 back4app 대시보드에 나열된 것을 볼 수 있습니다 이 프로젝트는 이 튜토리얼에서 논의된 모든 백엔드 구성의 기초가 될 것입니다 데이터베이스 스키마 설계 귀하의 소셜 네트워크 백엔드는 다음 클래스를 사용할 것입니다 사용자 (기본적으로 parse에서 제공됨) 약력 및 프로필 사진과 같은 필드로 확장 게시물 텍스트 콘텐츠 및 이미지 업로드를 보유 댓글 게시물에 대한 사용자 댓글을 저장 대화 사용자 간의 채팅 세션을 나타냅니다 메시지 대화에서 개별 메시지를 포함 타이핑 상태 사용자가 입력 중일 때를 나타냅니다 데이터베이스 클래스 추가하기 back4app에서는 데이터가 클래스 에 저장됩니다 back4app 대시보드에서 새 클래스를 만들거나 ai 에이전트를 통해 만들 수 있습니다 대시보드를 사용하여 생성하려면 다음을 수행하십시오 “데이터베이스” 섹션으로 이동하십시오 back4app 대시보드에서 새 클래스를 생성하십시오 제목(string) 및 iscompleted(boolean)과 같은 관련 열을 추가하십시오 귀하의 소셜 네트워크 백엔드는 다음 클래스를 사용할 것입니다 데이터베이스를 설정하려면 대시보드에서 "데이터베이스" 섹션으로 이동하세요 사용자 클래스를 향상시키다 기존 사용자 클래스를 엽니다 열 추가 바이오 (문자열) 아바타 (파일) 팔로워 (숫자, 기본값 0) 다음 (숫자, 기본값 0) 게시물 클래스 만들기 클릭 "클래스 만들기" 이름을 정하세요 게시물 을 추가하세요 내용 (문자열) 저자 (사용자에 대한 포인터 user ) 이미지 (파일) 좋아요 (숫자, 기본값 0) 좋아요 (배열) 댓글 클래스 만들기 이름을 정하세요 댓글 및 포함 내용 (문자열) 저자 (사용자에 대한 포인터 사용자 ) 게시물 (게시물에 대한 포인터) 대화 클래스 만들기 이름을 정하세요 대화 를 추가하세요 참가자 (배열) 마지막 메시지 (문자열) 메시지 클래스를 생성하십시오 이름을 정하세요 메시지 를 추가하세요 텍스트 (문자열) 발신자 (포인터에서 사용자 ) 대화 (대화에 대한 포인터) typingstatus 클래스를 생성합니다 이름을 typingstatus 추가하세요 사용자 (포인터에서 사용자 ) 대화 (대화에 대한 포인터) 입력 중 (부울) ai 에이전트를 사용하여 생성하려면 다음을 수행하십시오 ai 에이전트 열기 앱 대시보드 또는 메뉴에서 데이터 모델 설명하기 간단한 언어로 ai 에이전트가 스키마를 생성하도록 하세요 자동으로 클래스 권한 구성 데이터를 보호하기 위해 클래스 수준 권한(clps)을 조정하십시오 대시보드에서 "보안 및 키" 로 이동하십시오 "클래스 수준 보안" 아래에서 , 읽기/쓰기 규칙을 설정하십시오 예를 들어, 게시물 에 대해 공개 읽기 액세스를 허용하고 게시물 작성자에게 업데이트/삭제 작업을 제한하십시오 실시간 기능 활성화하기 livequery "서버 설정"에서 "parse server" 를 찾아 livequery 를 활성화하세요 실시간 모니터링을 위해 다음 클래스를 포함하세요 메시지 타이핑 상태 게시물 (좋아요 및 댓글을 실시간으로 업데이트하기 위해) livequery 설정 애플리케이션 키를 가져오세요 "앱 설정" > "보안 및 키" 로 이동하세요 다음 정보를 저장하세요 애플리케이션 id , javascript 키 , 서버 url , 및 livequery 서버 url 보안 키 2단계 — 프론트엔드와 back4app 연결하기 환경 변수 설정 프로젝트의 루트 디렉토리에 env local 파일을 생성하세요 react app parse app id=your application id react app parse js key=your javascript key react app parse server url=https //parseapi back4app com react app parse live query url=wss\ //your app id back4app io 자리 표시자를 실제 자격 증명으로 교체하세요 parse sdk 구성 parse를 초기화하기 위한 구성 파일을 생성하세요 // src/utils/parseconfig js import parse from 'parse/dist/parse min js'; // initialize parse with your environment variables parse initialize( process env react app parse app id, process env react app parse js key ); parse serverurl = process env react app parse server url; // enable livequery if configured if (process env react app parse live query url) { parse livequeryserverurl = process env react app parse live query url; } export default parse; 그런 다음 이 파일을 앱의 진입점(예 index js )에 가져오세요 // src/index js import react from 'react'; import reactdom from 'react dom/client'; import ' /index css'; import app from ' /app'; import ' /utils/parseconfig'; // initialize parse const root = reactdom createroot(document getelementbyid('root')); root render( \<react strictmode> \<app /> \</react strictmode> ); 3단계 — 사용자 인증 설정 back4app의 parse server는 parse user 클래스를 통해 내장된 사용자 관리 시스템을 제공합니다 parse가 사용자를 관리하는 방법 사용자 자격 증명(사용자 이름, 이메일, 비밀번호)을 처리합니다 인증 상태 및 세션 토큰을 관리합니다 등록 및 로그인 프로세스를 간소화합니다 사용자 등록 예제 // function to handle user sign up const handlesignup = async () => { try { const user = new parse user(); user set('username', username); user set('email', email); user set('password', password); // set additional user info user set('bio', ''); user set('followers', 0); user set('following', 0); await user signup(); console log('registration successful'); navigate('/feed'); } catch (error) { console error('sign up error ', error message); // handle errors based on error codes if (error code === 202) { seterrors({ errors, username 'username is taken'}); } else if (error code === 203) { seterrors({ errors, email 'email already exists'}); } } }; 사용자 로그인 예제 // function to handle user login const handlelogin = async () => { try { const user = await parse user login(username, password); console log('login successful ', user getusername()); navigate('/feed'); } catch (error) { console error('login error ', error message); setloginerror(error message); } }; 기존 세션 확인 const checkcurrentuser = async () => { try { const currentuser = parse user current(); if (currentuser) { console log('logged in as ', currentuser getusername()); return currentuser; } return null; } catch (error) { console error('error checking user ', error); return null; } }; 비밀번호 재설정 구현 const handlepasswordreset = async () => { try { await parse user requestpasswordreset(email); console log('reset email sent'); setresetemailsent(true); } catch (error) { console error('password reset error ', error message); setreseterror(error message); } }; 4단계 — 게시물 처리 새 게시물 만들기 const createpost = async () => { if (!postcontent trim() && !postimage) { console error('please add content or an image'); return; } try { const post = parse object extend('post'); const newpost = new post(); newpost set('content', postcontent); newpost set('author', parse user current()); newpost set('likes', 0); newpost set('likedby', \[]); if (postimage) { const parsefile = new parse file(postimage name, postimage); await parsefile save(); newpost set('image', parsefile); } await newpost save(); console log('post created successfully'); return newpost; } catch (error) { console error('error creating post ', error message); throw error; } }; 기존 세션 확인 게시물 가져오기 const fetchposts = async (page = 0, limit = 10) => { try { const post = parse object extend('post'); const query = new parse query(post); query include('author'); query descending('createdat'); query limit(limit); query skip(page limit); const results = await query find(); const posts = results map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, likedby post get('likedby') || \[], createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); return posts; } catch (error) { console error('error fetching posts ', error); throw error; } }; 좋아요/좋아요 취소 기능 구현하기 const togglelike = async (postid) => { try { const currentuser = parse user current(); const userid = currentuser id; const post = parse object extend('post'); const query = new parse query(post); const post = await query get(postid); const likedby = post get('likedby') || \[]; const isliked = likedby includes(userid); if (isliked) { post set('likedby', likedby filter(id => id !== userid)); post set('likes', math max((post get('likes') || 1) 1, 0)); } else { post set('likedby', \[ likedby, userid]); post set('likes', (post get('likes') || 0) + 1); } await post save(); return !isliked; } catch (error) { console error('error toggling like ', error); throw error; } }; 5단계 — 게시물에 댓글 달기 활성화 댓글 추가하기 const addcomment = async (postid, commentcontent) => { if (!commentcontent trim()) { console error('comment cannot be empty'); return; } try { const post = parse object extend('post'); const postquery = new parse query(post); const post = await postquery get(postid); const comment = parse object extend('comment'); const comment = new comment(); comment set('content', commentcontent); comment set('author', parse user current()); comment set('post', post); await comment save(); console log('comment added successfully'); return comment; } catch (error) { console error('error adding comment ', error); throw error; } }; 특정 게시물에 대한 댓글 가져오기 const fetchcomments = async (postid) => { try { const post = parse object extend('post'); const postquery = new parse query(post); const post = await postquery get(postid); const comment = parse object extend('comment'); const query = new parse query(comment); query equalto('post', post); query include('author'); query ascending('createdat'); const results = await query find(); const comments = results map(comment => ({ id comment id, content comment get('content'), createdat comment get('createdat'), author { id comment get('author') id, username comment get('author') get('username'), avatar comment get('author') get('avatar') ? comment get('author') get('avatar') url() null } })); return comments; } catch (error) { console error('error fetching comments ', error); throw error; } }; 6단계 — 사용자 프로필 관리 사용자 프로필 가져오기 const fetchuserprofile = async (userid) => { try { const query = new parse query(parse user); const user = await query get(userid); const post = parse object extend('post'); const postsquery = new parse query(post); postsquery equalto('author', user); postsquery include('author'); postsquery descending('createdat'); const posts = await postsquery find(); const userdata = { id user id, username user get('username'), bio user get('bio') || '', avatar user get('avatar') ? user get('avatar') url() null, followers user get('followers') || 0, following user get('following') || 0, posts posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') })) }; return userdata; } catch (error) { console error('error fetching user profile ', error); throw error; } }; 사용자 프로필 업데이트 const updateuserprofile = async (profiledata) => { try { const currentuser = parse user current(); if (profiledata bio !== undefined) { currentuser set('bio', profiledata bio); } if (profiledata avatarfile) { const parsefile = new parse file('avatar jpg', profiledata avatarfile); await parsefile save(); currentuser set('avatar', parsefile); } await currentuser save(); console log('profile updated successfully'); return currentuser; } catch (error) { console error('error updating profile ', error); throw error; } }; 7단계 — 실시간 메시징 구현 back4app의 livequery 기능은 실시간 메시징 및 입력 표시기를 가능하게 합니다 대화 만들기 const createconversation = async (participantids) => { try { const currentuser = parse user current(); const allparticipantids = \[ new set(\[currentuser id, participantids])]; const existingconversation = await findexistingconversation(allparticipantids); if (existingconversation) { return existingconversation; } const participantpointers = await promise all( allparticipantids map(async (id) => { const userquery = new parse query(parse user); return await userquery get(id); }) ); const conversation = parse object extend('conversation'); const conversation = new conversation(); conversation set('participants', participantpointers); conversation set('lastmessage', ''); await conversation save(); console log('conversation created successfully'); return conversation; } catch (error) { console error('error creating conversation ', error); throw error; } }; // helper to find an existing conversation (simplified for demo) const findexistingconversation = async (participantids) => { try { const conversation = parse object extend('conversation'); const query = new parse query(conversation); const results = await query find(); for (const conversation of results) { const participants = conversation get('participants') || \[]; const ids = participants map(p => p id); if (ids length === participantids length && ids every(id => participantids includes(id))) { return conversation; } } return null; } catch (error) { console error('error finding conversation ', error); return null; } }; 메시지 보내기 const sendmessage = async (conversationid, messagetext) => { try { const currentuser = parse user current(); const conversation = parse object extend('conversation'); const conversationquery = new parse query(conversation); const conversation = await conversationquery get(conversationid); const message = parse object extend('message'); const message = new message(); message set('text', messagetext); message set('sender', currentuser); message set('conversation', conversation); message set('read', false); await message save(); conversation set('lastmessage', messagetext); await conversation save(); console log('message sent successfully'); return message; } catch (error) { console error('error sending message ', error); throw error; } }; 라이브 쿼리를 통한 메시지 업데이트 구독 const subscribetomessages = async (conversationid, onnewmessage) => { try { const message = parse object extend('message'); const query = new parse query(message); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; query equalto('conversation', conversationpointer); query include('sender'); const subscription = await query subscribe(); subscription on('create', (message) => { const newmessage = { id message id, text message get('text'), createdat message get('createdat'), sender { id message get('sender') id, username message get('sender') get('username'), avatar message get('sender') get('avatar') ? message get('sender') get('avatar') url() null }, read message get('read') }; onnewmessage(newmessage); if (message get('sender') id !== parse user current() id) { markmessageasread(message); } }); return subscription; } catch (error) { console error('error subscribing to messages ', error); throw error; } }; const markmessageasread = async (message) => { try { message set('read', true); await message save(); } catch (error) { console error('error marking message as read ', error); } }; 타이핑 표시 구현하기 const updatetypingstatus = async (conversationid, istyping) => { try { const currentuser = parse user current(); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); query equalto('conversation', conversationpointer); query equalto('user', currentuser); let typingstatus = await query first(); if (typingstatus) { typingstatus set('istyping', istyping); } else { typingstatus = new typingstatus(); typingstatus set('conversation', conversationpointer); typingstatus set('user', currentuser); typingstatus set('istyping', istyping); } await typingstatus save(); } catch (error) { console error('error updating typing status ', error); } }; const subscribetotypingstatus = async (conversationid, ontypingstatuschange) => { try { const typingstatus = parse object extend('typingstatus'); const query = new parse query(typingstatus); const conversation = parse object extend('conversation'); const conversationpointer = new conversation(); conversationpointer id = conversationid; query equalto('conversation', conversationpointer); query include('user'); const subscription = await query subscribe(); subscription on('update', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); ontypingstatuschange({ userid user id, username user get('username'), istyping }); }); subscription on('create', (typingstatus) => { const user = typingstatus get('user'); const istyping = typingstatus get('istyping'); ontypingstatuschange({ userid user id, username user get('username'), istyping }); }); return subscription; } catch (error) { console error('error subscribing to typing status ', error); throw error; } }; 8단계 — 검색 기능 추가 사용자 검색 const searchusers = async (query, limit = 20) => { try { const userquery = new parse query(parse user); userquery matches('username', new regexp(query, 'i')); userquery limit(limit); const users = await userquery find(); const userresults = users map(user => ({ id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null, bio user get('bio') || '' })); return userresults; } catch (error) { console error('error searching users ', error); throw error; } }; 게시물 검색 const searchposts = async (query, limit = 20) => { try { const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(query, 'i')); postquery include('author'); postquery descending('createdat'); postquery limit(limit); const posts = await postquery find(); const postresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); return postresults; } catch (error) { console error('error searching posts ', error); throw error; } }; 해시태그 검색 const searchhashtags = async (tag, limit = 20) => { try { const hashtagquery = tag startswith('#') ? tag substring(1) tag; const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(`#${hashtagquery}\\\b`, 'i')); postquery include('author'); postquery descending('createdat'); postquery limit(limit); const posts = await postquery find(); const hashtagresults = posts map(post => ({ id post id, content post get('content'), image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat'), author { id post get('author') id, username post get('author') get('username'), avatar post get('author') get('avatar') ? post get('author') get('avatar') url() null } })); return hashtagresults; } catch (error) { console error('error searching hashtags ', error); throw error; } }; 9단계 — 고급 back4app 기능 활용 back4app은 백엔드를 풍부하게 하기 위해 cloud functions, 백그라운드 작업 및 후크와 같은 고급 도구를 제공합니다 서버 측 논리를 위한 cloud functions // cloud function example to notify users when a comment is added parse cloud aftersave("comment", async (request) => { if (request original) return; const comment = request object; const post = comment get("post"); const commenter = request user; const postquery = new parse query("post"); const fullpost = await postquery get(post id, { usemasterkey true }); const postauthor = fullpost get("author"); if (postauthor id === commenter id) return; const notification = parse object extend("notification"); const notification = new notification(); notification set("type", "comment"); notification set("fromuser", commenter); notification set("touser", postauthor); notification set("post", post); notification set("read", false); await notification save(null, { usemasterkey true }); }); cloud code를 이용한 고급 검색 parse cloud define("advancedsearch", async (request) => { const { query, type, limit = 20 } = request params; if (!query) { throw new error("search query is required"); } let results = \[]; switch (type) { case 'users' const userquery = new parse query(parse user); userquery matches('username', new regexp(query, 'i')); userquery limit(limit); results = await userquery find({ usemasterkey true }); break; case 'posts' const post = parse object extend('post'); const postquery = new parse query(post); postquery matches('content', new regexp(query, 'i')); postquery include('author'); postquery limit(limit); results = await postquery find({ usemasterkey true }); break; default throw new error("invalid search type"); } return results; }); 정기 작업을 위한 백그라운드 작업 parse cloud job("calculatetrendingtopics", async () => { const post = parse object extend("post"); const query = new parse query(post); const oneweekago = new date(); oneweekago setdate(oneweekago getdate() 7); query greaterthan('createdat', oneweekago); query limit(1000); const posts = await query find({ usemasterkey true }); const hashtagcounts = {}; posts foreach(post => { const content = post get('content') || ''; const hashtags = content match(/#(\w+)/g) || \[]; hashtags foreach(hashtag => { const tag = hashtag tolowercase(); hashtagcounts\[tag] = (hashtagcounts\[tag] || 0) + 1; }); }); const trendingarray = object entries(hashtagcounts) map((\[hashtag, count]) => ({ hashtag, count })) sort((a, b) => b count a count) slice(0, 10); const trendingtopics = parse object extend("trendingtopics"); const trending = new trendingtopics(); trending set("topics", trendingarray); trending set("calculatedat", new date()); await trending save(null, { usemasterkey true }); return "trending topics calculated successfully"; }); 추가 고급 기능 푸시 알림 back4app의 푸시 서비스를 사용하여 사용자에게 새 메시지에 대해 알립니다 역할 기반 접근 제어 콘텐츠 조정을 위한 작업에 대한 권한을 관리하기 위해 역할을 정의합니다 웹훅 클라우드 코드에서 웹훅을 트리거하여 외부 서비스를 통합합니다 단계 10 — 성능 최적화 백엔드가 확장됨에 따라 성능 조정이 중요합니다 다음 기술을 고려하십시오 데이터베이스 인덱싱 자주 쿼리되는 필드(예 author , createdat , username )에 인덱스를 생성하여 쿼리 성능을 향상시킵니다 쿼리 최적화 필요한 필드만 선택하고 페이지네이션을 사용하여 결과 집합을 제한하여 쿼리를 최적화합니다 배치 작업 api 호출을 최소화하기 위해 배치 업데이트 및 삭제를 활용하십시오 const updatemultipleposts = async (postids, updatedata) => { try { const post = parse object extend('post'); const posts = postids map(id => { const post = new post(); post id = id; return post; }); posts foreach(post => { object entries(updatedata) foreach((\[key, value]) => { post set(key, value); }); }); await parse object saveall(posts); console log('posts updated successfully'); } catch (error) { console error('error updating posts ', error); throw error; } }; 캐싱 전략 api 요청을 줄이기 위해 자주 접근하는 데이터에 대해 클라이언트 측 캐싱을 구현하십시오 효율적인 라이브 쿼리 사용 필요한 데이터에만 구독하고 데이터가 더 이상 필요하지 않을 때 항상 구독을 취소하십시오 파일 처리 최적화 대역폭과 저장 공간을 절약하기 위해 업로드 전에 이미지를 크기 조정하십시오 const uploadresizedimage = async (originalfile, maxwidth = 1200, maxheight = 1200) => { return new promise((resolve, reject) => { try { const reader = new filereader(); reader onload = (event) => { const img = new image(); img onload = () => { let width = img width; let height = img height; if (width > maxwidth) { height = math round(height (maxwidth / width)); width = maxwidth; } if (height > maxheight) { width = math round(width (maxheight / height)); height = maxheight; } const canvas = document createelement('canvas'); canvas width = width; canvas height = height; const ctx = canvas getcontext('2d'); ctx drawimage(img, 0, 0, width, height); canvas toblob(async (blob) => { const resizedfile = new file(\[blob], originalfile name, { type originalfile type, lastmodified date now() }); const parsefile = new parse file(resizedfile name, resizedfile); await parsefile save(); resolve(parsefile); }, originalfile type, 0 8); }; img src = event target result; }; reader readasdataurl(originalfile); } catch (error) { reject(error); } }); }; 모니터링 및 확장 back4app의 대시보드 분석 및 로그를 사용하여 api 사용량 및 성능을 모니터링하세요 사용자 기반이 성장함에 따라 요금제를 업그레이드하거나 샤딩 및 cdn 솔루션을 구현하는 것을 고려하세요 결론 이 튜토리얼에서는 back4app을 사용하여 소셜 네트워크 백엔드를 설정하는 방법을 배웠으며, 사용자 인증, 게시물 관리, 실시간 메시징, 검색 기능 및 고급 최적화 기술을 다루었습니다 또한 소셜 미디어 앱 개발 방법 에 대한 블로그 게시물을 읽어보세요 코딩을 즐기시고 소셜 네트워크 백엔드에 행운이 있기를 바랍니다!