발행일

목업 데이터 49개에서 478개로 — 엑셀을 mockup 시드로 자동화한 기록

목업 데이터 49개에서 478개로 — 엑셀을 mockup 시드로 자동화한 기록

용어 등록 화면을 만들면서 mockup 데이터가 49개밖에 안 돼서, 페이지네이션이나 검색·필터 동작을 제대로 검증하기 어려웠습니다. 데이터를 늘리려고 직접 손으로 만드는 대신, 운영 측에서 받은 엑셀 파일을 mockup 시드로 변환하는 작은 스크립트를 만든 이야기입니다.


1. 시작 — UI 검증이 빈약했다

mockup 단계의 용어 데이터(static/data/terms_mock.json)가 49건이었습니다. 49건은:

  • 페이지네이션 한 페이지에 다 들어감 (페이지 이동 자체를 검증 못 함)
  • 검색해도 결과가 한 화면에 다 보임 (필터 동작이 안 보임)
  • 무한 스크롤 UI를 검증할 수 없음 (스크롤할 양이 안 됨)

그러니까 "용어 등록 화면 mockup"이 시각적으로는 그럴듯해도 "진짜 양이 들어왔을 때 어떻게 보이는지" 는 가늠이 안 됐습니다.


2. 해결 후보 비교

세 가지를 비교했어요.

옵션작업량품질회수 가치
① 손으로 49 → 200~300개 추가큼 (반나절~하루)낮음 (반복 패턴)다시 늘릴 때마다 반복
② Faker 같은 라이브러리로 랜덤 생성작음진짜 데이터처럼 안 보임매번 형태가 달라짐
③ 운영 엑셀을 mockup 시드로 변환중간진짜 데이터 형태 보존시드 스크립트 한 번이면 반복 사용

③번으로 갔습니다. 운영 측에서 metadata_new.xlsx라는 진짜 도메인 데이터를 들고 있었는데, 478행이 있었어요. 이걸 mockup JSON 형식으로 변환하면 양도 충분하고 형태도 자연스러웠습니다.


3. 스크립트 — scripts/seed_terms_from_xlsx.py

엑셀을 읽어 JSON으로 떨구는 단순한 변환 스크립트입니다. 핵심만 보면:

# scripts/seed_terms_from_xlsx.py
from openpyxl import load_workbook
from pathlib import Path
import json
import re

SRC = Path("seeds/metadata_new.xlsx")
DST = Path("static/data/terms_mock.json")

COLUMN_MAP = {
    "용어명": "name",
    "영문명": "en_name",
    "정의": "definition",
    "네임스페이스": "namespace_prefix",
    "타입": "term_type",
    "URI": "uri",
}

def slugify(s):
    s = re.sub(r"[^\w\-]+", "_", s, flags=re.UNICODE).strip("_")
    return s.lower()

def main():
    wb = load_workbook(SRC, data_only=True)
    ws = wb.active
    headers = [cell.value for cell in ws[1]]
    
    rows = []
    for i, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=1):
        if not any(row):
            continue
        record = {}
        for col_idx, header in enumerate(headers):
            key = COLUMN_MAP.get(header)
            if key is None:
                continue
            value = row[col_idx]
            if value is None:
                continue
            record[key] = str(value).strip()
        
        # 시드 식별자: mock-{n}-{slug}
        record["id"] = f"mock-{i:04d}-{slugify(record.get('name', ''))}"
        rows.append(record)
    
    DST.write_text(json.dumps(rows, ensure_ascii=False, indent=2))
    print(f"✓ wrote {len(rows)} records → {DST}")

if __name__ == "__main__":
    main()

엑셀 컬럼명을 JSON 키에 매핑하는 COLUMN_MAP 한 곳만 잘 만들어두면, 다음에 엑셀 포맷이 바뀌어도 여기만 손대면 됩니다.


4. 의도적인 결정들

4.1 ID에 슬러그 포함

mock-0001이 아니라 mock-0001-위치_좌표처럼 슬러그를 붙였습니다. 화면에서 디버깅할 때 "이 ID가 어떤 용어를 가리키는지" 가 한눈에 보여요. 진짜 DB가 들어오면 어차피 시퀀스 ID로 교체될 거라 큰 비용이 아닙니다.

4.2 빈 값을 키 자체로 누락시킴

엑셀 셀이 비어 있으면 그 키를 JSON record에 아예 안 넣었습니다. 화면 측 코드가 term.uri 같은 접근에서 "빈 문자열인지 누락인지" 를 구별할 수 있도록.

4.3 결정론적 출력

같은 엑셀을 두 번 돌리면 같은 JSON이 나오게 했어요. 매번 다른 순서로 들어가거나, 새 ID가 부여되면 git diff에 무의미한 변경이 잔뜩 잡힙니다. 정렬도, ID도 입력 순서대로.


5. 사용 흐름

# 엑셀이 갱신되면
python scripts/seed_terms_from_xlsx.py

