본문 바로가기
App/SaveFood

하나의 보관함 여러명의 사용자

by 조현성 2025. 6. 26.

썸네일

세이브푸드(SaveFood) 앱을 처음 기획할 때만 해도 데이터 구조는 단순할 것이라고 생각했다. 사용자 정보를 저장하고, 보관함을 만들고, 그 보관함 안에 식자재를 넣는 단순한 CRUD(Create: 생성, Read: 읽기, Update: 수정, Delete: 삭제) 형태. 이 정도면 파이어스토어(Firestore)로도 충분할 줄 알았다.

 

하지만 개발을 시작하고 몇 주가 지나면서 하나둘씩 생기는 아이디어 속에 요구사항과 기능을 추가 하다보니, 처음의 단순한 스키마는 곧 무너졌다.

'내' 보관함이 아니라 '우리' 보관함

모든 건 "공유"라는 한 단어에서 시작 되었다. 세이브푸드는 기본적으로 사용자 개인의 냉장고 보관함을 관리하는 앱이지만, 이 보관함을 다른 가족 구성원이나 룸메이트와 함께 공유하고 관리하고 싶은 수요를 대응 하고 싶었다. "나만의 보관함"이 아니라, "우리의 보관함"인 것이다. 그런데 이 공유라는 개념 하나가 Firestore구조를 뒤흔들기 시작했다.

세이브푸드 냉장고 배경화면

 

Firestore는 NoSQL 구조이기 때문에 관계형 데이터베이스처럼 JOIN이 불가능하다. 그래서 보통은 데이터를 중복 저장하거나, 1:N 또는 N:1 관계를 직접 컬렉션 구조로 설계해야 한다. 그래서 고민 끝에 아래와 같은 구조를 설계했다.

  • users 컬렉션 : 각 사용자 정보 저장, userStorages 배열로 보관함 목록 관리
  • storages 컬렉션 : 보관함 자체를 저장, 소유자(ownerUID), 공유자(sharedUserUIDs) 정보 포함 
  • foods 컬렉션 : 각 storage 문서 아래 foods라는 서브컬렉션을 만들어 식자재 항목 관리

이 구조는 꽤 단단해 보였지만, 실제 기능을 얹어가면서 자잘한 문제들이 계속 튀어나왔다.

구조를 잡기까지 3번은 뒤엎었다

처음에는 storage 문서에 ownerEmail만 저장해도 될 줄 알았다. 사용자 UID도 몰랐고, Firebase Authentication에서 email만 받아오면 되는 줄 알았기 때문이다. 그러나 푸시 알림 기능을 붙이면서 사용자 UID를 반드시 저장해야 하는 상황이 생겼고, 결국 ownerEmail 대신 ownerUID를 기준으로 전면 수정해야 했다.

 

두 번째는 공유 기능을 넣으면서 발생한 문제였다. sharedUserUIDs를 배열로 넣고, 사용자 초대와 수락 상태를 별도로 관리하려고 invitation 컬렉션을 만들었지만, 초대 수락 후 sharedUserUIDs에 다시 반영해줘야 했고, 이 과정에서 sharedUserUIDs와 invitation 수락 상태가 어긋나는 오류가 빈번하게 발생했다. 결국 invitation 기반 로직을 제거하고, 수락과 동시에 sharedUserUIDs에 바로 추가하는 구조로 리팩토링 했다.

 

세 번째는 순서였다. 사용자 입장에서 보관함 순서는 매우 중요했다. "김치냉장고"를 첫 번째에, "와인냉장고"를 두 번째에 놓는 것만으로도 앱 사용성이 크게 달라졌다. 이걸 위해 users컬렉션 내 userStorages 배열에 각 보관함의 id와 함께 orderIndex값을 저장하도록 설계했다. 덕분에 순서를 쉽게 바꾸고, ForEach로 리스트를 그릴 때도 정확한 순서대로 출력할 수 있게 되었다.

 

