목록으로

AI · 바이브 코딩

바이브 코딩 실전 13화: 내 폰의 데이터를 클라우드로

BeanCon
바이브 코딩으로 Flutter 앱의 로컬 데이터를 Supabase 클라우드 데이터베이스와 API에 연결하는 과정을 설명하는 대표 이미지

바이브 코딩 실전 13화입니다. Flutter MyLog 앱의 로컬 데이터를 Supabase 클라우드 DB에 저장하는 구조를 정리했습니다. Postgres records 테이블, user_id 연결, Data API, Insert와 Select, Row Level Security, 권한 정책, 사용자별 데이터 격리, SQLite 데이터 이전, 네트워크 오류 처리와 사용자 두 명 테스트까지 다룹니다.

목차

지난 편에서 MyLog에 회원가입과 로그인을 붙였습니다.

이제 앱은 적어도

지금 사용하고 있는 사람이 누구인지

알 수 있습니다.

하지만 조금 이상한 상태입니다.

사용자 A로 로그인해도 기록은 SQLite에 있고,

사용자 B로 로그인해도 같은 스마트폰의 SQLite를 사용합니다.

즉 현재 구조를 그림으로 보면 이렇습니다.

사용자 A
   ↓
Supabase Auth
   ↓
MyLog
   ↓
SQLite
   ↓
기기 안의 기록

로그인은 클라우드인데 데이터는 여전히 스마트폰 안에 있습니다.

오늘은 이 구조를 바꿔보겠습니다.

사용자
   ↓
Supabase Auth
   ↓
MyLog
   ↓
Supabase Data API
   ↓
Postgres Database

드디어 MyLog가 온라인 서비스가 됩니다. ☁️

그런데 이번 편에서 가장 중요한 것은 클라우드에 데이터를 넣는 것 자체가 아닙니다.

사용자 A는 A의 기록만 보고, 사용자 B는 B의 기록만 보게 만드는 것.

여기가 핵심입니다.

1. 클라우드 DB를 쓰면 뭐가 달라질까?

지금까지 SQLite는 꽤 잘 일했습니다.

앱을 종료해도 기록이 남았고,

인터넷이 없어도 사용할 수 있었습니다.

그런데 스마트폰을 바꾸면 이야기가 달라집니다.

기록이 이전 스마트폰 안에 있기 때문입니다.

기존 스마트폰

SQLite
↓
내 기록 100개


새 스마트폰

SQLite
↓
텅 비어 있음

클라우드에 저장하면 구조가 달라집니다.

Android
    ↘
     Cloud DB
    ↗
iPhone

같은 계정으로 로그인하면 서버에서 자신의 데이터를 가져올 수 있습니다.

스마트폰은 더 이상 데이터의 유일한 집이 아닙니다.

2. 그런데 서버를 직접 만들어야 하지 않나요?

#02에서 우리는 앱과 DB 사이에 서버와 API가 있다고 배웠습니다.

앱

↓

API

↓

서버

↓

Database

그렇다면 이제 Node.js나 Java 같은 백엔드 서버부터 만들어야 할까요?

이번 MyLog에서는 아직 그럴 필요가 없습니다.

Supabase의 Flutter SDK는 Data API를 통해 Postgres 데이터를 조회하고 변경할 수 있습니다. Supabase Auth로 로그인한 상태에서는 사용자의 인증 토큰이 데이터 요청에 함께 사용되고, 여기에 RLS 정책을 결합하면 사용자별 접근 제어를 데이터베이스에서 적용할 수 있습니다.

즉 현재 구조는 대략 이렇게 생각할 수 있습니다.

Flutter

↓

supabase_flutter

↓

Supabase Data API

↓

Postgres

우리에게 필요한 API 계층을 Supabase가 제공해주는 셈입니다.

3. API를 직접 만들지 않는다고 API가 없는 것은 아니다

이 부분은 비개발자가 처음에 조금 헷갈릴 수 있습니다.

우리는

GET /records
POST /records

같은 REST API를 직접 만들지 않습니다.

하지만 Flutter에서 이렇게 작성하면

supabase.from('records').select();

실제로는 네트워크를 통해 Supabase Data API에 요청이 전달됩니다.

