inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

[인프런 워밍업 클럽 Full Stack 3기] 4주차

팥우유
0

1. 학습 내용

1.1. Supabase 인증 방식

1.1.1. Confirmation URL

사용자가 이메일로 받은 확인 링크를 클릭하여 계정을 인증하는 방식입니다.

1.1.2. 6-Digit OTP 방식

이메일로 6자리 일회용 코드를 전송하고, 사용자가 이를 입력하여 인증하는 방식입니다.

1.2. Supabase realtime

Supabase Realtime은 PostgreSQL 데이터베이스의 변경사항을 실시간으로 클라이언트에 제공하는 기능으로, 채팅, 알림, 실시간 협업 등을 구현할 수 있게 해주는 서비스입니다.

1.2.1. Broadcast

특정 채널을 통해 연결된 모든 클라이언트 간에 직접 메시지를 주고받을 수 있는 기능입니다. 데이터베이스를 거치지 않고 실시간 통신이 가능합니다.

특징

- 클라이언트 간 직접 메시지 교환

- 데이터베이스 저장 없이 임시 통신 가능

- 타이핑 표시기, 커서 위치 등 임시 상태에 적합

- 채널 기반 구독 시스템

1.2.2. Presence (중요)

Presence는 채널에 연결된 사용자의 상태 정보를 실시간으로 추적하고 공유하는 기능입니다. 온라인 상태, 사용자 활동, 현재 위치 등을 관리할 수 있습니다. 이번 미션을 진행할 때 presence가 굉장히 핵심적인 역할을 합니다.

특징

- 사용자 온라인/오프라인 상태 관리

- 자동 정리(cleanup) 기능

- 상태 데이터 저장 및 공유

- 연결 끊김 자동 감지

1.2.3. Postgres Changes

PostgreSQL 데이터베이스의 변경사항(INSERT, UPDATE, DELETE)을 실시간으로 감지하여 클라이언트에 전달하는 기능입니다.

특징

- 데이터베이스 수준의 변경사항 감지

- 테이블, 로우, 열 수준의 구독 가능

- 정교한 필터링 옵션

- RLS(Row Level Security)와 통합

1.3. RLS (중요)

PostgreSQL의 보안 기능으로, 데이터베이스 테이블의 개별 행(row)에 대한 접근을 사용자 수준에서 제어할 수 있는 메커니즘입니다. Supabase에서는 이를 통해 인증된 사용자가 자신의 데이터만 접근할 수 있도록 세밀한 보안 정책을 설정할 수 있습니다.(Policy 기능)

 

2. 미션

2.1. 지금까지 만든 모든 프로젝트 배포

TODO: https://inf.run/Gi4eA

DROPBOX: https://inf.run/ScSd4

NETFLIX: https://inf.run/TMWKr

INSTAGRAM: https://inf.run/HS4Sn

2.2. 메시지 삭제 기능

image제가 구현한 메시지 삭제 기능은 데이터베이스에서 실제로 행을 제거하지 않고 상태 플래그(is_deleted)만 변경하는 방식으로 구현했습니다.

2.2.1. 클라이언트 측 구현

// /components/chat/MessageArea.tsx

const deleteMessageMutation = useMutation({
  mutationFn: async (id: number) => {
    return deleteMessage({ id });
  },
  onSuccess: () => {
    getAllMessageQuery.refetch();
  },
  onError: error => {
    console.error('메시지 삭제 오류:', error);
    alert('메시지 삭제 중 오류가 발생했습니다.');
  },
});

function handleDeleteMessage(id: number) {
  confirm('정말 메시지를 삭제하시겠습니까?') &&
    deleteMessageMutation.mutate(id);
}

2.2.2. 서버 액션 구현

// /actions/chatAction.ts

export async function deleteMessage({ id }: { id: number }) {
  try {
    const user = await getCurrentUser();

    const supabase = await createServerSupabaseClient();
    const { error } = await supabase
      .from('message')
      .update({ is_deleted: true })
      .eq('id', id)
      .eq('sender', user.id);

    if (error) {
      throw new Error(error.message);
    }
  } catch (error) {
    console.error('메시지 삭제 중 오류:', error);
    throw error;
  }
}

2.2.3. Message UI 구현

// components/chat/Message.tsx

export function Message({
  isFromMe,
  message,
  deleteMessage,
  isDeleted,
  isRead = false,
}: MessageProps) {
  const DELETE_MESSAGE = '이미 삭제된 메세지입니다.';

  return (
    <div className={`group flex w-fit ${isFromMe && 'ml-auto'} `}>
      {/* ... 메시지 내용 ... */}
      <p>{isDeleted ? DELETE_MESSAGE : message}</p>
      
      {/* 삭제 버튼 (자신의 메시지이고 삭제되지 않은 경우에만 표시) */}
      {isFromMe && !isDeleted && (
        <button
          onClick={deleteMessage}
          className="hidden rounded-md rounded-l-none bg-red-400 px-2 text-sm text-white opacity-70 hover:opacity-100 group-hover:block"
        >
          X
        </button>
      )}
    </div>
  );
}