# git diff 로 변경 확인
git diff static/data/terms_mock.json

# 필요하면 commit

엑셀 자체는 seeds/ 폴더에 보관. 진짜 데이터가 일부 포함될 수 있어서 seeds/.gitignore에 추가했고, 대신 metadata_new.example.xlsx 같은 샘플 파일을 두어 "이런 컬럼 구성을 기대한다" 를 알 수 있게 했습니다.


6. 결과

  • mockup 데이터가 49 → 478개로 증가
  • 페이지네이션 UI 검증 가능 (한 페이지 20개 × 24페이지)
  • 검색/필터 동작 시각 확인 가능
  • 새 데이터가 더 필요하면 엑셀에 행 추가 후 스크립트 한 줄 실행

누가 봐도 진짜 같은 데이터 라는 점이 mockup의 신뢰도를 크게 끌어올렸어요. Faker로 만들었으면 "용어1, 용어2..." 같은 더미 느낌이 났을 텐데, 운영 도메인의 단어들이 들어가니까 디자인 리뷰에서도 "실제로 이런 양과 형태가 들어왔을 때 화면이 견디는가" 를 평가할 수 있게 됐습니다.


7. 한계 — 지금 다시 보면

① 원본은 감췄는데 파생물은 커밋됩니다.

seeds/.gitignore에 넣은 건 진짜 데이터가 저장소에 들어가는 걸 막기 위해서였습니다. 그런데 5절의 흐름을 다시 보면 이렇습니다.

python scripts/seed_terms_from_xlsx.py      # 엑셀 → JSON
git diff static/data/terms_mock.json        # 그리고 이건 커밋한다

변환 결과물에는 그 진짜 데이터가 그대로 들어 있습니다. 엑셀만 안 올렸을 뿐, 운영 도메인의 용어·정의·URI는 JSON으로 저장소에 들어가요. 원본을 감추는 것과 내용을 감추는 건 다른 문제인데, 그때는 원본 파일만 생각했습니다.

목업의 신뢰도를 올린 바로 그 성질("누가 봐도 진짜 같은 데이터")이 그대로 위험이 되는 구조였어요. 지금 같으면 최소한 정의·URI 같은 상세 필드는 마스킹하거나, 용어명만 남기고 나머지는 생성했을 겁니다.

② 컬럼이 조용히 누락됩니다.

key = COLUMN_MAP.get(header)
if key is None:
    continue          # 매핑에 없는 헤더는 그냥 건너뜀

엑셀 헤더가 "정의"에서 "용어 정의"로 바뀌면 에러 없이 그 필드만 빠진 JSON이 나옵니다. 478건이 그대로 생성되니 성공한 것처럼 보이고요. 매핑되지 않은 헤더를 경고로 남기거나, 필수 키가 빠지면 실패하게 했어야 합니다. 이 패턴은 나중에 정의 필드 길이 문제에서 다시 만났어요 — 적재 경로는 조용히 성공하는 게 제일 위험합니다.

scripts/ 아래 단독 파일입니다.

관리 커맨드가 아니라 독립 스크립트라, 경로가 상수로 박혀 있고 Django 설정과도 무관합니다. 목업 단계에서는 그게 맞았지만, 이후 실제 적재가 필요해지면서 결국 관리 커맨드로 다시 짜게 됐어요.


8. 이후 — 이 스크립트가 간 곳

목업용으로 만든 이 변환기가 결국 실제 적재 파이프라인의 원형이 됐습니다. 엑셀을 정본으로 두고 코드가 그걸 읽어 들이는 구조는 그대로 살아남았고, 나중에는 2단 병합 헤더를 읽는 워크북 리더와 네임스페이스 → 데이터타입 → 클래스 → 속성 순서로 도는 4패스 로더로 발전했어요.

그때 속성 range를 FK 둘로 쪼개면서 데이터 마이그레이션 스크립트 없이 재적재만으로 과거 오적재를 정정할 수 있었던 것도, 이때 세운 "엑셀이 정본, DB는 적재 결과" 라는 전제 덕분이었습니다.

목업 데이터를 늘리려고 만든 반나절짜리 스크립트가 그 전제를 처음 세운 자리였던 셈이에요.


9. 교훈

  • 목업 데이터도 시드 스크립트로 만들어 두면 향후 작업이 단순해진다. 손으로 만들기 시작하면 늘 부족함.
  • 진짜 데이터의 형태가 목업 품질의 8할. 진짜 데이터를 한 번 시드로 가져올 수 있으면 그게 가장 좋다.
  • 결정론적 출력은 시드 스크립트의 기본기. 입력이 같으면 출력도 항상 같게.
  • 시드 데이터 파일은 가능하면 .gitignore에 — 진짜 데이터가 포함될 수 있는 파일은 저장소에 두지 않기.

작은 도구지만, 한 번 만들어두고 나니 "데이터 좀 더 늘려보자" 가 1분 작업이 됐어요. 도구 만드는 데 든 시간을 그 자체로 회수한 셈입니다.