새로운 데이터를 넣는

supabase.from('records').insert(...);

역시 마찬가지입니다. Supabase의 현재 Flutter API는 insert()를 이용한 데이터 삽입과 select()를 이용한 조회를 지원합니다.

그러니까 코드가 간단해졌을 뿐,

뒤에서는 여전히

요청

↓

인증

↓

API

↓

DB

↓

응답

이라는 구조가 움직이고 있습니다.

4. 가장 먼저 Record 구조부터 바꾼다

지금까지 MyLog의 Record는 이랬습니다.

Record

id
title
content
createdAt

그런데 이제 여러 사용자가 같은 클라우드 DB를 사용합니다.

그래서 반드시 하나가 더 필요합니다.

userId

최종적으로는

Record

id
userId
title
content
createdAt

입니다.

예를 들어

Record 1

userId = AAA
title = 오늘의 기록


Record 2

userId = BBB
title = 여행 메모

처럼 데이터의 주인이 명확해집니다.

지난 편에서 로그인 기능을 만든 진짜 이유가 여기서 드러납니다.

5. Supabase에 records 테이블을 만든다

이제 Postgres에 실제 테이블을 만들겠습니다.

예를 들면 다음 정도면 충분합니다.

create table public.records (
  id uuid primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  title text not null,
  content text not null default '',
  created_at timestamptz not null default now()
);

하나씩 보면 어렵지 않습니다.

id

기록 자체의 고유 ID

user_id

이 기록을 소유한 사용자의 ID

title

제목

content

본문

created_at

작성 시각

그리고

references auth.users(id)

를 통해 Supabase Auth의 사용자와 기록을 연결합니다.

이제 데이터베이스에서도

이 기록은 이 사용자의 것이다.

라는 관계가 생깁니다.

6. user_id에는 인덱스도 만들어두자

기록이 몇 개 없을 때는 체감하기 어렵습니다.

하지만 나중에 데이터가 많아지면 거의 모든 요청에서

이 사용자의 기록만 찾기

를 하게 됩니다.

그래서 user_id에 인덱스를 두는 것이 좋습니다.

create index records_user_id_idx
on public.records(user_id);

Supabase의 현재 RLS 문서에서도 정책에서 자주 사용하는 user_id 같은 컬럼에는 적절한 인덱스를 두면 성능에 도움이 될 수 있다고 안내하고 있습니다.

첫 MVP라고 성능 최적화를 잔뜩 할 필요는 없지만,

데이터 소유자를 기준으로 계속 검색할 컬럼

이라는 점은 미리 알고 가겠습니다.

7. 그리고 오늘 가장 중요한 RLS를 켠다

여기서 진짜 중요한 부분입니다.

Row Level Security

줄여서 RLS입니다.

이름은 어렵지만 하는 일은 매우 명확합니다.

이 사용자가 이 데이터 한 줄에 접근해도 되는가?

를 DB가 판단합니다.

예를 들어 사용자 A가 로그인했습니다.

DB에는 이런 데이터가 있습니다.

Record 1 → user A
Record 2 → user A
Record 3 → user B
Record 4 → user B

사용자 A가 기록을 조회하면

Record 1
Record 2

만 보내줘야 합니다.

Record 3과 4는 아예 접근할 수 없어야 합니다.

8. 화면에서 숨기는 것과 DB에서 막는 것은 다르다

이 부분은 정말 중요합니다.

Flutter 코드에서 이렇게 작성했다고 해보겠습니다.

.eq('user_id', currentUser.id)

그러면 자기 기록만 조회합니다.

좋아 보입니다.

하지만 누군가 앱을 변조하거나 직접 API 요청을 만들어 필터를 제거하면 어떻게 될까요?

클라이언트 코드만 믿으면 다른 데이터를 요청할 수도 있습니다.

그래서 보안은

Flutter

"내 기록만 요청"

에서 끝나면 안 됩니다.

DB에서도

Postgres

"당신은 당신 기록만 볼 수 있습니다."

라고 강제해야 합니다.

그 역할이 RLS입니다.

