Project Templates
Social Network
소셜 네트워크를 위한 피드 및 상호작용
38 분
소개 이 튜토리얼에서는 back4app을 백엔드 서비스로 사용하여 게시물, 댓글 및 좋아요와 같은 인터랙티브 기능을 갖춘 소셜 네트워크 피드를 구축하는 방법을 배웁니다 이러한 기능은 모든 소셜 네트워킹 애플리케이션의 핵심을 형성하여 사용자가 콘텐츠를 공유하고 다른 사용자와 소통할 수 있게 합니다 back4app은 parse server를 기반으로 구축된 backend as a service (baas) 플랫폼으로, 개발자에게 서버 측 코드를 관리하지 않고도 확장 가능한 애플리케이션을 만들 수 있는 강력한 인프라를 제공합니다 실시간 데이터베이스 기능과 내장된 사용자 관리 기능 덕분에 소셜 네트워킹 애플리케이션 개발에 탁월한 선택입니다 이 튜토리얼이 끝나면 사용자가 다음을 수행할 수 있는 완전한 기능의 소셜 피드를 구현하게 됩니다 텍스트와 이미지를 포함한 게시물 작성 피드에서 다른 사용자의 게시물 보기 게시물 좋아요 게시물에 댓글 달기 댓글과 함께 게시물 세부정보 보기 효율적으로 데이터베이스 클래스를 구조화하고, 실시간 업데이트를 구현하며, 장치 간 원활한 경험을 제공하는 반응형 사용자 인터페이스를 만드는 방법을 배웁니다 전제 조건 이 튜토리얼을 완료하려면 다음이 필요합니다 back4app 계정 back4app com https //www back4app com back4app 프로젝트 설정 새로운 프로젝트를 만드는 방법은 우리의 back4app 시작하기 가이드 https //www back4app com/docs/get started/welcome 로컬 머신에 node js가 설치되어 있어야 합니다 javascript 및 react js에 대한 기본 지식 작동하는 인증 시스템 아직 설정하지 않았다면, 우리의 소셜 네트워크를 위한 인증 시스템 튜토리얼을 먼저 따라하세요 현대 웹 개발 개념(컴포넌트, 상태 관리 등)에 대한 친숙함 back4gram 프로젝트 여기 에서 완전한 코드를 찾으세요 소셜 네트워크 샘플 프로젝트 back4app으로 구축된 1단계 – 데이터 모델 이해하기 코드를 작성하기 전에, 우리의 소셜 네트워크에 대한 데이터 모델을 이해하는 것이 중요합니다 피드 기능을 구축하기 위해 여러 개의 parse 클래스가 필요합니다 게시물 클래스 게시물 클래스는 모든 게시물 관련 정보를 저장합니다 작성자 게시물을 작성한 사용자를 가리키는 포인터 내용 게시물의 텍스트 내용 이미지 선택적 이미지 파일 좋아요 게시물이 받은 좋아요 수를 나타내는 숫자 댓글 댓글 객체에 대한 포인터 배열 (선택적, 별도로 쿼리 가능) 생성일 back4app에 의해 자동으로 관리됨 수정일 back4app에 의해 자동으로 관리됨 댓글 클래스 이 댓글 클래스는 게시물에 대한 댓글을 저장합니다 작성자 댓글을 작성한 사용자에 대한 포인터 게시물 댓글이 달린 게시물에 대한 포인터 내용 댓글의 텍스트 내용 생성일 back4app에 의해 자동으로 관리됨 수정일 back4app에 의해 자동으로 관리됨 좋아요 추적 좋아요를 추적하는 두 가지 옵션이 있습니다 간단한 카운터 각 게시물에 대한 좋아요 수를 저장합니다 (더 쉽지만 덜 상세함) 좋아요 기록 각 개별 좋아요를 추적하는 별도의 좋아요 클래스를 생성합니다 (더 상세함) 이 튜토리얼에서는 더 간단한 카운터부터 시작하여 각 개별 좋아요 기록을 구현하는 방법을 보여주며 두 가지 접근 방식을 모두 구현할 것입니다 이제 back4app에서 이러한 클래스를 설정해 보겠습니다 2단계 – back4app에서 데이터 모델 설정하기 이 단계에서는 back4app 데이터베이스에 필요한 클래스를 생성합니다 게시물 클래스 생성하기 back4app 대시보드에 로그인하고 프로젝트로 이동합니다 왼쪽 사이드바에서 "데이터베이스"를 클릭하여 데이터베이스 브라우저를 엽니다 페이지 상단의 "클래스 생성" 버튼을 클릭합니다 나타나는 모달에서 "post"를 클래스 이름으로 입력하고 유형으로 "custom"을 선택합니다 그런 다음 "클래스 생성"을 클릭합니다 \[image back4app create class modal with "post" entered as class name] 이제 post 클래스에 다음 열을 추가하세요 "열 추가" 클릭 다음 열을 생성하세요 작성자 (유형 포인터, 대상 클래스 user) 내용 (유형 문자열) 이미지 (유형 파일) 좋아요 (유형 숫자, 기본값 0) 댓글 클래스 생성하기 "클래스 생성"을 다시 클릭하세요 "comment"를 클래스 이름으로 입력하고 "custom"을 유형으로 선택하세요 다음 열을 추가하세요 작성자 (유형 포인터, 대상 클래스 user) 게시물 (유형 포인터, 대상 클래스 post) 내용 (유형 문자열) 클래스 수준 권한 설정 보안을 위해 클래스에 적절한 권한을 설정합시다 데이터베이스 브라우저에서 post 클래스를 선택합니다 "보안" 버튼을 클릭합니다 권한을 구성합니다 공개 읽기 예 (누구나 게시물을 볼 수 있음) 공개 쓰기 아니오 (인증된 사용자만 게시물을 생성할 수 있음) 공개 필드 추가 아니오 찾기, 가져오기, 생성하기, 업데이트, 삭제 사용자 인증 필요 comment 클래스에 대해 유사한 설정을 반복합니다 이제 데이터 모델이 설정되었으므로, 소셜 네트워크 피드를 위한 프론트엔드 구성 요소를 구현합시다 피드 페이지는 모든 소셜 네트워크의 핵심입니다 사용자로부터의 게시물을 표시하고 상호작용을 허용합니다 게시물을 가져오고 표시하는 피드 컴포넌트를 만들어 봅시다 파일을 생성하십시오 src/pages/feedpage js import react, { usestate, useeffect, useref } from 'react'; import { usenavigate } from 'react router dom'; import { box, flex, vstack, heading, text, button, input, simplegrid, avatar, hstack, iconbutton, textarea, spinner, center, image, } from '@chakra ui/react'; import { link as routerlink } from 'react router dom'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function feedpage() { const \[searchquery, setsearchquery] = usestate(''); const \[newpostcontent, setnewpostcontent] = usestate(''); const \[posts, setposts] = usestate(\[]); const \[isloading, setisloading] = usestate(true); const \[isposting, setisposting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // new state variables for image upload const \[selectedimage, setselectedimage] = usestate(null); const \[imagepreview, setimagepreview] = usestate(null); const fileinputref = useref(null); const navigate = usenavigate(); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { // redirect to login if not authenticated navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // handle image selection const handleimagechange = (e) => { if (e target files && e target files\[0]) { const file = e target files\[0]; setselectedimage(file); // create a preview url const reader = new filereader(); reader onloadend = () => { setimagepreview(reader result); }; reader readasdataurl(file); } }; // clear selected image const handleclearimage = () => { setselectedimage(null); setimagepreview(null); if (fileinputref current) { fileinputref current value = ''; } }; // fetch posts useeffect(() => { const fetchposts = async () => { if (!currentuser) { console log('no current user, skipping post fetch'); return; } setisloading(true); console log('fetching posts for user ', currentuser id); try { // create a query for the post class const query = new parse query('post'); console log('created post query'); // include the user who created the post query include('author'); console log('including author in query'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query console log('executing query '); const results = await query find(); console log('query results received ', results length, 'posts found'); // convert parse objects to plain objects const fetchedposts = \[]; for (let i = 0; i < results length; i++) { const post = results\[i]; try { const author = post get('author'); console log(`processing post ${i+1}/${results length}, author `, author ? author id 'null'); if (!author) { console warn(`post ${post id} has no author, skipping`); continue; } const postobj = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; fetchedposts push(postobj); } catch (posterror) { console error(`error processing post ${i+1} `, posterror); console error('post data ', post tojson()); } } console log('successfully processed', fetchedposts length, 'posts'); setposts(fetchedposts); } catch (error) { console error('error fetching posts ', error); console error('error details ', error code, error message); toaster create({ title 'error loading posts', description error message, type 'error', }); } finally { setisloading(false); console log('post loading completed'); } }; fetchposts(); }, \[currentuser, navigate]); // function to create a new post with image const handlecreatepost = async () => { if (!newpostcontent trim() && !selectedimage) return; setisposting(true); console log('creating new post '); try { // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { console log('uploading image '); const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); console log('image uploaded successfully'); } console log('post object created, saving to database '); // save the post const savedpost = await post save(); console log('post saved successfully with id ', savedpost id); // add the new post to the state const newpost = { id savedpost id, content savedpost get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, image savedpost get('image') ? savedpost get('image') url() null, likes 0, createdat savedpost get('createdat') }; console log('adding new post to state'); setposts(\[newpost, posts]); setnewpostcontent(''); setselectedimage(null); setimagepreview(null); toaster create({ title 'post created', description 'your post has been published successfully!', type 'success', }); } catch (error) { console error('error creating post ', error); console error('error details ', error code, error message); toaster create({ title 'error creating post', description error message, type 'error', }); } finally { setisposting(false); console log('post creation completed'); } }; // function to like a post const handlelikepost = async (postid) => { try { // get the post const query = new parse query('post'); const post = await query get(postid); // increment likes post increment('likes'); await post save(); // update the post in the state setposts(posts map(p => { if (p id === postid) { return { p, likes p likes + 1 }; } return p; })); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // function to logout const handlelogout = async () => { try { await parse user logout(); navigate('/login'); } catch (error) { console error('error logging out ', error); } }; const handlesearchsubmit = (e) => { e preventdefault(); if (searchquery trim()) { navigate(`/search?q=${encodeuricomponent(searchquery)}`); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; return ( \<flex direction="row" h="100vh"> {/ left sidebar (navigation) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderright="1px solid" bordercolor="gray 600" \> \<vstack align="stretch" spacing={4}> \<heading size="md">social network\</heading> \<button as={routerlink} to="/feed" variant="ghost" justifycontent="flex start"> home \</button> \<button as={routerlink} to="/search" variant="ghost" justifycontent="flex start"> search \</button> \<button as={routerlink} to="/messages" variant="ghost" justifycontent="flex start"> messages \</button> \<button as={routerlink} to="/profile" variant="ghost" justifycontent="flex start"> profile \</button> \<button onclick={handlelogout} variant="ghost" colorscheme="red" justifycontent="flex start"> logout \</button> \</vstack> \</box> {/ main content (feed) /} \<box flex="1" p={4} overflowy="auto"> \<form onsubmit={handlesearchsubmit}> \<input placeholder="search " value={searchquery} onchange={(e) => setsearchquery(e target value)} mb={4} /> \</form> {/ create post with image upload /} \<box border="1px solid" bordercolor="gray 600" p={4} borderradius="md" mb={6}> \<textarea placeholder="what's on your mind?" value={newpostcontent} onchange={(e) => setnewpostcontent(e target value)} mb={2} resize="none" /> {/ image preview /} {imagepreview && ( \<box position="relative" mb={2}> \<image src={imagepreview} alt="preview" maxh="200px" borderradius="md" /> \<button position="absolute" top="2" right="2" size="sm" colorscheme="red" onclick={handleclearimage} \> remove \</button> \</box> )} \<box> \<input type="file" accept="image/ " onchange={handleimagechange} style={{ display 'none' }} ref={fileinputref} id="image upload" /> \<button as="label" htmlfor="image upload" cursor="pointer" variant="outline" size="sm" mr={2} mb={0} \> 📷 add photo \</button> \</box> \<button colorscheme="blue" onclick={handlecreatepost} isloading={isposting} disabled={(!newpostcontent trim() && !selectedimage) || isposting} \> post \</button> \</box> {/ posts feed with images /} {isloading ? ( \<center py={10}> \<spinner size="xl" /> \</center> ) posts length > 0 ? ( \<vstack align="stretch" spacing={4}> {posts map(post => ( \<box key={post id} border="1px solid" bordercolor="gray 600" p={4} borderradius="md" \> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(post createdat)}\</text> \</hstack> \<text mb={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box mb={4}> \<image src={post image} alt="post image" borderradius="md" maxh="400px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" size="sm" onclick={() => handlelikepost(post id)} \> ❤️ {post likes} \</button> \<button variant="ghost" size="sm" as={routerlink} to={`/post/${post id}`} \> 💬 comment \</button> \<button variant="ghost" size="sm"> 🔄 share \</button> \</hstack> \</box> ))} \</vstack> ) ( \<text>no posts yet follow users or create your first post!\</text> )} \</box> {/ right sidebar (trending hashtags) /} \<box w={\['0px', '250px']} bg="gray 700" p={4} display={\['none', 'block']} borderleft="1px solid" bordercolor="gray 600" \> \<heading size="md" mb={4}> trending today \</heading> \<simplegrid columns={1} spacing={2}> \<button variant="outline" size="sm" colorscheme="whitealpha">#travel\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#tech\</button> \<button variant="outline" size="sm" colorscheme="whitealpha">#foodie\</button> \</simplegrid> \</box> \</flex> ); } export default feedpage; 이 피드 구성 요소에는 여러 가지 주요 기능이 포함되어 있습니다 인증 확인 로그인한 사용자만 피드를 볼 수 있도록 보장합니다 게시물 생성 사용자가 텍스트와 선택적 이미지를 포함한 새 게시물을 생성할 수 있도록 합니다 이미지 업로드 parse file을 사용하여 이미지 선택, 미리보기 및 업로드를 처리합니다 피드 표시 모든 사용자의 게시물을 역순으로 표시합니다 좋아요 기능 사용자가 간단한 카운터 방식으로 게시물에 좋아요를 누를 수 있도록 합니다 탐색 애플리케이션의 다른 부분으로의 링크를 제공합니다 back4app이 이러한 기능을 구현하는 데 어떻게 도움을 주는지 살펴보겠습니다 parse를 사용한 게시물 생성 back4app은 이미지를 포함한 게시물 생성을 간단하게 만듭니다 // create a new post object const post = parse object extend('post'); const post = new post(); // set post data post set('content', newpostcontent); post set('author', currentuser); post set('likes', 0); // handle image upload if an image is selected if (selectedimage) { const parsefile = new parse file(selectedimage name, selectedimage); await parsefile save(); post set('image', parsefile); } // save the post const savedpost = await post save(); parse file은 파일 업로드, 저장 및 url 생성을 자동으로 처리하여 게시물에 이미지를 추가하는 것을 쉽게 만듭니다 parse 쿼리를 사용한 게시물 가져오기 back4app의 쿼리 시스템은 게시물을 쉽게 가져오고 표시할 수 있게 해줍니다 // create a query for the post class const query = new parse query('post'); // include the user who created the post query include('author'); // sort by creation date, newest first query descending('createdat'); // limit to 20 posts query limit(20); // execute the query const results = await query find(); include('author') 메서드는 특히 강력합니다 이 메서드는 각 게시물과 함께 참조된 사용자 객체를 자동으로 포함하여 여러 쿼리의 필요성을 줄여줍니다 back4gram 프로젝트 전체 코드를 찾으려면 여기 에서 소셜 네트워크 샘플 프로젝트 를 확인하세요 back4app으로 구축되었습니다 4단계 – 댓글이 있는 게시물 상세 보기 구현 이제 댓글이 있는 단일 게시물을 표시하고 사용자가 새 댓글을 추가할 수 있는 게시물 상세 페이지를 만들어 보겠습니다 파일을 생성하세요 src/pages/postdetailspage js import react, { usestate, useeffect } from 'react'; import { useparams, usenavigate, link as routerlink } from 'react router dom'; import { box, flex, vstack, heading, text, button, avatar, hstack, textarea, spinner, center, image } from '@chakra ui/react'; import parse from 'parse/dist/parse min js'; import { toaster } from ' /components/ui/toaster'; function postdetailspage() { const { id } = useparams(); const navigate = usenavigate(); const \[post, setpost] = usestate(null); const \[comments, setcomments] = usestate(\[]); const \[newcomment, setnewcomment] = usestate(''); const \[isloading, setisloading] = usestate(true); const \[iscommenting, setiscommenting] = usestate(false); const \[currentuser, setcurrentuser] = usestate(null); // check if user is authenticated useeffect(() => { const checkauth = async () => { try { const user = await parse user current(); if (!user) { navigate('/login'); return; } setcurrentuser(user); } catch (error) { console error('error checking authentication ', error); navigate('/login'); } }; checkauth(); }, \[navigate]); // fetch post and comments useeffect(() => { const fetchpostdetails = async () => { if (!currentuser) return; setisloading(true); try { // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); // convert post to plain object const fetchedpost = { id postobject id, content postobject get('content'), author { id postobject get('author') id, username postobject get('author') get('username'), avatar postobject get('author') get('avatar') ? postobject get('author') get('avatar') url() null }, likes postobject get('likes') || 0, createdat postobject get('createdat'), image postobject get('image') ? postobject get('image') url() null }; // convert comments to plain objects const fetchedcomments = commentresults map(comment => ({ id comment id, content comment get('content'), 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 }, createdat comment get('createdat') })); setpost(fetchedpost); setcomments(fetchedcomments); } catch (error) { console error('error fetching post details ', error); toaster create({ title 'error loading post', description error message, type 'error', }); navigate('/feed'); } finally { setisloading(false); } }; if (id) { fetchpostdetails(); } }, \[id, currentuser, navigate]); // function to add a comment const handleaddcomment = async () => { if (!newcomment trim()) return; setiscommenting(true); try { // get the post object const postquery = new parse query('post'); const postobject = await postquery get(id); // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); // add the new comment to the state const newcommentobj = { id comment id, content comment get('content'), author { id currentuser id, username currentuser get('username'), avatar currentuser get('avatar') ? currentuser get('avatar') url() null }, createdat comment get('createdat') }; setcomments(\[ comments, newcommentobj]); setnewcomment(''); toaster create({ title 'comment added', description 'your comment has been posted successfully!', type 'success', }); } catch (error) { console error('error adding comment ', error); toaster create({ title 'error adding comment', description error message, type 'error', }); } finally { setiscommenting(false); } }; // function to like the post const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // increment likes postobject increment('likes'); await postobject save(); // update the post in the state setpost({ post, likes post likes + 1 }); } catch (error) { console error('error liking post ', error); toaster create({ title 'error', description 'could not like the post please try again ', type 'error', }); } }; // format date const formatdate = (date) => { return new date(date) tolocalestring(); }; if (isloading) { return ( \<center h="100vh"> \<spinner size="xl" /> \</center> ); } if (!post) { return ( \<center h="100vh"> \<vstack> \<text>post not found\</text> \<button as={routerlink} to="/feed" colorscheme="blue"> back to feed \</button> \</vstack> \</center> ); } return ( \<box maxw="800px" mx="auto" p={4}> \<button as={routerlink} to="/feed" mb={4} variant="ghost"> ← back to feed \</button> {/ post /} \<box border="1px solid" bordercolor="gray 600" p={6} borderradius="md" mb={6}> \<hstack mb={2}> \<avatar src={post author avatar} name={post author username} size="sm" /> \<vstack align="start" spacing={0}> \<text fontweight="bold">{post author username}\</text> \<text fontsize="sm" color="gray 400">{formatdate(post createdat)}\</text> \</vstack> \</hstack> \<text fontsize="lg" my={post image ? 2 4}>{post content}\</text> {/ display post image if available /} {post image && ( \<box my={4}> \<image src={post image} alt="post image" borderradius="md" maxh="500px" w="auto" /> \</box> )} \<hstack spacing={4}> \<button variant="ghost" onclick={handlelikepost} \> ❤️ {post likes} \</button> \<button variant="ghost"> 🔄 share \</button> \</hstack> \</box> {/ comments section /} \<box> \<heading size="md" mb={4}> comments ({comments length}) \</heading> {/ add comment /} \<box mb={6}> \<textarea placeholder="write a comment " value={newcomment} onchange={(e) => setnewcomment(e target value)} mb={2} /> \<button colorscheme="blue" onclick={handleaddcomment} isloading={iscommenting} disabled={!newcomment trim()} \> post comment \</button> \</box> \<hr style={{ marginbottom "1rem" }} /> {/ comments list /} {comments length > 0 ? ( \<vstack align="stretch" spacing={4}> {comments map(comment => ( \<box key={comment id} p={4} bg="gray 700" borderradius="md"> \<hstack mb={2}> \<avatar src={comment author avatar} name={comment author username} size="sm" /> \<text fontweight="bold">{comment author username}\</text> \<text fontsize="sm" color="gray 400">• {formatdate(comment createdat)}\</text> \</hstack> \<text>{comment content}\</text> \</box> ))} \</vstack> ) ( \<text color="gray 400">no comments yet be the first to comment!\</text> )} \</box> \</box> ); } export default postdetailspage; this post detail page provides several important features 1\ post display shows the complete post with author information, content, image, and likes 2\ comment creation allows users to add new comments to the post 3\ comment listing displays all comments in chronological order 4\ like functionality enables users to like the post let's examine the key aspects of how back4app is helping us implement these features \### fetching post details with related objects back4app makes it easy to fetch a post with related information ```javascript // get the post const query = new parse query('post'); query include('author'); const postobject = await query get(id); 그 include('author') 메서드는 관련된 사용자 객체를 게시물과 함께 자동으로 검색하여 별도의 쿼리가 필요 없도록 합니다 관련 댓글 쿼리하기 back4app의 쿼리 시스템을 사용하면 특정 게시물과 관련된 모든 댓글을 찾을 수 있습니다 // get comments const commentquery = new parse query('comment'); commentquery equalto('post', postobject); commentquery include('author'); commentquery ascending('createdat'); const commentresults = await commentquery find(); 그 equalto('post', postobject) 메서드는 댓글을 현재 게시물과 관련된 것만 필터링합니다 포인터로 댓글 만들기 게시물과의 관계를 가진 댓글을 만드는 것은 간단합니다 // create a new comment object const comment = parse object extend('comment'); const comment = new comment(); // set comment data comment set('content', newcomment); comment set('author', currentuser); comment set('post', postobject); // save the comment await comment save(); 그 게시물 필드는 게시물 객체에 대한 포인터로 설정되어 댓글과 게시물 간의 관계를 설정합니다 back4gram 프로젝트 찾기 여기 전체 코드는 소셜 네트워크 샘플 프로젝트 back4app으로 구축되었습니다 5단계 – parse로 고급 좋아요 기능 구현하기 지금까지 구현한 간단한 좋아요 카운터는 기본 기능에 잘 작동하지만, 몇 가지 한계가 있습니다 사용자는 게시물을 여러 번 좋아요 할 수 있습니다 사용자는 게시물을 "좋아요 취소"할 수 없습니다 우리는 누가 게시물을 좋아했는지 보여줄 수 없습니다 별도의 좋아요 클래스를 사용하여 개별 좋아요를 추적하는 더 고급 좋아요 시스템을 만들어 보겠습니다 좋아요 클래스 설정하기 먼저, back4app 대시보드에서 새 클래스를 생성합니다 데이터베이스 브라우저에서 "클래스 만들기"를 클릭하세요 클래스 이름으로 "like"를 입력하고 유형으로 "custom"을 선택하세요 다음 열을 추가하세요 user (유형 포인터, 대상 클래스 user) post (유형 포인터, 대상 클래스 post) 우리 코드에서 좋아요 관리 구현하기 이제 postdetailspage js 파일에서 게시물 좋아요 기능을 업데이트해 보겠습니다 // add this state at the beginning of the component const \[hasliked, sethasliked] = usestate(false); // add this to the useeffect that fetches post details // check if current user has liked this post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const userlike = await likequery first(); sethasliked(!!userlike); // replace the handlelikepost function with this new version const handlelikepost = async () => { try { // get the post const query = new parse query('post'); const postobject = await query get(id); // check if user has already liked the post const likequery = new parse query('like'); likequery equalto('user', currentuser); likequery equalto('post', postobject); const existinglike = await likequery first(); if (existinglike) { // user already liked the post, so remove the like await existinglike destroy(); // decrement likes count on the post postobject increment('likes', 1); await postobject save(); // update ui sethasliked(false); setpost({ post, likes post likes 1 }); } else { // user hasn't liked the post yet, so add a like const like = parse object extend('like'); const like = new like(); like set('user', currentuser); like set('post', postobject); await like save(); // increment likes count on the post postobject increment('likes'); await postobject save(); // update ui sethasliked(true); setpost({ post, likes post likes + 1 }); } } catch (error) { console error('error toggling like ', error); toaster create({ title 'error', description 'could not process your like please try again ', type 'error', }); } }; 그런 다음 현재 상태를 반영하도록 좋아요 버튼을 업데이트합니다 \<button variant="ghost" onclick={handlelikepost} color={hasliked ? "red 500" "inherit"} \> {hasliked ? "❤️" "🤍"} {post likes} \</button> 이 구현 개별 좋아요를 별도의 좋아요 클래스 사용자가 좋아요를 전환할 수 있도록 허용합니다 (좋아요 및 좋아요 취소) 현재 사용자가 게시물을 좋아했는지 여부를 시각적으로 표시합니다 효율적인 쿼리를 위해 게시물의 좋아요 수를 유지합니다 게시물을 좋아한 사람 표시하기 게시물을 좋아한 사람을 표시하는 기능을 추가할 수 있습니다 // add this state const \[likeusers, setlikeusers] = usestate(\[]); const \[showlikes, setshowlikes] = usestate(false); // add this function const fetchlikes = async () => { try { const postquery = new parse query('post'); const postobject = await postquery get(id); const likequery = new parse query('like'); likequery equalto('post', postobject); likequery include('user'); const likes = await likequery find(); const users = likes map(like => { const user = like get('user'); return { id user id, username user get('username'), avatar user get('avatar') ? user get('avatar') url() null }; }); setlikeusers(users); setshowlikes(true); } catch (error) { console error('error fetching likes ', error); } }; 그런 다음 게시물을 좋아한 사용자를 표시하는 ui 요소를 추가하세요 \<text color="blue 400" cursor="pointer" onclick={fetchlikes} fontsize="sm" \> see who liked this post \</text> {showlikes && ( \<box mt={2} p={2} bg="gray 700" borderradius="md"> \<heading size="xs" mb={2}>liked by \</heading> {likeusers length > 0 ? ( \<vstack align="stretch"> {likeusers map(user => ( \<hstack key={user id}> \<avatar size="xs" src={user avatar} name={user username} /> \<text fontsize="sm">{user username}\</text> \</hstack> ))} \</vstack> ) ( \<text fontsize="sm">no likes yet\</text> )} \</box> )} back4gram 프로젝트 여기에서 전체 코드를 찾으세요 소셜 네트워크 샘플 프로젝트 back4app으로 구축되었습니다 6단계 – parse live query로 실시간 업데이트 구현하기 back4app의 가장 강력한 기능 중 하나는 live query를 통해 실시간 업데이트를 받을 수 있는 기능입니다 이를 통해 새로운 게시물이나 댓글이 생성될 때 애플리케이션이 수동으로 새로 고침할 필요 없이 자동으로 업데이트됩니다 back4app에서 실시간 쿼리 설정하기 먼저, back4app에서 애플리케이션에 대해 실시간 쿼리를 활성화해야 합니다 back4app 대시보드로 이동합니다 앱 설정 > 서버 설정으로 이동합니다 "parse live query class names"로 스크롤합니다 "post"와 "comment"를 클래스 목록에 추가합니다 변경 사항을 저장합니다 실시간 게시물을 위한 실시간 쿼리 구현하기 우리의 feedpage js 를 업데이트하여 실시간 게시물 업데이트를 위한 live query를 포함합시다 // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser) return; try { console log('setting up live query for posts'); // create a query that will be used for the subscription const query = new parse query('post'); // 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'); }); // handle new posts livequerysubscription current on('create', (post) => { console log('new post received via live query ', post id); // skip posts from the current user as they're already in the ui if (post get('author') id === currentuser id) { console log('skipping post from current user'); return; } // format the new post const author = post get('author'); const newpost = { id post id, content post get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, image post get('image') ? post get('image') url() null, likes post get('likes') || 0, createdat post get('createdat') }; // add the new post to the posts state setposts(prevposts => \[newpost, prevposts]); }); // handle post updates livequerysubscription current on('update', (post) => { console log('post updated via live query ', post id); // format the updated post const newlikes = post get('likes') || 0; // update the post in the state setposts(prevposts => prevposts map(p => p id === post id ? { p, likes newlikes } p ) ); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser]); 실시간 댓글을 위한 live query 구현 유사하게, 우리의 postdetailspage js 를 업데이트하여 실시간 댓글을 위한 live query를 포함합시다 // add to your import statements import { useeffect, useref } from 'react'; // add to your component const livequerysubscription = useref(null); // add this to your component to set up and clean up the subscription useeffect(() => { const setuplivequery = async () => { if (!currentuser || !id) return; try { console log('setting up live query for comments'); // get the post object to use in the query const postquery = new parse query('post'); const postobject = await postquery get(id); // create a query that will be used for the subscription const query = new parse query('comment'); query equalto('post', postobject); // subscribe to the query livequerysubscription current = await query subscribe(); console log('successfully subscribed to live query for comments'); // handle connection open livequerysubscription current on('open', () => { console log('live query connection opened'); }); // handle new comments livequerysubscription current on('create', (comment) => { console log('new comment received via live query ', comment id); // skip comments from the current user as they're already in the ui if (comment get('author') id === currentuser id) { console log('skipping comment from current user'); return; } // format the new comment const author = comment get('author'); const newcomment = { id comment id, content comment get('content'), author { id author id, username author get('username'), avatar author get('avatar') ? author get('avatar') url() null }, createdat comment get('createdat') }; // add the new comment to the comments state setcomments(prevcomments => \[ prevcomments, newcomment]); }); // handle errors livequerysubscription current on('error', (error) => { console error('live query error ', error); }); } catch (error) { console error('error setting up live query for comments ', error); } }; setuplivequery(); // clean up subscription return () => { if (livequerysubscription current) { console log('unsubscribing from live query'); livequerysubscription current unsubscribe(); } }; }, \[currentuser, id]); 이 라이브 쿼리 구현을 통해 귀하의 소셜 네트워크는 이제 실시간으로 업데이트됩니다 다른 사용자로부터의 새로운 게시물이 피드에 자동으로 나타납니다 다른 사용자가 게시물을 좋아할 때 좋아요 수가 실시간으로 업데이트됩니다 새로운 댓글이 게시물 상세 페이지에 자동으로 나타납니다 back4gram 프로젝트 여기에서 전체 코드를 확인하세요 소셜 네트워크 샘플 프로젝트 는 back4app으로 구축되었습니다 7단계 – 고급 쿼리 및 성능 최적화 소셜 네트워크가 성장함에 따라 성능이 점점 더 중요해집니다 back4app의 강력한 쿼리 기능을 사용하여 애플리케이션을 최적화하는 몇 가지 고급 기술을 살펴보겠습니다 페이지네이션 구현하기 모든 게시물을 한 번에 로드하는 대신, 배치로 로드하기 위해 페이지네이션을 구현하세요 // add these state variables const \[page, setpage] = usestate(0); const \[hasmoreposts, sethasmoreposts] = usestate(true); const postsperpage = 10; // update your fetchposts function const fetchposts = async (loadmore = false) => { if (!currentuser) return; if (!loadmore) { setisloading(true); } try { const query = new parse query('post'); query include('author'); query descending('createdat'); query limit(postsperpage); query skip(page postsperpage); const results = await query find(); // process and format posts as before const fetchedposts = \[ ]; // update the hasmoreposts state sethasmoreposts(results length === postsperpage); // update the posts state if (loadmore) { setposts(prevposts => \[ prevposts, fetchedposts]); } else { setposts(fetchedposts); } // update the page if this was a "load more" request if (loadmore) { setpage(prevpage => prevpage + 1); } } catch (error) { // handle errors } finally { setisloading(false); } }; // add a load more button at the bottom of your posts list {hasmoreposts && ( \<button onclick={() => fetchposts(true)} variant="outline" isloading={isloading} mt={4} \> load more posts \</button> )} 사용자 특정 피드 구현하기 더 개인화된 경험을 위해 현재 사용자가 팔로우하는 사용자만의 게시물을 보여주고 싶을 수 있습니다 // assuming you have a "follow" class with "follower" and "following" pointers const fetchfollowingposts = async () => { try { // first, find all users that the current user follows const followquery = new parse query('follow'); followquery equalto('follower', currentuser); const followings = await followquery find(); // extract the users being followed const followedusers = followings map(follow => follow\ get('following')); // add the current user to show their posts too followedusers push(currentuser); // create a query for posts from these users const query = new parse query('post'); query containedin('author', followedusers); query include('author'); query descending('createdat'); query limit(20); const results = await query find(); // process results } catch (error) { // handle errors } }; 복잡한 작업을 위한 클라우드 함수 사용 여러 쿼리가 필요한 복잡한 작업의 경우 back4app 클라우드 함수를 사용하는 것을 고려하세요 back4app 대시보드로 이동 클라우드 코드 > 클라우드 함수로 이동 새 함수를 생성하세요 // example function to get a feed with posts, likes, and comment counts parse cloud define("getfeed", async (request) => { try { const user = request user; const page = request params page || 0; const limit = request params limit || 10; if (!user) { throw new error("user must be logged in"); } // query for posts const query = new parse query("post"); query include("author"); query descending("createdat"); query limit(limit); query skip(page limit); const posts = await query find({ usemasterkey true }); // format posts and get additional info const formattedposts = await promise all(posts map(async (post) => { // check if user has liked this post const likequery = new parse query("like"); likequery equalto("post", post); likequery equalto("user", user); const hasliked = await likequery count({ usemasterkey true }) > 0; // get comment count const commentquery = new parse query("comment"); commentquery equalto("post", post); const commentcount = await commentquery count({ usemasterkey true }); // format post for response return { id post id, content post get("content"), image post get("image") ? post get("image") url() null, 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 }, likes post get("likes") || 0, hasliked hasliked, commentcount commentcount, createdat post get("createdat") }; })); return { posts formattedposts }; } catch (error) { throw new error(`error fetching feed ${error message}`); } }); 그런 다음 앱에서 이 클라우드 기능을 호출하세요 const fetchfeed = async () => { try { setisloading(true); const result = await parse cloud run('getfeed', { page 0, limit 10 }); setposts(result posts); } catch (error) { console error('error fetching feed ', error); toaster create({ title 'error loading feed', description error message, type 'error', }); } finally { setisloading(false); } }; 이 접근 방식은 데이터베이스에 대한 쿼리 수를 크게 줄이고 성능을 향상시킬 수 있습니다 back4gram 프로젝트 찾기 여기 전체 코드를 위한 소셜 네트워크 샘플 프로젝트 back4app으로 구축된 결론 이 튜토리얼에서는 back4app을 사용하여 상호작용 기능이 포함된 포괄적인 소셜 네트워크 피드를 구축했습니다 여러분이 성취한 내용을 요약해 보겠습니다 데이터 모델 설계 게시물, 댓글 및 좋아요를 위한 구조화된 데이터 모델을 생성했습니다 피드 구현 모든 사용자의 게시물을 표시하는 피드 페이지를 구축했습니다 게시물 생성 사용자가 텍스트와 이미지를 포함한 게시물을 생성할 수 있는 기능을 구현했습니다 댓글 시스템 사용자가 게시물에 참여할 수 있는 댓글 시스템을 만들었습니다 좋아요 기능 간단한 좋아요 시스템과 고급 좋아요 시스템을 모두 구현했습니다 실시간 업데이트 실시간 게시물 및 댓글 업데이트를 위한 라이브 쿼리를 추가했습니다 성능 최적화 앱이 확장됨에 따라 성능을 개선하기 위한 기술을 탐색했습니다 back4app이 제공한 내용 사용자 생성 콘텐츠를 저장하기 위한 안전하고 확장 가능한 데이터베이스 게시물 이미지를 위한 파일 저장소 보안을 위한 내장 사용자 인증 실시간 업데이트를 위한 라이브 쿼리 복잡한 백엔드 작업을 위한 클라우드 함수 이러한 구성 요소가 마련되면, 귀하의 소셜 네트워크는 사용자 기반이 성장함에 따라 확장할 수 있는 견고한 기반을 갖추게 됩니다 다음 단계 소셜 네트워크를 더욱 향상시키기 위해 다음 기능을 구현하는 것을 고려해 보세요 알림 사용자의 게시물이 좋아요나 댓글을 받을 때 사용자에게 알림을 보냅니다 사용자 팔로우 시스템 사용자가 서로를 팔로우하고 피드를 사용자화할 수 있도록 합니다 해시태그 및 검색 해시태그 시스템과 향상된 검색 기능을 구현합니다 미디어 풍부한 게시물 비디오, 여러 이미지 및 기타 미디어 유형을 지원합니다 분석 참여도 및 사용자 행동을 추적하여 애플리케이션을 개선합니다 back4gram 소셜 네트워크 애플리케이션의 전체 코드는 github 저장소 https //github com/templates back4app/back4gram back4app의 강력한 백엔드 서비스를 활용하여, 플랫폼이 소셜 네트워킹 애플리케이션에 필요한 복잡한 인프라를 처리하는 동안 훌륭한 사용자 경험을 만드는 데 집중할 수 있습니다