Google Sheet to relational schema: 단계별 모델링 계획
Google Sheet를 관계형 스키마로 전환하는 과정을 단계별로 설명합니다: 반복 그룹 찾기, 키 선택, 관계 매핑, 이후 데이터 엉킴 방지.

스프레드시트가 데이터베이스가 될 때 왜 엉키는가
스프레드시트는 작은 목록을 관리하기에 탁월합니다. 열을 즉석에서 바꾸고, 아무 곳에나 메모를 추가하고, 눈으로 문제를 고칠 수 있습니다. 하지만 파일이 공동의 진실의 출처가 되면 그 자유는 금방 무너집니다.
데이터가 커지면 같은 문제들이 반복적으로 나타납니다. 고객이나 제품을 저장할 단일 장소가 없어 중복이 생깁니다. 두 행이 같은 항목(예: 전화번호)에 대해 서로 다른 값을 가질 때 모순이 생깁니다. 일부 열이 목록을 숨기거나("Tags", "Products", "Attendees") 형식을 섞어 놓으면("$1,200", "1200", "1.2k") 필터링과 리포팅이 고통스러워집니다.
Google Sheet에서 관계형 스키마로 옮기는 것은 안전을 위한 작업입니다. 데이터베이스는 더 명확한 구조를 강제해서 쿼리하고 검증하고 업데이트할 때 새로운 모순을 만들지 않게 해줍니다.
유용한 사고 모델: 한 행은 하나의 실제 대상을 나타내야 합니다. 한 행이 거래, 고객, 제품 목록을 동시에 나타내면 그 중 하나라도 나중에 업데이트하기가 고통스럽습니다.
간단한 테스트: 하나의 행이 같은 필드에 대해 두 개 이상의 값을 가져야 하나?
- 한 주문에 여러 제품이 있음
- 한 프로젝트에 여러 팀원이 있음
- 한 고객에 여러 주소가 있음
만약 답이 예라면, 이는 "넓은 행(wide row)" 문제가 아니라 "별도 테이블" 문제입니다. 깔끔하게 모델링하면 불안정한 수작업 대신 폼과 검증을 쌓아 관리할 수 있습니다.
시트가 실제로 무엇을 의미하는지 정의하기
스프레드시트는 정돈되어 보여도 사람마다 다르게 해석할 수 있습니다. Google Sheet를 관계형 스키마로 바꾸기 전에 시트가 무엇을 추적하는지 합의하세요.
열이 아니라 결과물(outcomes)부터 시작하세요. 데이터가 어떤 결정을 지원해야 하나요: 주간 매출 리포트, 연체 티켓 목록, 후속 할당 워크플로, 고객 통화 중 빠른 조회 등. 결정할 수 없는 필드는 보통 데이터베이스에 속하지 않습니다.
다음으로 헤더와 메모에 숨어 있는 명사들을 꺼내세요. 이는 보통 향후 테이블이 됩니다: customers, orders, products, invoices, tickets, agents, locations. 만약 열이 두 개의 명사를 섞고 있다면(예: "Customer + Company"), 하나의 장소에 여러 것을 저장하고 있는 것입니다.
정의에 초기에 합의하기
의미의 작은 차이가 나중에 큰 정리에 영향을 줍니다. 기본을 명확히 하세요:
- "주문(order)"의 범위는 무엇인가(견적, 결제된 구매, 혹은 둘 다)?
- "고객(customer)"은 누구를 뜻하나(개인, 회사, 혹은 둘 다)?
- 한 주문에 여러 제품이 포함될 수 있나?
- 하나의 이메일이 여러 고객에 속할 수 있나?
- "상태(status)"는 현재 상태를 뜻하나, 아니면 이력인가?
예: 시트가 각 행을 "Order"로 다루지만 "Products" 셀에 쉼표로 구분된 목록이 들어 있다면, 그 행이 체크아웃인지, 발송인지, 청구서인지 결정하세요. 선택에 따라 다른 스키마가 필요합니다.
원본 시트의 읽기 전용 복사본을 고정해 두세요. 새 테이블이 여전히 같은 질문에 답하는지 검증할 때 사용합니다.
구조가 보이도록 시트를 정리하기
Google Sheet를 관계형 스키마로 변환하기 전에, 시트를 데이터처럼 보이게 하세요. 데이터베이스는 일관된 행과 열을 필요로 합니다. 장식성 레이아웃은 모델링해야 할 패턴을 숨깁니다.
병합된 셀, 여러 헤더 행, 데이터 범위 안의 소계 같은 레이아웃 트릭을 제거하세요. 헤더는 한 행만 남기고 그다음은 레코드 행만 있게 하세요. 합계가 필요하면 별도의 요약 탭에 두어 실제 레코드와 섞이지 않게 하세요.
그다음 각 열의 형식을 일관되게 만드세요. 데이터베이스는 "1/2/24", "2024-02-01", "Feb 1"이 같은 날짜라는 걸 추측하지 못합니다. 전화번호, 통화, 이름도 마찬가지입니다. 하나의 형식을 골라 어디든 동일하게 사용하세요.
단기간 정리로 효과가 큰 작업들:
- 각 행이 하나의 대상을 나타내는지 확인(한 주문, 한 고객, 한 티켓)
- 빈 구분 행/열 삭제
- "N/A", "-", 빈 문자열을 하나의 규칙으로 통일
- 어느 열이 계산된 값이고 어느 열이 사람이 입력한 값인지 표시
마지막으로 한 셀에 여러 값이 들어있는 경우(예: "red, blue, green")를 표시하세요. 아직 스키마를 고치지는 말고, 나중에 별도 행이 될 것임을 표시만 해두세요.
반복 그룹과 목록을 숨기는 필드 식별하기
스프레드시트 데이터 모델링에서 가장 큰 경고 신호는 반복입니다. 시트는 종종 "여러 개의 것"을 하나의 행에 밀어 넣거나 한 셀에 여러 값을 담아 처리합니다. 빠르게는 괜찮아 보여도 필터링·보고·일관된 업데이트가 필요할 때 깨집니다.
보통 "별도 테이블이어야 함"을 의미하는 패턴
다음 형태를 찾아보세요:
Item 1,Item 2,Item 3또는Phone 1,Phone 2같은 번호가 붙은 열- "Home"과 "Work"처럼 복제된 주소 필드 블록
- 쉼표, 줄바꿈, "and"로 값을 합친 셀(예: "Mouse, Keyboard, Monitor")
- "Approved 2025-01-10" 또는 "Alex (Manager)"처럼 두 개념을 섞은 열
- 주문 행이 동시에 주문 품목 전체를 담으려는 경우처럼 두 수준을 한 행에 저장하는 경우
예: 판매 추적기가 Order ID, Customer, Product 1, Qty 1, Product 2, Qty 2처럼 구성되어 있다면 한계를 금방 맞습니다. 어떤 주문은 1개 품목, 어떤 주문은 8개 품목을 가질 수 있어 시트가 옆으로 늘어나거나 데이터가 손실됩니다. 관계형 모델에서는 "Orders"는 하나의 테이블이 되고 "Order Items"는 주문의 각 제품마다 한 행을 가지는 별도 테이블이 됩니다.
셀 안의 목록은 각 값을 별도의 레코드로 취급하세요. 셀에 "Email, SMS"가 들어있다면 채널을 깔끔하게 추적하기 위해 별도 테이블(또는 조인 테이블)이 필요합니다.
혼합된 열도 조용히 위험합니다. 각 필드가 하나의 명확한 사실만 저장하도록 일찍 분리하세요.
찾은 엔티티로 테이블 만들기
시트에서 실제 세상에서의 항목을 이름 붙일 수 있다면, 각 항목을 테이블로 전환하세요. 시트는 하나의 큰 그리드가 아니라 목적이 뚜렷한 여러 목록 세트가 됩니다.
한 행이 두 가지 다른 것에 대한 세부를 섞고 있다면 아마 두 개의 테이블이 필요합니다. 예: 판매 추적 행에 고객 정보(이름, 전화), 주문 정보(날짜, 상태), 제품 정보(SKU, 가격)가 섞여 있으면 고객은 주문이 바뀔 때마다 변하지 않고 제품은 특정 주문에 종속되지 않습니다. 분리하면 중복 수정과 값 불일치를 막을 수 있습니다.
최종 확정 전에 각 테이블의 목적을 한 문장으로 적어보세요. "그리고 또한(and also)"라는 표현 없이 설명할 수 없다면 보통 너무 광범위한 것입니다.
몇 가지 실용적 규칙:
- 같은 것을 설명하고 동일한 라이프사이클을 공유하는 속성은 함께 유지(예: 고객 이름과 고객 이메일)
- 여러 번 나타날 수 있는 항목은 별도 테이블로 이동(여러 주문 품목, 여러 주소)
- 셀이 목록을 포함하면(쉼표 구분 등) 별도 테이블
- 서로 다른 이유로 변경되는 필드 세트는 분리(주문 상태 vs 고객 연락처 정보)
그다음 열 이름을 명확하고 일관되게 정하세요. "Info"나 "Details" 같은 모호한 레이블은 피합니다.
시간이 지나도 안정적인 키 선택하기
각 테이블에 대한 기본 키를 빨리 선택하세요. 좋은 키는 지루합니다: 절대 바뀌지 않고, 항상 존재하며, 하나의 행만 식별합니다.
자연 키(실세계 값)는 정말로 안정적일 때만 작동합니다. SKU는 영구적이도록 설계된 경우 좋은 자연 키가 될 수 있습니다. 이메일은 안정적일 것 같지만 사람들이 바꾸거나 중복 계정을 만들 수 있습니다. 이름, 전화번호, 주소는 변경될 수 있고 고유성을 보장하지 않습니다.
안전한 기본값은 자동 생성 ID(예: customer_id, order_id)입니다. 자연 식별자는 일반 필드로 유지하고 비즈니스 규칙이 맞을 때 유니크 규칙을 추가하세요. 이메일이 바뀌어도 customer_id는 그대로라 관련 주문이 여전히 올바른 고객을 가리킵니다.
간단한 키 규칙:
- 실세계 식별자가 바뀔 가능성이 있거나 누락되기 쉬우면 자동 ID 사용
- 영구성이 보장된 실세계 식별자만 자연 키로 사용(SKU 등)
- 중복이 잘못이라면 그때만 필드를 유니크로 표시
- NULL은 "알 수 없음"이 유효한 상태일 때만 허용
- "유니크"의 범위(테이블 전체, 회사별, 기간별)를 문서화
예: Contacts 테이블에서는 기본키로 contact_id를 사용하세요. email을 유니크로 할지 여부는 한 연락처당 하나의 이메일이라는 규칙이 확실할 때만 적용하세요. phone은 빈 값 허용 등 현실을 반영하세요.
관계 맵핑은 추측하지 말고 하라
가장 큰 실수는 관계를 추측해서 생깁니다. 간단한 규칙: 한 행이 많은 것을 "소유"하면 그건 일대다입니다. 외래키는 "많은" 쪽에 둡니다.
예: 한 고객이 여러 주문을 가질 수 있다면 Orders 테이블에 customer_id를 저장하세요. 고객 안에 쉼표로 구분된 주문 번호 목록을 두면 중복과 누락이 금세 생깁니다.
다대다는 스프레드시트의 흔한 함정입니다. 한 주문이 여러 제품을 포함하고 한 제품이 여러 주문에 나타날 수 있다면 조인 테이블(라인 아이템)을 만들어야 합니다. 보통 order_id, product_id와 수량, 구매 당시 가격을 포함합니다.
일대일 관계는 드뭅니다. 부가 데이터가 선택적이거나 개인정보·성능 때문에 분리하는 경우에 적절합니다(예: User와 UserProfile). 한 탭이 있어서 무작정 나눴다면 경고 신호일 수 있습니다.
이력(history)은 별도의 구조가 필요합니다. 값이 시간에 따라 변할 수 있다면(상태, 가격, 주소) 하나의 열을 덮어쓰지 마세요. 변경을 행으로 저장해 "그 날짜에 무엇이었나?"를 답할 수 있게 하세요.
모순을 막을 만큼만 정규화하기
간단한 규칙: 하나의 사실은 한 곳에 저장하세요. 고객 전화번호가 다섯 행에 나타나면 누군가 네 개만 업데이트하고 다섯 번째를 놓칠 것입니다.
정규화 실무 설명:
1NF, 2NF, 3NF를 실제적으로 보기
첫 번째 정규형(1NF)은 각 셀이 하나의 값을 갖는다는 뜻입니다. "red, blue, green"이나 "SKU1|SKU2|SKU3" 같은 숨겨진 목록이 있으면 관련 테이블의 행으로 쪼개세요.
두 번째 정규형(2NF)은 라인 아이템에서 주로 나타납니다. 예: OrderItems의 키가 (OrderID, ProductID)라면 CustomerName 같은 필드는 거기에 있으면 안 됩니다. 그 값은 주문에 의존하기 때문입니다.
세 번째 정규형(3NF)은 비키 필드가 다른 비키 필드에 의존하면 안 된다는 뜻입니다. 예: ZipCode로 City가 결정된다면 둘을 함께 저장하면 불일치가 생깁니다.
빠른 자가 점검:
- 같은 값을 두 곳 이상 편집해야 하는가?
- 하나의 변경이 다른 여러 행을 업데이트하게 만드는가?
- ID로부터 유도 가능한 라벨을 저장하고 있는가?
- 합계가 그것을 만들어내는 원시 행 옆에 저장되어 있는가?
비정규화가 괜찮은 경우
리드 중심 리포팅을 위해 성능 목적으로 비정규화할 수 있지만 안전하게 하세요: 리포트 테이블은 재생성 가능한 복사본으로 취급하세요. 정규화된 테이블을 진실의 출처로 유지합니다.
합계·잔액·상태 같은 파생값은 명확한 재계산 규칙이 없다면 중복 저장하지 마세요. 실거래를 저장하고 쿼리에서 합계를 계산하며, 성능이 필요할 때만 캐시하세요.
나중에 정리해야 할 흔한 함정
대부분의 "시트에서는 괜찮았음" 문제는 도구가 아니라 의미에서 옵니다. 목표는 모든 행이 매번 하나의 명확한 사실을 동일하게 말하게 하는 것입니다.
흔한 함정:
- 이름을 ID로 사용: "John Smith"는 고유 식별자가 아니고 변경됩니다. 생성된 ID를 사용하고 표시 이름은 라벨로 처리하세요.
- 목록을 하나의 셀에 포장: 간단해 보이지만 검색·검증·보고에 문제를 만듭니다. 목록은 관련 테이블로 옮기세요.
- 현재 상태와 이력을 섞음: 하나의 Status 열로 최신 상태와 변경 이력을 모두 표현하려 하지 마세요. 시점이 중요하면 이벤트로 저장하세요.
- 하나의 테이블에 여러 의미를 넣음: 고객·공급업체·직원을 모두 포함한 Contacts 시트는 일부 행에만 적용되는 필드를 만들기 쉽습니다. 역할별로 분리하거나 공통 Person 테이블에 역할별 테이블을 추가하세요.
- 필수 vs 선택 필드 무시: 핵심 필드가 비어 있으면 조인이 불가능한 행이 생깁니다. 무엇이 필수인지 미리 정하세요.
예: Orders 테이블에 Item 1, Item 2, Item 3 같은 열이 있다면 반복 그룹을 보고 있는 것입니다. Orders 테이블과 OrderItems 테이블을 계획하세요.
스키마 확정 전에 빠른 체크리스트
스키마를 잠그기 전 마지막 점검을 하세요. 대부분의 데이터베이스 고통은 초기에 무해해 보였던 작은 지름길에서 옵니다.
각 테이블이 하나의 간단한 질문에 답하는가? "Customers"는 고객만 의미해야 합니다. 한 문장으로 설명할 수 없다면 혼합되어 있는 것입니다.
최종 점검 항목:
- 이름이 바뀌어도 각 행을 고유하게 식별하는 열(또는 열 집합)을 가리킬 수 있는가?
- 어떤 셀에 둘 이상의 값이 들어 있는가(쉼표로 구분된 태그, 여러 이메일, Item1/Item2 열)? 있으면 자식 테이블로 분리
- 각 관계가 의도된 외래키로 저장되어 있는가? 다대다면 조인 테이블이 있는가?
- 중요한 필드에 규칙(필수, 유니크, 형식 검증)이 있는가?
- 하나의 사실(고객 주소, 제품 가격, 직원 역할)을 한 곳에서만 업데이트할 수 있는가?
현실 검증: 누군가 약간 다르게 철자해 같은 고객을 두 번 입력할 수 있다면, 더 나은 키나 유니크 규칙을 추가하세요.
예: 판매 추적 시트를 깔끔한 테이블로 바꾸기
딜별로 한 행이 있는 판매 추적기를 생각해보세요. 열은 Customer Name, Customer Email, Deal Amount, Stage, Close Date, Products(쉼표로 구분된 목록), Notes(한 셀에 여러 메모가 들어감) 같은 구성입니다.
그 한 행은 두 가지 반복 그룹을 숨깁니다: 제품(한 딜에 여러 제품 가능)과 노트(한 딜에 여러 노트 가능). 셀 안의 목록 때문에 변환이 흔히 잘못되는데, 목록은 쿼리하기 어렵고 모순이 생기기 쉽습니다.
작업 방식을 반영한 깔끔한 "후" 모델:
- Customers (CustomerId, Name, Email)
- Deals (DealId, CustomerId, Amount, Stage, CloseDate)
- Products (ProductId, Name, SKU)
- DealProducts (DealId, ProductId, Quantity, UnitPrice)
- DealNotes (NoteId, DealId, NoteText, CreatedAt)
CustomerId, DealId, ProductId는 안정적인 식별자입니다. DealProducts는 다대다 관계를 해결합니다: 한 딜은 여러 제품을 포함할 수 있고 한 제품은 여러 딜에 등장할 수 있습니다. DealNotes는 노트를 분리해 "Note 1, Note 2, Note 3" 같은 열이 생기지 않게 합니다.
모델링 전에는 "제품별 수익" 같은 리포트가 문자열을 분할하고 사람들이 이름을 일관되게 입력했길 바라는 작업이 됩니다. 모델링 후에는 DealProducts, Deals, Products를 조인하는 간단한 쿼리로 해결됩니다.
다음 단계: 스키마를 작동하는 앱으로 옮기기
스키마가 종이에 맞아 보이면 실제 데이터베이스에 옮겨 시험해보세요. 한 번에 모두 가져오지 마세요. 먼저 작은 배치를 로드하고 깨지는 부분을 고친 뒤 반복하세요.
위험을 줄이는 실용적 순서:
- 테이블과 관계 생성
- 50~200행을 가져와 합계를 검증하고 레코드를 샘플 점검
- 매핑 문제(잘못된 열, 누락된 ID, 중복)를 수정하고 재가져오기
- 안정되면 나머지 데이터를 로드
초기에 검증 규칙을 추가해 지저분한 시트 습관이 돌아오지 못하게 하세요. 필수 필드를 강제하고, 허용 값 목록(예: 상태)을 제한하고, 형식(날짜, 이메일)을 검증하며 외래키로 존재하지 않는 고객 주문을 만들지 못하게 하세요.
그다음 업데이트는 시트가 아니라 폼과 워크플로에서 이루어지게 하세요. 사람들이 단순한 폼과 명확한 절차로 작업하면 데이터 보호가 훨씬 쉬워집니다.
코드 없이 스키마를 실제 내부 도구로 바꾸고 싶다면 AppMaster (appmaster.io)가 도움이 됩니다: 테이블과 관계를 시각적으로 모델링한 뒤 동일 모델에서 프로덕션용 백엔드, 웹 앱, 네이티브 모바일 앱을 생성할 수 있습니다.
자주 묻는 질문
시트가 공유되는 진실의 출처로 사용되며 중복, 모순된 값, 번거로운 보고가 자주 발생할 때 시작하세요. 쉼표로 구분된 목록, Item 1/Item 2 같은 열, 계속되는 복사/붙여넣기 수정으로 시간을 낭비하고 있다면 관계형 스키마로 전환하면 빠르게 비용을 줄일 수 있습니다.
한 행이 같은 필드에 대해 여러 값을 가져야 하면 반복 그룹이 있는 것입니다. 예: 하나의 주문에 여러 제품, 한 고객에 여러 주소, 한 이벤트에 여러 참석자가 있는 경우. 이런 항목들은 자식 테이블(또는 조인 테이블)로 만들어야 하며, 열을 늘리거나 셀 내 목록으로 해결하면 안 됩니다.
원본 시트의 읽기 전용 복사본을 저장한 뒤, 병합된 셀, 여러 헤더 행, 데이터 범위 안의 소계 행을 제거하세요. 각 열의 형식을 통일(하나의 날짜 형식, 하나의 통화 형식, 공백 표현 방식 통일)하면 실제 구조가 보이기 시작합니다.
기본적으로 각 테이블에 자동 생성된 ID를 기본키로 사용하는 걸 권합니다. 이메일이나 이름은 사람들이 바꾸거나 중복을 만들기 쉬우므로 안정적인 식별자로 보장되지 않습니다. 실제 식별자(예: SKU)는 일반 필드로 보관하고, 비즈니스 규칙상 중복이 틀리면 유니크 제약을 추가하세요.
소유관계로 매핑하세요: 한 고객이 여러 주문을 가질 수 있다면 Orders 테이블에 customer_id를 둡니다. 주문과 제품이 서로 많은 관계라면 OrderItems 같은 조인 테이블을 만들어 order_id, product_id와 수량·구매 시 가격을 저장하세요.
요지는 한 사실을 한 곳에 저장하는 것입니다. 같은 고객 전화번호가 여러 행에 흩어져 있으면 누군가 하나만 업데이트하고 다른 건은 놓치기 쉽습니다. 완벽한 정규화가 아니더라도, 중복을 제거해 모순을 예방하세요.
쉼표로 구분된 목록은 각각 별도의 행으로 분리하세요. 예: Email, SMS처럼 셀에 여러 값이 있으면 관련 테이블(또는 조인 테이블)에 각각의 선택값을 한 레코드로 저장해야 필터링과 검증이 쉬워집니다.
현재 상태와 변경 이력을 분리하세요. 현재 상태 필드를 유지하되, 상태 변경에 시점이 중요하면 타임스탬프가 있는 이벤트/히스토리 테이블에 변경을 기록하세요. 이렇게 하면 “지난달 상태는 어땠나?” 같은 질문에 정확히 답할 수 있습니다.
작은 배치(약 50–200행)를 먼저 가져와 합계와 레코드를 대조해 검증하세요. 매핑 오류, 누락된 ID, 중복을 수정하고 재가져오기합니다. 프로세스가 반복 가능하고 예측 가능해졌을 때 전체를 로드하세요.
코드 없이 스키마를 작동하는 앱으로 만들고 싶다면 노코드/로우코드 도구가 도움됩니다. AppMaster (appmaster.io) 같은 도구는 테이블과 관계를 시각적으로 모델링하고 동일 모델에서 프로덕션용 백엔드, 웹 앱, 네이티브 모바일 앱을 생성할 수 있게 해줍니다.