Supabase 공식 문서에서도 외부에 노출되는 스키마의 테이블에서는 RLS를 활성화해야 하며, RLS 정책을 Auth와 결합해 행 단위 접근을 제한하도록 안내합니다.

9. RLS를 활성화한다

SQL로 테이블을 만들었다면 명시적으로 활성화해줍니다.

alter table public.records
enable row level security;

중요한 특징이 있습니다.

RLS를 켜놓고 정책을 하나도 만들지 않으면,

클라이언트에서 데이터에 접근할 수 없습니다.

처음에는

분명 데이터 넣었는데 왜 안 나오지?

라고 당황할 수 있습니다.

하지만 오히려 좋은 상태입니다.

문이 잠겼고 이제 누구에게 어떤 문을 열어줄지 정하는 단계니까요.

참고로 Supabase Dashboard의 Table Editor로 만든 테이블은 RLS가 기본 활성화될 수 있지만, SQL로 직접 생성했다면 상태를 명확히 확인하는 편이 좋습니다.

10. 자신의 기록만 읽을 수 있게 한다

첫 번째 정책은 조회입니다.

create policy "Users can read own records"
on public.records
for select
to authenticated
using (
  (select auth.uid()) = user_id
);

핵심 부분은 이것입니다.

auth.uid() = user_id

auth.uid()는 현재 로그인해 요청하고 있는 사용자의 ID입니다.

즉 DB가 매 행마다 확인합니다.

현재 사용자 ID

=

이 Record의 user_id

?

같으면 보여줍니다.

다르면 보여주지 않습니다.

Supabase 공식 RLS 문서도 자기 데이터만 조회하는 대표적인 정책으로 auth.uid()와 user_id를 비교하는 방식을 안내하고 있습니다.

11. 저장할 때도 자기 user_id만 넣을 수 있어야 한다

조회만 막아놓으면 충분할까요?

아닙니다.

사용자 A가 이런 데이터를 저장하려고 할 수도 있습니다.

user_id = B

그래서 Insert에도 정책을 만듭니다.

create policy "Users can insert own records"
on public.records
for insert
to authenticated
with check (
  (select auth.uid()) = user_id
);

이번에는 using이 아니라

with check

가 등장합니다.

아주 단순하게 생각하면

DB에 새롭게 들어갈 데이터가 조건을 만족하는지 확인한다

는 의미입니다.

사용자 A가

user_id = A

를 넣으면 통과.

user_id = B

를 넣으면 거부합니다.

클라이언트를 믿지 않고 DB에서 다시 확인합니다.

보안은 약간 의심 많은 경비원처럼 행동하는 편이 좋습니다. 🕵️

12. 최신 Supabase에서는 권한과 RLS를 둘 다 생각해야 한다

여기서 하나 더 알아둘 것이 있습니다.

현재 Supabase Data API에서는 Postgres 권한(Grants) 과 RLS 정책이 서로 다른 역할을 합니다.

예를 들어 authenticated 사용자에게 필요한 테이블 권한을 주고,

그 안에서 어떤 행에 접근할 수 있는지는 RLS 정책으로 제한하는 식입니다.

현재 Supabase 공식 Flutter 설치 문서도 RLS를 먼저 활성화하고 필요한 정책을 만든 뒤, 각 역할에 필요한 최소한의 select, insert, update, delete 권한만 부여하는 방식을 안내합니다.

현재 MyLog에서는 아직 조회와 저장만 하므로 개념적으로는

authenticated

SELECT 가능
INSERT 가능

정도면 충분합니다.

수정과 삭제는 그 기능을 만들 때 열겠습니다.

문을 아직 사용하지 않는데 열쇠부터 전부 나눠줄 필요는 없습니다.

13. AI에게 RLS를 절대 "편하게" 풀게 하지 않는다

여기서 바이브 코딩의 위험한 순간이 하나 등장합니다.

Flutter에서 DB를 조회했더니 Permission Error가 발생했습니다.

AI에게 말합니다.

안 되는데 해결해줘.

그러자 AI가 이런 SQL을 제안할 수 있습니다.

create policy "Allow everything"
on records
for all
using (true)
with check (true);

잘 됩니다.

너무 잘 됩니다.

