코드로 우주평화

Supabase의 RPC를 활용해 실시간 카운터 기능 구현하기 본문

나는 이렇게 학습한다/DB

Supabase의 RPC를 활용해 실시간 카운터 기능 구현하기

daco2020 2024. 9. 14. 20:25
반응형

제가 최근에 만든 서비스는 홈 화면에 사용자들의 이용 수 보여주고 있습니다.

명절 잔소리 마스터는 '잔소리 시작 수'를 홈 화면에 제공하고 있다.
명절 잔소리 마스터는 '잔소리 시작 수'를 홈 화면에 제공하고 있다.



이처럼 사용자의 이용 수를 카운트하여 보여주고 흥미를 끄는 방식이 있는데요. 이런 카운트 기능을 어떻게 구현할까 고민하다 supabase의 RPC를 이용해 구현해 보았습니다.

 

이 글은 그 과정에 대해 설명하는 글입니다. 이 글을 끝까지 읽으면 꼭 카운터가 아니더라도 비슷한 상황에서 적절히 사용하실 수 있을 겁니다.

 

 

사용자가 버튼을 클릭하면 숫자를 올려줘

처음에는 이 기능을 이용 중에 생성된 DB row의 수를 기준으로 숫자를 카운트했습니다. 하지만 이 row는 서비스를 끝까지 이용했을 때에만 생성되기 때문에 중간에 이탈한 사용자의 수는 숫자로 카운트되지 않았죠..

 

그래서 버튼을 클릭하자마자 숫자가 올라가도록 수정했습니다. 먼저 supabase 테이블에 count를 컬럼으로 갖는 테이블 하나를 생성합니다.

 

[jansori_count] 테이블 형태

id count
{int} {int}

 

 

이 테이블은 숫자 카운트에 대한 하나의 row 만 있습니다. 사용자가 시작하기 버튼을 누르면 해당 row의 count 가 1씩 올라가는 거죠.

 

사실 row 하나만을 위해 테이블을 추가하는 것이 괜찮은가... 싶긴 했지만 빠르게 수정해야 하는 상황에서 다른 아이디어는 떠오르지 않더군요. 이에 대해서는 이 글의 주요 주제에는 벗어나므로 여기까지만 말하겠습니다.

 

supabase에서 테이블을 추가했다면 이제 클라이언트(Next.js)에서 serverActions을 통해 supabase로 요청을 보내야 합니다. 그런데 문득 그런 생각이 들더군요. 만약 여러 클라이언트가 동시다발적으로 요청을 보내면 어떻게 될까..? 제가 우려한 것은 동시성 문제였습니다. 

 

이 동시성 문제를 해결하기 위해 supabase의 RPC를 사용하기로 합니다.

 

 

Supabase에서 RPC 함수 생성하기

supabase는 SQL 에디터를 사용해 RPC(원격 프로시저 호출) 함수를 생성할 수 있습니다. RPC 함수는 데이터베이스에서 직접 호출 가능한 서버 측 함수로, Lock을 명시적으로 설정함으로써 동시성 문제를 해결할 수 있죠.

 

자, 그렇다면 어떻게 RPC 함수를 만들고 적용하는지 살펴보죠.

 

Supabase SQL 에디터에서 RPC 함수 작성

1. 먼저 Supabase 대시보드에 로그인한 후, SQL Editor 탭으로 이동합니다.
2. 그러면 이제 SQL을 작성하는 화면이 나올 텐데
다음과 같은 SQL 쿼리를 입력합니다.

CREATE OR REPLACE FUNCTION increment_count()
RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
  current_count INTEGER;
BEGIN
  -- ID 1에 대한 데이터를 읽고 비관적 락을 건다.
  SELECT count INTO current_count
  FROM jansori_count
  WHERE id = 1
  FOR UPDATE;

  -- 읽은 값을 기반으로 count를 증가시킨다.
  UPDATE jansori_count
  SET count = current_count + 1
  WHERE id = 1;
END;
$$;

 

이 함수는 jansori_count 테이블의 count 값을 원자적으로 증가시키는 기능을 수행합니다. 여러 사용자가 동시에 요청을 보내더라도, 동시성 문제없이 안전하게 값을 증가시킬 수 있습니다.

 

그 이유는 SELECT 문에서 FOR UPDATE로 값을 불러와 다른 요청이 반영되지 않도록 명시적으로 lock을 걸기 때문입니다. 만약 이러한 lock 이 없다면 다음과 같은 문제가 발생할 수 있습니다.

 