2.2.4. 기능 설명 요약

  1. 사용자가 메시지의 X 버튼 클릭

  2. handleDeleteMessage 함수가 호출되어 확인 대화상자 표시

  3. 확인 시 deleteMessageMutation.mutate(id) 실행

  4. 서버 액션 deleteMessage가 호출되어 데이터베이스 업데이트

  5. 성공 시 getAllMessageQuery.refetch()로 메시지 목록 새로고침

  6. 삭제된 메시지는 UI에서 "이미 삭제된 메세지입니다"로 표시

2.3. 채팅 '읽음/안읽음' 표시 기능

image

2.3.1. 읽음 상태 관리

// /hooks/reactQueries.ts
 
export function useMarkMessagesAsRead(selectedUserId: string | null) {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: () => {
      if (!selectedUserId) return Promise.resolve()
      return markMessagesAsRead(selectedUserId)
    },
    onSuccess: () => {
      // 쿼리 무효화 로직
    },
  })
}

2.3.2. 메시지 카운트 조회 기능

// /actions/chatActions.ts

export async function getNotReadMessageCount(chatUserId: string) {
  // 수행 로직
  const { count, error: countError } = await supabase
    .from('message')
    .select('*', { count: 'exact', head: true })
    .match({
      sender: chatUserId,
      receiver: user.id,
      is_read: false,
    })
}

2.3.3. 자동 읽음 처리 기능

// /components/chat/ChatScreen.tsx

const checkUserPresence = useCallback(async () => {
  if (!selectedUserId || !loggedUser) return

  const chatPartnerId = presence?.[`${selectedUserId}`]?.[0]?.activeChatId
  if (chatPartnerId === loggedUser.id) {
    markAsRead()
  }
}, [selectedUserId, presence, markAsRead, loggedUser])

2.3.4. 읽음 상태 UI 표시

// /compoents/chat/Message.tsx

export function Message({ isFromMe, message, isRead = false }) {
  return (
    <div>
      <p>{message}</p>
      {isFromMe && (
        <span className="ml-1 text-xs text-gray-500">
          {isRead ? '읽음' : '안읽음'}
        </span>
      )}
    </div>
  )
}

2.3.5. Presence 채널 설정

// /components/chat/CHatPeopleList.tsx

const channel = supabase.channel('online_users', {
  config: {
    presence: {
      key: loggedInUser.id,
    },
  },
})

channel.on('presence', { event: 'sync' }, () => {
  const newState = channel.presenceState()
  setPresence(newStateOObject)
})

2.3.6. 활성 채팅 상태 추적 기능

// /components/chat/ChatPeopleList.tsx

channel.Subscribe(async status => {
  if (status !== 'SUBSCRIBED') return

  const newPresenceStatus = await channel.track({
    onlineAt: new Date().toISOString(),
    activeChatId: selectedUserId,
  })
})

2.3.7. 실시간 메시지 변경 감지 및 처리

// /components/chat/ChatScreen.tsx

supabase
  .channel('message_postgres_changes')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'message',
    },
    payload => {
      if (payload.eventType === 'INSERT') {
        refetch()
        checkUserPresence()
      }
    }
  )
  .subscribe()

2.3.8. 정책 설정 (supabase policy)

image

2.3.9. 기능 설명 요약

  1. 초기 설정: 사용자가 로그인하면 Presence 채널 구독 및 상태 추적 시작

  2. 채팅방 전환: 사용자가 특정 채팅방을 선택하면 activeChatId 업데이트

  3. 자동 읽음 처리: 상대방이 대화를 보고 있으면 자동으로 메시지 읽음 처리

  4. 상태 변경 감지: 메시지 읽음 상태가 변경되면 UI 업데이트

  5. 안 읽은 메시지 카운트: 각 사용자별 안 읽은 메시지 개수 조회 및 표시

 

3. 그 외 추가 작업 사항

3.1. 코드 구조 개선

3.1.1. 인증 및 상태 관리

3.1.2. 커스텀 훅 도입

3.1.3. 컴포넌트 구조 개선

3.2. 사용자 경험(UX) 개선

3.2.1. 현재 사용자 표시

로그인한 사용자를 유저 리스트 최상단에 시각적으로 구분하여 표시

3.2.2. 가이드 메시지 추가

3.2.3. 자동 스크롤 구현

3.2.4. 입력 경험 개선

 

4. 한 달 간의 개발 스터디를 마치며

매주 개발 프로젝트와 미션을 수행하며 그 과정에서 해보고 싶다고 생각되는 것도 시도해보다 보니 어느새 스터디 마지막에 도달했습니다. 한 달 간의 꾸준한 코딩은 개발 트렌드 확인에 많은 도움이 되었습니다. 공부 습관도 다시 생길 것 같습니다.

아쉬운 점은 인프런 블로그 에디터가 일정 분량 이상 작성하면 급격히 느려져 매주 하고 싶었던 이야기를 충분히 담지 못했다는 것입니다. 오늘도 같은 이유로 긴 회고를 남기기 어렵네요.

짧은 시간이었지만 이번 스터디를 통해 얻은 기술적 성장과 개발 습관은 앞으로의 여정에 큰 도움이 될 것입니다.

답변 0