사용자 A도 보고,

사용자 B도 보고,

다 봅니다. 😇

개발 중 권한 오류를 없애려고

using (true)

같은 광범위 정책을 사용하는 것은 실제 사용자 데이터에서는 매우 조심해야 합니다.

프로젝트 규칙에도 추가하겠습니다.

## Database Security

- RLS 오류를 해결하기 위해 전체 접근 허용 정책을 임의로 만들지 않는다.
- 사용자 데이터는 auth.uid()와 user_id를 기준으로 제한한다.
- 보안 정책을 제거해서 기능 문제를 해결하지 않는다.

기능이 안 된다고 현관문을 떼어내지는 않겠습니다.

14. 이제 Flutter에서 Record를 클라우드에 저장한다

로그인한 사용자를 먼저 가져옵니다.

개념적으로는

final user = supabase.auth.currentUser;

입니다.

로그인하지 않은 사용자가 저장하려고 한다면 중단합니다.

그리고 새 Record를 저장합니다.

await supabase.from('records').insert({
  'id': record.id,
  'user_id': user.id,
  'title': record.title,
  'content': record.content,
  'created_at': record.createdAt.toIso8601String(),
});

Supabase Flutter에서는 이런 식으로 insert()에 Map을 전달해 한 행을 추가할 수 있습니다.

이제 저장 흐름이 완전히 달라졌습니다.

사용자 입력

↓

Record 생성

↓

현재 사용자 ID 추가

↓

Data API

↓

RLS 검사

↓

Postgres 저장

15. 기록을 읽어오는 것도 바뀐다

지금까지는 SQLite에서 읽었습니다.

이제 Supabase에서 조회합니다.

final data = await supabase
    .from('records')
    .select()
    .order('created_at', ascending: false);

현재 Supabase Flutter API에서는 select()로 데이터를 조회하고 order()로 결과 순서를 지정할 수 있습니다.

여기서 중요한 부분이 하나 있습니다.

코드에

user_id = 현재 사용자

필터가 없는데도 괜찮을까요?

RLS가 정상적으로 설정됐다면 DB가 자기 데이터만 반환합니다.

물론 코드에서도 사용자 ID 조건을 명시할 수 있습니다.

하지만 보안의 최종 방어선은 Flutter 코드가 아니라 RLS입니다.

16. DB에서 받은 데이터를 다시 Record로 바꾼다

Supabase에서 데이터가 돌아오면 대략 Map 형태입니다.

{
  id: ...,
  user_id: ...,
  title: ...,
  content: ...,
  created_at: ...
}

우리 Flutter 화면은 Record 객체를 사용하고 있습니다.

그래서 다시 변환합니다.

Postgres Row

↓

Map

↓

Record

↓

Flutter UI

#11에서 SQLite를 사용할 때도 비슷한 과정을 봤습니다.

저장소가 SQLite에서 Supabase로 바뀌었을 뿐,

앱의 데이터 모델과 외부 저장 형태 사이에 변환 과정이 존재한다

는 개념은 똑같습니다.

17. API가 들어오자 화면 상태가 또 늘어난다

SQLite는 스마트폰 안에 있었습니다.

이번에는 인터넷 너머에 데이터가 있습니다.

그래서 상황이 조금 복잡해집니다.

목록 화면에는 이제 최소한 이런 상태가 필요합니다.

Loading

Data

Empty

Error

기록 불러오는 중...

↓

성공
├── 기록 있음
└── 기록 없음

또는

↓

오류

입니다.

네트워크 기능이 하나 들어왔을 뿐인데 화면이 갑자기 바빠졌습니다.

제품 개발에서 API를 붙이면 항상 이런 친구들이 따라옵니다.

18. 저장 버튼도 네트워크를 기다려야 한다

SQLite에서는 저장이 빠르게 끝났습니다.

이제 서버 응답을 기다려야 합니다.

저장 버튼 클릭

↓

저장 중...

↓

Data API 요청

↓

성공 / 실패

저장 중에 사용자가 버튼을 열 번 누르게 두면 좋지 않습니다.

따라서

저장 중...

[저장 중]

처럼 중복 입력을 막을 수 있습니다.