- 요청 A가 데이터를 읽음 (현재 값: 10)
- 요청 B가 데이터를 읽음 (현재 값: 10)
- 요청 A가 데이터를 1 증가시켜 11로 업데이트
- 요청 B도 데이터를 1 증가시키려고 하며 11이 아닌, 10에 1을 더해 11로 업데이트

 

하지만 위의 쿼리는 읽을 때부터 lock 이 걸리기 때문에, 요청 B는 A의 작업이 끝나기 전까지 읽을 수 없는 것이죠. 그 때문에 값을 올바르게 저장할 수 있습니다.

 

 

Supabase에서 RPC 호출하기

RPC 함수가 생성되었으므로, 이제 Supabase 클라이언트로 이를 호출하는 방법을 구현해 봅시다.

 

RPC 호출 구현 (TypeScript)

먼저, 서버 측에서 RPC 함수를 호출하는 countActions.ts 파일을 작성합니다. 이 파일에서 RPC 함수 호출 및 카운트 값을 가져오는 함수를 정의합니다.

// actions/countActions.ts

"use server";

import { createServerSupabaseClient } from "utils/supabase/server";

export async function incrementCount() {
  const supabase = await createServerSupabaseClient();

  // RPC 호출 함수: Supabase에 저장된 stored procedure 호출
  const { data, error } = await supabase.rpc("increment_count");

  if (error) {
    console.error("Error incrementing counter:", error.message);
    return;
  }

  console.log("Counter incremented successfully:", data);
}

export async function getCount() {
  const supabase = await createServerSupabaseClient();

  const { data, error } = await supabase
    .from("jansori_count")
    .select("count")
    .eq("id", 1)
    .single();

  if (error) {
    console.error("Error fetching count:", error.message);
    return;
  }

  return data.count;
}

 

incrementCount() 함수는 supabase.rpc('increment_count') 메서드 호출하여 jansori_count 테이블에서 카운트를 증가시킵니다. 이때, 'increment_count'는 위에서 우리가 생성한 RPC 함수 이름입니다.

- getCount() 함수는 supabase client를 통해 현재 카운트를 가져오는 기능을 합니다. 그냥 id 1번의 count 값을 가져오는 것입니다. 이는 row의 전체 수를 계산해서 가져오던 기존 방식보다 더 빠른 결과 값을 가져올 수 있죠.

 

참 쉽죠? 

 

(이 글에서는 Next.js 프로젝트의 supabase 설정에 대한 내용은 다루지 않습니다. supabase 설정이 되어있는 상태를 전제로 설명합니다.)

 

 

프론트엔드(Next.js 14)와 RPC 연결

이제 사용자가 "시작하기" 버튼을 누를 때마다 카운터가 증가하도록, 프론트엔드에서 RPC 호출을 연결합니다.

'시작하기 버튼' 처리 함수 onClick

  // '시작하기' 버튼 클릭 시 카운터 증가
  const onClick = async () => {
    await incrementCount(); // 카운터 증가 RPC 호출
	...
  };

 

사용자가 "시작하기" 버튼을 클릭할 때마다 incrementCount 함수가 호출하도록 함수를 호출합니다. 

 

 

버튼과 onClick 함수 연결

  return (
	...
      <button
        className="px-6 py-4 text-lg bg-deep-orange-300 text-white font-semibold rounded-lg"
        onClick={onClick} // 이 부분
      >
        명절 잔소리 시작하기
      </button>
	...
  );

 

이제 onClick 이벤트에서 incrementCount 함수를 호출하여 카운트를 증가시킵니다.

 

 

count 값 가져오는 getCount 함수 호출

  const { data: count, isLoading, isError } = useQuery({
    queryKey: ["jansoriCount"],
    queryFn: getCount, // 이 부분
  });

 

이 코드는 react-query를 사용하여 Supabase에서 실시간 카운트 값을 가져옵니다.

 

 

아래는 코드가 적용된 최종 화면입니다.

 

자, 이렇게 Supabase의 RPC 함수를 활용하여 동시성 문제를 해결하면서 실시간 카운터 기능을 구현했습니다. 

 

핵심 요약

- Supabase에서 동시성 문제없이 데이터를 업데이트하는 RPC 함수를 추가합니다.

- incrementCount() 함수를 통해 카운트 값을 증가시키고, getCount() 함수를 통해 실시간 데이터를 가져옵니다.

- 프론트엔드에서 사용자가 버튼을 누를 때마다 RPC 함수를 호출하여 데이터를 업데이트합니다.

 


 

아래는 예제로 사용된 [명절 잔소리 마스터] 웹사이트 입니다.

 

명절 잔소리 마스터

잔소리를 통해 대화 능력을 키워보세요.

holiday-jansori.vercel.app

 

반응형