<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Supabase on Kelly Dev</title><link>https://kelly-chui.github.io/tags/supabase/</link><description>Recent content in Supabase on Kelly Dev</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 10 Feb 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://kelly-chui.github.io/tags/supabase/index.xml" rel="self" type="application/rss+xml"/><item><title>SniffMEET. 세션 갱신과 정보 접근을 SessionManager에 캡슐화하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-session-manager-encapsulation/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-session-manager-encapsulation/</guid><description>&lt;h2 id="sessionmanager-사용성-개선"&gt;SessionManager 사용성 개선&lt;/h2&gt;
&lt;h3 id="세션을-갱신하는-방법"&gt;세션을 갱신하는 방법&lt;/h3&gt;
&lt;p&gt;Supabase 서버와의 통신이 필요할 때, 우선 세션의 유효성을 검증해야 한다.&lt;/p&gt;
&lt;p&gt;기존 방식에선 직접 AuthManager의 싱글톤 객체에서 세션을 갱신하는 작업을 했지만&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AuthManager와 SessionManager의 역할 명확하게 분리&lt;/li&gt;
&lt;li&gt;AuthManager의 싱글톤 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;와 같은 변화점이 있었고, 기존 AuthManager가 담당하던 세션 갱신, 복원 작업이 SessionManager로 옮겨졌다. SessionManager의 싱글톤 객체에서 세션의 유효성을 체크하는 컴퓨티드 프로퍼티의 부울리언 값을 체크하여 세션을 갱신하는 메소드를 호출하는 방식을 사용했다:&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;SessionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExpired&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SessionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refreshSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;이 방식은 SessionManager의 싱글톤 객체 호출이 두 번 일어나고, &lt;code&gt;refreshSession&lt;/code&gt;이라는 메소드를 직접 노출해서, SessionManager가 세션을 갱신하는 방법을 직접 노출하는 문제가 있다.&lt;/p&gt;</description></item><item><title>SniffMEET. ISP로 Supabase 요청 빌더 역할 나누기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-request-interface-segregation/</link><pubDate>Thu, 06 Feb 2025 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-request-interface-segregation/</guid><description>&lt;h2 id="들어가며"&gt;들어가며&lt;/h2&gt;
&lt;p&gt;Supabase REST API를 직접 감싸서 사용하다 보니, 유즈케이스 레이어에서 &lt;code&gt;eq.&lt;/code&gt;, &lt;code&gt;in.&lt;/code&gt;, &lt;code&gt;gte.&lt;/code&gt; 같은 쿼리 문자열을 직접 조립하는 코드가 늘어났다.&lt;/p&gt;
&lt;p&gt;처음에는 단순했지만, 요청이 많아질수록 오타 가능성이 커지고 Supabase의 세부 문법이 상위 레이어로 새어 나오는 문제가 있었다. 이번 글은 이 쿼리 구성 책임을 Supabase DB Layer 안으로 옮기기 위해 요청 빌더와 쿼리 파라미터 래퍼를 도입한 과정을 정리한다.&lt;/p&gt;
&lt;h2 id="기본-구조"&gt;기본 구조&lt;/h2&gt;
&lt;p&gt;SNMNetwork의 동작을 간단하게 설명하면, &lt;code&gt;SNMRequestConvertible&lt;/code&gt;을 채택하는 객체의 정보로 네트워크 요청을 생성하고, 통신한 다음, 응답을 반환하는 방식으로 동작한다.&lt;/p&gt;</description></item><item><title>SniffMEET. anon-key와 익명 사용자의 접근 권한 구분</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-anon-access-control/</link><pubDate>Thu, 28 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-anon-access-control/</guid><description>&lt;h2 id="anon-role과-anon-user의-접근-권한-차이"&gt;anon-role과 anon-user의 접근 권한 차이&lt;/h2&gt;
&lt;p&gt;Public Scheme에 있는 테이블을 &lt;code&gt;anon-key&lt;/code&gt;로는 접근할 수 있었지만, &lt;code&gt;anon-user&lt;/code&gt;의 JWT 토큰으로는 접근하지 못하는 문제가 있었다.&lt;br&gt;
처음에는 둘이 비슷한 범주라고 생각했지만, 실제로는 권한 체계와 접근 방식이 달라서 같은 문제로 볼 수 없었다.&lt;/p&gt;
&lt;h3 id="문제를-확인하기-전-생각한-내용"&gt;문제를 확인하기 전 생각한 내용&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;anon-key&lt;/code&gt;, &lt;code&gt;anon-role&lt;/code&gt;, &lt;code&gt;anon-user&lt;/code&gt;의 차이부터 다시 정리했다.&lt;br&gt;
&lt;code&gt;anon-key&lt;/code&gt;를 이용한 접근은 &lt;code&gt;anon-role&lt;/code&gt;로 보고, &lt;code&gt;anon-user&lt;/code&gt;는 이메일 주소나 전화번호를 등록하지 않은 유저를 의미한다. 이 경우 role 자체는 &lt;code&gt;authenticated-role&lt;/code&gt;이었다.&lt;/p&gt;
&lt;p&gt;익명 유저가 접근했을 때 &lt;code&gt;200 OK&lt;/code&gt;가 뜨지만 실제 값을 가져오지 못하는 경우도 있었다.&lt;br&gt;
그래서 접근 자체는 허용되지만 &lt;code&gt;RLS&lt;/code&gt;에서 막히고 있다고 추정할 수 있었다.&lt;/p&gt;</description></item><item><title>SniffMEET. Storage 버킷과 접근 제어</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-storage-access-control/</link><pubDate>Sun, 24 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-storage-access-control/</guid><description>&lt;p&gt;SniffMEET의 프로필 이미지는 사용자 정보와 성격이 다르다. 사용자 이름이나 소개처럼 구조화된 데이터는 Database에 저장하고, 이미지 파일은 Storage에 저장하는 편이 적합하다.&lt;/p&gt;
&lt;p&gt;Storage의 Postgres 테이블에는 파일 자체가 아니라 버킷과 객체의 메타데이터가 기록된다.&lt;/p&gt;
&lt;h2 id="버킷과-객체-경로"&gt;버킷과 객체 경로&lt;/h2&gt;
&lt;p&gt;버킷은 파일을 묶는 최상위 컨테이너다. 사용자마다 버킷을 만드는 것보다 용도별 버킷을 만들고 객체 경로로 사용자를 구분하는 편이 관리하기 쉽다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;profile-images/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── {user-id}/profile.jpg
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── {user-id}/thumbnail.jpg&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;이 구조에서는 &lt;code&gt;profile-images&lt;/code&gt; 버킷이 공통된 공개 범위와 파일 제한을 담당하고, 사용자 ID가 포함된 경로를 RLS 정책에서 검사할 수 있다.&lt;/p&gt;</description></item><item><title>SniffMEET. Supabase 세션 갱신 요청이 실패한 이유</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-refresh-token-request/</link><pubDate>Thu, 21 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-refresh-token-request/</guid><description>&lt;h2 id="문제"&gt;문제&lt;/h2&gt;
&lt;p&gt;Supabase 세션 갱신 요청이 제대로 동작하지 않았다. 세션 로직이나 네트워크 레이어 문제처럼 보였지만, 실제 원인은 요청 형식이었다. 당시 메모는 이랬다.&lt;/p&gt;
&lt;h2 id="수정"&gt;수정&lt;/h2&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;grant_type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;refreshToken&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;grant_type&lt;/code&gt; 값을 Swift식 camelCase로 보내고 있었다. Supabase가 기대하는 값은 &lt;code&gt;refresh_token&lt;/code&gt;이었다.&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;grant_type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;refresh_token&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;body도 마찬가지였다. Supabase가 기대하는 key는 &lt;code&gt;refreshToken&lt;/code&gt;이 아니라 &lt;code&gt;refresh_token&lt;/code&gt;이었다. refresh 요청에서는 기존 access token의 &lt;code&gt;Authorization&lt;/code&gt; 헤더도 제거했다. (토큰을 갱신받기 위한 요청인데, 만료된 토큰을 보내는거니까 아무 의미 없다!)&lt;/p&gt;
&lt;div class="code-theme-github"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;{ &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s"&gt;refresh_token&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;refreshToken&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\&amp;#34;&lt;/span&gt;&lt;span class="s"&gt; }&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;refresh 요청은 기존 access token으로 리소스에 접근하는 요청이 아니라, refresh token으로 새 세션을 발급받는 요청이기 때문이다.&lt;/p&gt;</description></item><item><title>SniffMEET. JWT와 Supabase RLS 정책에 연결하기</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-jwt-rls/</link><pubDate>Wed, 20 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-jwt-rls/</guid><description>&lt;p&gt;Supabase Auth로 로그인하면 클라이언트는 Access Token으로 JWT를 받는다. Supabase는 요청에 포함된 JWT를 검증하고, 그 안의 사용자 정보와 역할을 RLS 정책에 전달한다.&lt;/p&gt;
&lt;pre class="mermaid" data-mermaid-source="flowchart&amp;#43;LR%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;Client%5B%22%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%22%5D&amp;#43;--%3E%7C%22JWT%EB%A5%BC&amp;#43;%ED%8F%AC%ED%95%A8%ED%95%9C&amp;#43;%EC%9A%94%EC%B2%AD%22%7C&amp;#43;Supabase%5B%22Supabase&amp;#43;API%22%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;Supabase&amp;#43;--%3E%7C%22JWT&amp;#43;%EA%B2%80%EC%A6%9D%22%7C&amp;#43;Claims%5B%22sub%2C&amp;#43;role&amp;#43;%EB%93%B1%EC%9D%98&amp;#43;%ED%81%B4%EB%A0%88%EC%9E%84%22%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;Claims&amp;#43;--%3E&amp;#43;RLS%5B%22PostgreSQL&amp;#43;RLS&amp;#43;%EC%A0%95%EC%B1%85%22%5D%0A&amp;#43;&amp;#43;&amp;#43;&amp;#43;RLS&amp;#43;--%3E&amp;#43;Database%5B%22%ED%97%88%EC%9A%A9%EB%90%9C&amp;#43;%ED%96%89%EB%A7%8C&amp;#43;%EC%A1%B0%ED%9A%8C&amp;#43;%EB%98%90%EB%8A%94&amp;#43;%EB%B3%80%EA%B2%BD%22%5D"&gt;&lt;/pre&gt;&lt;h2 id="jwt의-클레임"&gt;JWT의 클레임&lt;/h2&gt;
&lt;p&gt;JWT는 헤더, 페이로드, 서명으로 구성된다. 각 부분은 Base64URL로 인코딩되지만 암호화되지는 않으므로 페이로드에 비밀 정보를 넣으면 안 된다. 서명은 헤더와 페이로드가 변조되지 않았는지 검증하는 데 사용한다.&lt;/p&gt;
&lt;p&gt;Supabase의 JWT에는 다음과 같은 클레임이 포함된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sub&lt;/code&gt;: 사용자의 고유 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;role&lt;/code&gt;: RLS를 적용할 PostgreSQL 역할&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iat&lt;/code&gt;: 토큰이 발급된 시각&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exp&lt;/code&gt;: 토큰이 만료되는 시각&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="supabase에서-jwt-사용하기"&gt;Supabase에서 JWT 사용하기&lt;/h2&gt;
&lt;p&gt;RLS는 PostgreSQL이 행 단위로 접근을 제한하는 기능이다. Supabase에서는 &lt;code&gt;auth.uid()&lt;/code&gt;로 현재 JWT의 &lt;code&gt;sub&lt;/code&gt; 값을 가져와 행의 사용자 ID와 비교할 수 있다.&lt;/p&gt;</description></item><item><title>SniffMEET. RLS와 테이블 권한</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-rls-empty-response/</link><pubDate>Wed, 20 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-rls-empty-response/</guid><description>&lt;h2 id="문제"&gt;문제&lt;/h2&gt;
&lt;p&gt;Supabase에서 anon key로 Public 테이블에 접근하면 데이터가 보이는데, 익명 로그인 후 받은 JWT access token으로 접근하면 &lt;code&gt;200 OK&lt;/code&gt;만 오고 데이터가 비어 있었다.&lt;/p&gt;
&lt;img loading="lazy" src="https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-rls-empty-response/image-003-optimized-image.webp"&gt;&lt;p&gt;처음에는 authenticated role이 anon role보다 상위 권한이라고 생각해서 더 헷갈렸다. 하지만 Supabase에서는 role의 상하관계보다 RLS 정책을 어떻게 작성했는지가 더 중요했다.&lt;/p&gt;
&lt;h2 id="확인"&gt;확인&lt;/h2&gt;
&lt;img loading="lazy" src="https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-rls-empty-response/image-002-optimized-image.webp"&gt;&lt;p&gt;JWT가 만료된 경우에는 &lt;code&gt;401 Unauthorized&lt;/code&gt;가 발생했다. 반면 RLS 조건에 맞지 않는 경우에는 요청 자체는 성공해서 &lt;code&gt;200 OK&lt;/code&gt;가 오지만, row가 필터링되어 데이터가 내려오지 않았다.&lt;/p&gt;
&lt;img loading="lazy" src="https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-rls-empty-response/image-004-optimized-image.webp"&gt;&lt;p&gt;authenticated 유저가 읽을 수 있도록 다음 policy를 추가하자 데이터를 읽을 수 있었다.&lt;/p&gt;</description></item><item><title>SniffMEET. 익명 로그인 세션의 생성·복원·갱신 흐름</title><link>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-anonymous-session-lifecycle/</link><pubDate>Thu, 14 Nov 2024 00:00:00 +0000</pubDate><guid>https://kelly-chui.github.io/posts/sniffmeet-dev-log-supabase-anonymous-session-lifecycle/</guid><description>&lt;h2 id="익명-로그인-생성-복원-갱신-로직-구현"&gt;익명 로그인 생성, 복원, 갱신 로직 구현&lt;/h2&gt;
&lt;p&gt;JWT, Refresh Token, 세션의 개념이 아직 명확하게 정리되지 않은 상태에서 익명 계정을 먼저 만들게 됐다. 개념이 명확하지 않아서, 세션 관리에 꽤 애를 먹었는데, 앱 재실행 이후에도 익명 계정을 어떻게 이어서 사용할지부터 정리할 필요가 있었다.&lt;/p&gt;
&lt;p&gt;생성, 복원, 갱신 로직은 서로 밀접하게 연결되어 있어서, 처음 앱을 실행했을 때와 다시 실행했을 때의 흐름을 먼저 구분해야 했다.&lt;/p&gt;
&lt;h3 id="실제-문제-해결-과정"&gt;실제 문제 해결 과정&lt;/h3&gt;
&lt;p&gt;먼저 시점별로 세션 상태를 표로 정리했다.&lt;/p&gt;</description></item></channel></rss>