그리고 실패했다면 기록이 저장됐다고 거짓말하면 안 됩니다.

기록 저장에 실패했습니다.
다시 시도해주세요.

처럼 알려줘야 합니다.

19. 인터넷을 끊으면 드디어 문제가 생긴다

자, 와이파이를 끄겠습니다.

새 기록을 작성합니다.

저장합니다.

실패합니다.

정상입니다.

현재 우리가 만든 구조는 클라우드 DB를 직접 사용하는 온라인 방식이기 때문입니다.

여기서 선택지가 생깁니다.

방법 A

인터넷이 없으면 저장하지 않는다.

방법 B

SQLite에 먼저 저장하고 인터넷이 연결되면 서버와 동기화한다.

제품 관점에서는 B가 더 멋져 보입니다.

하지만 구현 난이도가 갑자기 올라갑니다.

Local Record

↓

Cloud Record

↓

동기화 여부

↓

중복 처리

↓

수정 충돌

↓

삭제 동기화

기능 하나가 갑자기 가족을 데리고 왔습니다.

그래서 이번 MVP에서는 온라인 상태에서 클라우드 저장까지 구현합니다.

오프라인 동기화는 나중의 확장 문제로 남겨두겠습니다.

20. 그러면 기존 SQLite 데이터는 어떻게 할까?

오늘 꽤 중요한 선택이 하나 있습니다.

이전 편에서 작성한 기존 로컬 기록이 이미 있습니다.

그냥 버려도 될까요?

연재 실습에서는 실제로 한번 클라우드로 옮겨보겠습니다.

개념적인 마이그레이션은 다음과 같습니다.

로그인

↓

SQLite 기존 기록 조회

↓

각 Record에 현재 userId 연결

↓

Supabase 업로드

↓

모든 업로드 성공 확인

↓

로컬 Migration 완료 표시

여기서 아주 중요한 조건이 하나 있습니다.

업로드 성공 전에 로컬 데이터를 지우면 안 됩니다.

네트워크 오류가 났는데 먼저 삭제하면 데이터가 사라집니다.

21. 같은 데이터를 두 번 올리는 문제도 생각해야 한다

첫 번째 마이그레이션 도중 앱이 종료됐다고 해보겠습니다.

다시 실행합니다.

또 마이그레이션합니다.

같은 Record가 두 번 생길 수도 있습니다.

그래서 Record의 기존 ID를 그대로 클라우드에서도 사용하는 방식이 도움이 됩니다.

이미 같은 id가 있다면 중복을 판단할 수 있기 때문입니다.

Supabase Flutter는 Primary Key 등을 이용해 필요에 따라 upsert() 방식도 지원합니다. 동일한 키가 없으면 추가하고, 있으면 기존 데이터를 갱신하는 방식입니다.

다만 이번 단계에서 자동 동기화 시스템까지 만들지는 않겠습니다.

마이그레이션이 필요한 실제 서비스라면

중복 방지
실패 복구
완료 여부
재시도

를 별도로 설계해야 합니다.

22. 개발 중인 MyLog라면 더 단순하게 갈 수도 있다

사실 현재 MyLog는 우리가 만든 실습 앱입니다.

실제 사용자가 수천 명 있는 서비스가 아닙니다.

그래서 개발 단계라면 기존 SQLite 테스트 데이터를 초기화하고 클라우드 데이터부터 새로 작성하는 방법도 충분히 가능합니다.

중요한 것은

데이터를 버려도 되는 상황인지 아닌지 사람이 결정하는 것입니다.

AI가 편하다는 이유로 기존 데이터를 자동 삭제하게 두지 않습니다.

프로젝트가 제품 단계에 가까워질수록 데이터 삭제는 매우 신중해야 합니다.

23. AI에게 이번 작업을 이렇게 맡겨보자

이제 요구사항이 제법 구체적입니다.

PRD, PROJECT_RULES, APP_FLOW을 확인해줘.

MyLog의 Record 데이터를
SQLite 중심 구조에서 Supabase Postgres 중심 구조로 변경한다.

Record 구조:
- id
- userId
- title
- content
- createdAt

Supabase의 public.records 테이블을 사용한다.

