-
6. 메세지 리스트 표시하기사이드프로젝트 2024. 7. 22. 19:21
https://www.youtube.com/watch?v=-xXASlyU0Ck&t=2007s
를 보며 실습하는 영상
-----
이제 채팅들을 화면에서 보여주는 걸 만들건데, 우선 쉽게 관리하기 위해 컴포넌트 두개를 새로 만들어준다.
components/ListMessages.tsx
components/ChatMessages.tsx
그리고 이전에 app/page.tsx에서 채팅들을 표시주는 부분을 긁어와서 ListMessages.tsx 컴포넌트에 넣어주고, 이를 다시 ChatMessages.tsx 컴포넌트에서 가져온다. page.tsx에서는 이부분을 새로만든 컴포넌트로 대체해준다.
// components/ListMessages.tsx
"use client"; import React from "react"; const ListMessages = () => { return ( <div className="flex-1 flex flex-col p-5 h-full overflow-y-auto"> <div className="flex-1"></div> <div className="space-y-7"> {Array.from({ length: 15 }, (v, i) => i).map((value) => { return ( <div className="flex gap-2" key={value}> <div className="h-10 w-10 bg-green-500 rounded-full"></div> <div className="flex-1"> <div className="flex items-center gap-1"> <h1 className="font-bold">Jaehyeon</h1> <h1 className="text-sm text-gray-400"> {new Date().toDateString()} </h1> </div> <p className="text-gray-300"> Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quisquam minus dolorum voluptatibus perferendis nesciunt ex odit rem est a maiores! </p> </div> </div> ); })} </div> </div> ); }; export default ListMessages;
// components/ChatMessages.tsx
import React from "react"; import ListMessages from "./ListMessages"; const ChatMessages = () => { return ( <div> <ListMessages /> </div> ); }; export default ChatMessages;
// app/page.tsx
import ChatHeader from "@/components/ChatHeader"; import { ChatInput } from "@/components/ChatInput"; import ListMessages from "@/components/ListMessages"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import InitUser from "@/lib/store/InitUser"; import { createClient } from "@/utils/supabase/server"; // supabase 객체 불러오기. // 서버 컴포넌트니깐 서버에서 임포트해온다. import React from "react"; const page = async () => { const supabase = createClient(); // supabase 객체 불러오기. // const session = await supabase.auth.getSession(); // console.log(session.data.session?.user); const user = (await supabase.auth.getUser()).data.user; // console.log("user: ", user); return ( <> <div className="max-w-3xl mx-auto md:py-10 h-screen"> <div className="h-full border rounded-md flex flex-col"> <ChatHeader user={user} /> <ChatMessages /> // 방금 만든 컴포넌트로 기존 채팅리스트를 보여주던걸 대체해준다 <ChatInput /> </div> </div> <InitUser user={user} /> {/* user state 관리를 시작할 수 있도록. */} </> ); }; export default page;
구조는 ChatMessages가 서버 컴포넌트이고, ListMessages가 클라이언트 컴포넌트다. ChatMessages에서 데이터를 패칭해와서 ListMessages에 넘겨준다.
이제 ChatMessages에서 메세지 데이터를 가져와보자.
// ChatMessages.tsx import React, { Suspense } from "react"; import ListMessages from "./ListMessages"; import { createClient } from "@/utils/supabase/server"; const ChatMessages = async () => { const supabase = createClient(); const result = await supabase.from("messages").select("*,users(*)"); console.log("result : ", result); // console.log("result : ", result); return ( <Suspense fallback={"loading..."}> <ListMessages /> </Suspense> ); }; export default ChatMessages; // console result : { error: null, data: [ { id: '89c7dafe-4ee3-4ee1-95f1-03867906f837', user_id: 'd4507390-2d03-401d-93df-a8b04e3118c3', text: 'test', is_edit: false, created_at: '2024-07-22T08:06:32.501212+00:00', is_deleted: null, users: null // null 인걸 볼 수 있다. }, ], count: null, status: 200, statusText: 'OK' }
이렇게 데이터를 가져오는걸 볼 수 있다.
그런데 저기서
select("*,users(*)")가 의미하는 것은 messages 의 모든 칼럼의 데이터들을 다 가져오고, 이 메세지와 연결된 users 테이블의 모든 정보를 가져오라는 말이다.(유저 닉네임, 아바타등을 표시해주기 위해)
그러니깐, JOIN이라는 말이다.
그런데 users가 null 로 나오는 것을 볼 수 있다. 이는 users에는 권한설정을 해주지 않아서 그렇다.
supabase project page로 이동한다.
table editor -> users 선택 -> Add RLS Policy -> Create Policy
설정을 저장해주고,
이제 다시 들어가서 콘솔을 찍어보면 (이전꺼는 너무 포괄적으로 콘솔을 찍어서 범위를 살짝 좁혀서 데이터만 찍게 해줬따.)
console.log("result : ", result.data); // result result : [ { id: '89c7dafe-4ee3-4ee1-95f1-03867906f837', user_id: 'd4507390-2d03-401d-93df-a8b04e3118c3', text: 'test', is_edit: false, created_at: '2024-07-22T08:06:32.501212+00:00', is_deleted: null, users: { // 유저의 정보가 나온다. id: 'd4zxcv390-2d12403-41501d-935df-a8bzxcv118c3', avatar_url: 'https://avatars.githubusercontent.com/u/42367317?v=4', created_at: '2024-07-21T17:17:47.959987+00:00', deleted_at: null, updated_at: null, display_name: 'Wunhyeon' } }, ]
유저의 정보까지 나오는 걸 볼 수 있다.
그럼 이제 이 메세지들을 ListMessages.tsx로 넘겨줘서 표시해줘야 하는데, 이걸 props로 넘겨줄 수도 있지만,
user에서 했던 것처럼 zustand로 넘겨준 다음 사용해줄 수도 있따. zustand로 넘겨줘서 사용하는 방법으로 하겠다.
zustand를 사용하기 위해 먼저 store를 만들어준다. lib/store/messages.ts
import { create } from "zustand"; export type IMessage = { created_at: string; id: string; is_deleted: string | null; is_edit: boolean; text: string; user_id: string; users: { avatar_url: string; created_at: string; deleted_at: string | null; display_name: string; id: string; updated_at: string | null; } | null; }; interface MessageState { messages: IMessage[]; } export const useMessage = create<MessageState>()((set) => ({ messages: [], }));
user의 경우 supabase library에 기본 user type이 있었기에 type을 따로 안만들어줬지만, 메세지는 없어서 IMessage라는 이름으로 타입을 만들어줬다.
타입을 만들어주는 꿀팁은 vscode를 쓴다면, 데이터를 받아오는 곳으로 가서 마우스를 대보면 type을 보여준다.
물론 예전에 한 type generate 를 다 해야 이렇게 표시된다. 이걸 긁어다가 타입 설정을 해주면 된다. 그런데 타입 설정해줄건 배열이 아니므로 뒤에 배열표시만 지워주면 된다.
아니면 위에서 콘솔 찍은걸 보고 일일이 만들어줘도 되고, types/supabase.ts에 들어가보면 테이블마다 칼럼이랑 이런게 다 정의되어있으므로 이걸 가져다가 수작업으로 진행해줘도 된다. 근데 다 번거로우니, 위처럼 긁어다 쓰는게 제일 편하다.
그리고 user에서 했던것처럼 Messages도 처음 init을 해주기 위해 파일을 만들어준다.
lib/store/InitMessages.tsx
"use client"; // react hook을 사용할 거기 때문에, 클라이언트 컴포넌트로 import React, { useEffect, useRef } from "react"; import { IMessage, useMessage } from "./messages"; const InitMessages = ({ messages }: { messages: IMessage[] }) => { // props로 유저 객체를 받아옴. const initState = useRef(false); // 초기값 useEffect(() => { if (!initState.current) { // 초기값이 false라면, state management를 할 수 있도록 user객체를 넣어준다. useMessage.setState({ messages }); } initState.current = true; }, []); return <></>; }; export default InitMessages;
그리고 ChatMessages에서 InitMessages를 넣어줘서 상태를 저장할 수 있도록 한다.
// components/ChatMessages.tsx import React, { Suspense } from "react"; import ListMessages from "./ListMessages"; import { createClient } from "@/utils/supabase/server"; import InitMessages from "@/lib/store/InitMessages"; const ChatMessages = async () => { const supabase = createClient(); const result = await supabase.from("messages").select("*,users(*)"); console.log("result : ", result.data); // console.log("result : ", result); return ( <Suspense fallback={"loading..."}> <ListMessages /> <InitMessages messages={result.data || []} /> {/* result.data는 null 이 올수도 있는데, InitMessages에서는 props로 IMessages[]만 받고있으므로 null일경우 빈배열을 넣어주기 위해 || []를 해줌. */} </Suspense> ); }; export default ChatMessages;
이제 저장된 상태를 불러와서 ListMessages에서 사용해준다.
"use client"; import { useMessage } from "@/lib/store/messages"; import React from "react"; const ListMessages = () => { const messages = useMessage((state) => state.messages); // zustand에 전역으로 저장된 state를 불러옴. return ( <div className="flex-1 flex flex-col p-5 h-full overflow-y-auto"> <div className="flex-1"></div> <div className="space-y-7"> {messages.map((value, idx) => { return ( <div className="flex gap-2" key={idx}> <div className="h-10 w-10 bg-green-500 rounded-full"></div> <div className="flex-1"> <div className="flex items-center gap-1"> <h1 className="font-bold">Jaehyeon</h1> <h1 className="text-sm text-gray-400"> {new Date().toDateString()} </h1> </div> {/* 불러온 메세지를 표시해보자! */} <p className="text-gray-300">{value.text}</p> </div> </div> ); })} </div> </div> ); }; export default ListMessages;
위 글에서 주석표시된 부분들을 잘 봐주면 좋다. zustand에 전역으로 저장된 state를 불러와서 사용해주고 있다.
이제 불러온 메세지가 잘 표시되었는지 화면으로가서 확인해보면
이렇게 잘 표시되는 걸 볼 수 있따.
데이터를 잘 불러오는 걸 알았으니, 이제 컴포넌트를 조금 더 분리해보자.
components/Message.tsx 파일을 만들어준다.
그리고 components/ListMessages.tsx에서 map안에 들어와있던 내용을 복사해서 넣어주고, 유저아이디, 내용등을 반영할 수 있도록 약간 수정해준다.
// components/Message.tsx import { IMessage } from "@/lib/store/messages"; import Image from "next/image"; import React from "react"; const Message = ({ message }: { message: IMessage }) => { return ( <div className="flex gap-2"> <div> <Image src={message.users?.avatar_url!} alt={message.users?.display_name!} width={40} height={40} className="rounded-full" /> </div> <div className="flex-1"> <div className="flex items-center gap-1"> <h1 className="font-bold">{message.users?.display_name}</h1> <h1 className="text-sm text-gray-400"> {new Date(message.created_at).toDateString()} </h1> </div> <p className="text-gray-300">{message.text}</p> </div> </div> ); }; export default Message;
// components/ListMessages.tsx "use client"; import { useMessage } from "@/lib/store/messages"; import React from "react"; import Message from "./Message"; const ListMessages = () => { const messages = useMessage((state) => state.messages); // zustand에 전역으로 저장된 state를 불러옴. return ( <div className="flex-1 flex flex-col p-5 h-full overflow-y-auto"> <div className="flex-1"></div> <div className="space-y-7"> {messages.map((value, idx) => { return <Message key={idx} message={value} />; })} </div> </div> ); }; export default ListMessages;
여기까지 하면
이런 에러가 발생하는 걸 볼 수 있는데, nextJs에서는 외부 url을 가져올 때, 허용된 것만 가져올 수 있기 때문이다. next.config.js로 이동해, avatar-url을 허용해주자. (next.config.mjs 로 파일이 생성되어 있을 수도 있는데, 아무 상관없다.)
// next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [ { hostname: "avatars.githubusercontent.com", protocol: "https" }, ], }, }; export default nextConfig;
여기서 주의할 점은 hostname에는 https://를 붙여주지 않는다.
그리고 서버를 껐다 켜고 새로고침 해보면 사진이 잘 나오는 걸 볼 수 있다.
여기까지 하면 채팅 메세지를 저장하고, 출력하는 것 까지 됬다. 실시간은 아니지만 메세지를 입력하고, 새로고침하면 새로운 메세지가 들어간 걸 볼 수 있따.
github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/6.MessageList
'사이드프로젝트' 카테고리의 다른 글
9. Message Delete (7) 2024.07.23 8.Message Menu (1) 2024.07.22 5. Message 보내기. - 메세지 테이블 만들기, Policy 생성, type 생성 (8) 2024.07.22 4. Chat UI (2) 2024.07.22 NextJS + Supabase 채팅 앱 만들기 - 3. 로그아웃, 유저 상태관리 (Zustand), OAuth User생성 Trigger를 통해 public Schema에도 유저 생성해주기 (1) 2024.07.22