이 3번의 구조 변경은 단순히 모델 파일만 수정하는 것이 아니라, Firestore 읽기/쓰기 쿼리 및 규칙도 전부 수정해야 했고, ViewModel의 로직도 거의 다시 짜는 수준이었다. 그때마다 앱이 터지고, 데이터가 꼬이고, UI가 깨졌다. 하지만 그 과정을 통해 데이터 흐름에 대한 감각이 생기기 시작 했다.

세이브푸드 구현중

Firestore는 NoSQL이지만, 설계는 관계형처럼

Firestore는 분명히 문서(Document) 기반이고, 관계형이 아니라고 말하지만, 실제 앱에서 구현하는 대부분의 요구사항은 결국 관계형 데이터의 특성을 필요로 한다. 사용자와 보관함의 관계, 보관함과 식자재의 관계, 사용자 간 공유 관계, 모두 일종의 N:M 관계다.

 

이를 표현하려면 결국 구조적인 설계가 필요했고, 이럴 땐 오히려 관계형 데이터베이스처럼 생각하는게 도움 됐다. 예를 들어,   

    보관함은 여러 사용자에게 공유될 수 있다 (1:N)  

    사용자는 여러 보관함에 소속될 수 있다 (N:1)   

    보관함 내부에는 여러 식자재가 들어간다 (1:N)   

이 관계를 NoSQL 구조로 풀기 위해 중복을 허용하고, 필드를 명시하고, 때론 배열을 쓰고, 때론 서브컬렉션을 썼다. 이 구조는 정확하게 표현되지는 않지만, 앱에서 필요한 데이터를 가장 빠르게 가져올 수 있는 구조로 점점 다듬어졌다.

바이브코딩과 함께 구조 설계

ChatGPT와의 바이브코딩도 이 구조 설계에 큰 도움이 되었다. 예를 들어 "Firestore에서 N:M 관계를 표현 하려면 어떻게 해야 할까?" 라는 질문에 GPT는 일반적인 방향을 알려줬고, 실제로 어떻게 구현할지는 내가 직접 코드를 짜면서 경험적으로 익혀야 했다.

 

무엇보다 좋았던건, 내가 짠 구조가 문제가 있을 때 ChatGPT에게 구조를 던지고 피드백을 받는 식으로 대화를 이어갈 수 있었다는 점이다. 그 대화의 축적이 이 앱을 지금까지 끌고 온 동력이 되었다.

구조가 앱을 만든다

지금 와서 돌아보면, SaveFood 앱의 핵심은 구조였다. 이 구조 덕분에 공유 기능도, 알림 기능도, 구독 기능도 추가 할 수가 있었다. 그리고 이 구조는 기능이 늘어나도 쉽게 확장되도록 설계되어 있다.

 

데이터는 앱의 뼈대다. 그 뼈대를 어떻게 설계했는가에 따라 앱의 확장성과 유지보수성이 달라진다. 세이브푸드(SaveFood)의 구조는 지금도 조금씩 변하고 있지만, 그 중심에는 여전히 사용자와 보관함이라는 두 축이 있다. 그리고 이 두 축이 안정적으로 연결되어 있기에, 앱이 흔들리지 않고 잘 작동할 수 있다.

 

나는 지금도 이 구조를 다듬고 있고, 앞으로도 더 나은 방식이 있을 거라 생각한다. 하지만 지금 이 구조로 앱이 작동하고, 사용자들이 그 위에서 식자재를 등록하고 알림을 받는 다는 사실 하나만으로도, 이 구조는 충분히 성공했다고 말할 수 있을 것 같다.

노션 정리 내역

 

구조를 튼튼하게 지키기 위해 노션에도 각 폴더 및 파일별 역할에 대해서, Apple 개발자 관련 구성이나 Firebase 세팅 관련해서도 Task로 구분해서 관리를 하고 있다. 이렇게 세이브푸드(SaveFood)프로젝트는 오늘도 진행중 이다.