보안 요구사항:
- RLS를 반드시 사용한다.
- 로그인한 사용자는 자신의 Record만 조회할 수 있다.
- 로그인한 사용자는 자신의 userId로만 Record를 생성할 수 있다.
- RLS를 using(true) 같은 전체 허용 방식으로 해결하지 않는다.
- Secret Key 또는 service_role을 Flutter에 사용하지 않는다.

이번 구현 범위:
1. Record에 userId 추가
2. Cloud Record 저장
3. Cloud Record 조회
4. 작성일 기준 목록 정렬
5. Loading 상태
6. Empty 상태
7. Network/Error 상태
8. 저장 중 중복 클릭 방지

이번 단계에서 구현하지 않는다:
- Record 수정
- Record 삭제
- 검색
- 실시간 동기화
- 완전한 Offline Sync
- 사진 업로드

기존 SQLite 데이터 처리 방안도 먼저 제안해줘.

아직 코드를 수정하지 말고
DB Schema, RLS Policy, 변경 파일,
데이터 흐름과 위험 요소를 먼저 설명해줘.

이 정도면 AI가 추측해야 할 영역이 상당히 줄었습니다.

24. SQL도 AI에게 만들게 한 뒤 우리가 확인한다

AI가 SQL을 만들어줬다면 그냥 실행하지 않습니다.

특히 확인할 것은 이것입니다.

RLS enabled?

user_id가 auth.users를 참조하는가?

SELECT Policy가 자기 기록만 허용하는가?

INSERT Policy가 자기 userId만 허용하는가?

authenticated 역할에 필요한 권한만 줬는가?

public 전체 허용 정책이 있는가?

Database SQL은 버튼 색상을 바꾸는 코드보다 훨씬 조심해서 봐야 합니다.

한 번 잘못된 정책을 적용하면 모든 사용자 데이터에 영향을 줄 수 있기 때문입니다.

25. 그리고 반드시 사용자 두 명으로 테스트한다

오늘 테스트에서 가장 중요한 부분입니다.

계정을 두 개 만듭니다.

User A

User B

먼저 A로 로그인합니다.

A 기록 1
A 기록 2

를 작성합니다.

로그아웃합니다.

B로 로그인합니다.

이때 화면에는

A 기록 1
A 기록 2

가 보여서는 안 됩니다.

B 계정에서는

아직 작성한 기록이 없습니다.

가 나와야 합니다.

B가 기록 하나를 작성합니다.

로그아웃.

다시 A 로그인.

A에게는 다시

A 기록 1
A 기록 2

만 보여야 합니다.

이 테스트가 통과해야 오늘의 핵심 기능이 성공한 것입니다.

26. Flutter 필터만 제거해서도 확인해보자

가능하다면 개발 중 한 번쯤 이런 검증도 해볼 가치가 있습니다.

Flutter 코드의

user_id 조건

에만 의존하지 않고,

RLS가 실제로 데이터를 보호하는지 확인합니다.

즉 앱에서 별도의 사용자 필터를 빼거나 Supabase SQL 도구 등을 이용해 정책을 점검해봅니다.

사용자 A의 인증으로 B의 Row를 조회할 수 없어야 합니다.

이 검증이 중요한 이유는 하나입니다.

보안이 UI 구현에 의존하지 않는다는 것을 확인하기 위해서입니다.

27. 로그인하지 않은 상태도 테스트한다

지난 편에서 배웠듯 인증되지 않은 요청에서는 auth.uid()가 null이 될 수 있습니다. Supabase는 RLS 정책에서 인증된 사용자만을 명시하고 auth.uid()와 데이터 소유자를 비교하는 방식을 권장합니다.

따라서 로그아웃 상태에서 직접 DB 요청이 발생하지 않는지도 확인합니다.

정상적인 MyLog라면 애초에 Auth Gate가 로그인 화면으로 보내야 합니다.

그리고 혹시 요청이 발생해도 DB가 데이터를 내주지 않아야 합니다.

화면에서도 막고, DB에서도 막습니다.

28. 오늘의 삽질 방지 팁 🛠️

Supabase에서 데이터가 안 보이면 무조건 RLS부터 끄지 마세요.

AI에게도 이렇게 요청하는 편이 좋습니다.

현재 Supabase 조회가 권한 오류로 실패한다.

RLS를 끄거나 전체 허용 정책을 만들지 말고

1. 현재 로그인 Session
2. 요청 역할
3. Table Grant
4. SELECT Policy
5. auth.uid()
6. Record의 user_id

순서로 원인을 확인해줘.

이 순서가 꽤 중요합니다.

권한 문제가 발생했다고 보안을 제거하면 디버깅은 쉬워질 수 있습니다.

하지만 우리가 원하는 것은

작동하는 앱

이 아니라

안전하게 작동하는 앱

입니다.

29. SQLite는 이제 완전히 필요 없을까?

아직 파일을 당장 지우지는 않겠습니다.

클라우드 전환이 정상적으로 확인되고,

기존 데이터 처리까지 끝난 다음 정리합니다.

그리고 훗날 우리가

오프라인에서도 기록 작성

을 지원하고 싶다면 SQLite가 다시 중요한 역할을 맡을 수도 있습니다.

그때 구조는 이렇게 될 수 있습니다.

Flutter UI

↓

Local Database

↓

Sync Layer

↓

Cloud Database

하지만 Sync Layer는 생각보다 만만한 녀석이 아닙니다.

현재는 만나지 않겠습니다.

멀리서 손만 흔들겠습니다. 👋

30. API 호출이 많아지면 서비스 계층도 고민할 시점이다

지금까지 화면에서 직접 DB를 호출해도 규모가 작아서 큰 문제가 없었습니다.

하지만 앞으로

저장,

조회,

수정,

삭제,

검색

API가 늘어난다면 Supabase 코드를 화면 곳곳에 흩뿌리는 것은 좋지 않습니다.

그래서 이제 이런 구조를 고려할 수 있습니다.

screens/

models/

services/
    record_service.dart

RecordService는

createRecord()

getRecords()

정도의 책임을 가집니다.

화면은

기록 저장해줘.

라고 요청하고,

실제 Supabase 호출은 Service가 담당합니다.

우리가 #11에서 SQLite 코드를 LocalDatabase로 분리했던 것과 같은 이유입니다.

다만 오늘도 과하게 만들지는 않습니다.

필요한 구조 한 겹만 추가합니다.

31. AI에게 API 오류를 그대로 숨기게 하지 않는다

서비스 호출 중 오류가 발생했다고 해보겠습니다.

사용자 화면에서는

기록을 불러오지 못했습니다.

[다시 시도]

정도로 보여줄 수 있습니다.

하지만 개발 단계에서는 원인을 확인할 수 있어야 합니다.

사용자 메시지

기록을 불러오지 못했습니다.

개발 로그

Network Error
Permission Error
Database Error

는 분리합니다.

물론 인증 토큰이나 개인정보를 로그에 남기지는 않습니다.

제품의 오류 처리는

사용자에게는 이해할 수 있게, 개발자에게는 추적할 수 있게

가 기본 방향입니다.

32. 이제 데이터는 내 폰이 아니라 사용자에게 속한다

이 변화가 이번 편에서 가장 중요합니다.

이전에는

이 스마트폰의 기록

이었습니다.

이제는

이 사용자의 기록

이 됩니다.

완전히 다른 모델입니다.

사용자 A가 Android에서 기록을 작성합니다.

클라우드에 저장됩니다.

나중에 iPhone 앱을 만들고 같은 계정으로 로그인합니다.

DB에서 동일한 사용자 ID의 기록을 읽어옵니다.

이제

기기 중심 앱

↓

계정 중심 서비스

로 넘어가기 시작했습니다.

33. 정상이라면 Git에 저장한다

직접 테스트합니다.

A 계정과 B 계정의 데이터가 제대로 나뉘고,

앱을 재실행해도 클라우드 기록이 정상적으로 불러와지고,

오류 처리까지 확인했습니다.

이제

git status

와 Diff를 확인합니다.

특히

RLS 관련 SQL

Record 모델

Supabase 호출 코드

SQLite 변경

프로젝트 문서

를 확인합니다.

정상이라면 커밋합니다.

예를 들면

feat: 사용자별 Supabase 클라우드 기록 저장 구현

이제 Git 기록은 이렇게 자랍니다.

Flutter 프로젝트

↓

기록 작성·목록·상세

↓

SQLite 로컬 저장

↓

회원가입·로그인

↓

Cloud Database + RLS ← SAVE

MyLog가 꽤 앱다워졌습니다.

34. 오늘의 완료 조건

이번 편도 체크해보겠습니다.

[ ] Record에 userId가 추가됐다.

[ ] Supabase에 records 테이블을 만들었다.

[ ] records.user_id가 Auth 사용자와 연결되어 있다.

[ ] user_id 검색을 위한 인덱스를 검토했다.

[ ] RLS가 활성화되어 있다.

[ ] 로그인한 사용자는 자신의 Record만 조회할 수 있다.

[ ] 다른 사용자의 userId로 Record를 생성할 수 없다.

[ ] Flutter에서 Secret Key를 사용하지 않는다.

[ ] 새 Record가 Supabase에 저장된다.

[ ] 앱 실행 시 Cloud Record를 불러온다.

[ ] 기록이 작성일 기준으로 표시된다.

[ ] Loading 상태가 있다.

[ ] Empty 상태가 있다.

[ ] 네트워크 또는 API 오류 상태가 있다.

[ ] 저장 중 중복 요청을 방지한다.

[ ] User A와 User B 데이터 분리 테스트를 했다.

[ ] 기존 SQLite 데이터의 처리 방법을 결정했다.

[ ] 정상 상태를 Git Commit으로 남겼다.

여기까지 됐다면 MyLog는 이제 단순한 로컬 기록 앱이 아닙니다.

로그인한 사용자의 데이터를 클라우드에서 가져오는 온라인 서비스의 기본 구조를 가지게 됐습니다.

35. 그런데 이제 진짜 스마트폰에서 실행해봐야 한다

에뮬레이터에서는 잘 됩니다.

개발 PC의 빠른 인터넷에서도 잘 됩니다.

Supabase 연결도 잘 됩니다.

여기서 만족하면 안 됩니다.

다음에는 실제 Android 스마트폰이나 iPhone에 설치해보겠습니다.

그리고 높은 확률로 새로운 문제를 만나게 됩니다.

키보드가 입력창을 가리고,

네트워크가 느리면 로딩 화면이 오래 보이고,

화면 크기가 달라지고,

권한이나 플랫폼 설정 문제가 나타날 수도 있습니다.

에뮬레이터에서는 발견하지 못했던 현실 세계가 드디어 들어옵니다.

다음 편

[바이브 코딩 실전 #14] 드디어 실제 스마트폰에서 실행해봤습니다 | MVP 첫 실기기 테스트

다음 편에서는 기능을 추가하지 않습니다.

대신 우리가 지금까지 만든 앱을 직접 스마트폰에 넣고,

일부러 흔들어보고,

인터넷도 끊어보고,

앱도 껐다 켜보고,

화면도 여기저기 눌러보겠습니다.

개발자의 컴퓨터에서는 얌전했던 MyLog가 실제 스마트폰에서도 얌전한지 확인할 차례입니다. 📱🔍

연재 기준: 2026년 8월Supabase의 Flutter SDK, Data API 권한 구조와 RLS 작성 방식은 업데이트될 수 있습니다. 현재 공식 문서에서는 노출된 데이터 테이블에 RLS를 적용하고, 필요한 Postgres 권한만 부여하며, Supabase Auth의 사용자 ID와 RLS 정책을 결합해 사용자별 데이터 접근을 제한하는 방식을 안내하고 있습니다. 실제 프로젝트에서는 적용 시점의 공식 문서를 다시 확인하는 것을 권장합니다.

#바이브코딩 #VibeCoding #AI코딩 #Flutter #Supabase #SupabaseDatabase #PostgreSQL #RLS #RowLevelSecurity #API연동 #클라우드DB #사용자별데이터 #비개발자앱개발 #MVP개발 #MyLog #바이브코딩실전

댓글

0

댓글을 불러오는 중입니다.