2025년 8월 20일·6분 읽기

Kotlin + SQLite를 위한 오프라인 우선 폼 충돌 해결

오프라인 우선 폼 충돌 해결 방법: 명확한 병합 규칙, 간단한 Kotlin + SQLite 동기화 흐름, 편집 충돌에 대한 실용적인 UX 패턴을 배워보세요.

Kotlin + SQLite를 위한 오프라인 우선 폼 충돌 해결

두 사람이 오프라인에서 동시에 편집하면 실제로 무슨 일이 일어날까?

오프라인 우선(offline-first) 폼은 네트워크가 느리거나 없을 때도 사용자가 데이터를 보고 편집할 수 있게 합니다. 서버를 기다리는 대신 앱은 변경 내용을 먼저 로컬 SQLite 데이터베이스에 쓰고, 나중에 동기화합니다.

이 방식은 즉각적으로 느껴지지만 단순한 현실을 만듭니다: 두 기기가 서로 모르는 상태에서 같은 레코드를 변경할 수 있습니다.

일반적인 충돌 사례는 이렇습니다. 현장 기술자가 지하에서 신호 없이 태블릿으로 작업 지시서를 열어 status를 "Done"으로 바꾸고 메모를 추가합니다. 동시에 감독자는 다른 전화기로 같은 작업 지시서를 수정해 담당자를 바꾸고 기한을 변경합니다. 둘 다 저장합니다. 둘 다 로컬에서는 성공합니다. 잘못한 사람은 없습니다.

동기화가 일어나면 서버는 어떤 레코드를 "진짜"로 볼지 결정해야 합니다. 충돌을 명시적으로 처리하지 않으면 보통 다음과 같은 결과가 생깁니다:

  • 마지막 쓰기 우선(LWW): 나중에 동기화된 것이 이전 변경을 덮어써서 누군가의 데이터가 사라집니다.
  • 강제 실패: 동기화가 한 업데이트를 거부하고 앱은 도움이 되지 않는 오류를 보여줍니다.
  • 중복 레코드: 덮어쓰지 않으려고 시스템이 두 번째 복사본을 만들고 보고서가 복잡해집니다.
  • 조용한 병합: 시스템이 변경을 합치지만 사용자 기대와 다르게 필드들이 섞입니다.

충돌은 버그가 아닙니다. 사람들에게 라이브 연결 없이 작업하게 허용하면 예측 가능한 결과로 충돌이 발생하는 것이 전체 포인트입니다.

목표는 데이터 보호와 앱의 사용성 유지입니다. 보통은 명확한 병합 규칙(종종 필드 별)과 정말 필요할 때만 사용자 흐름을 차단하는 UX를 뜻합니다. 두 편집이 서로 다른 필드를 건드리면 대개 조용히 병합할 수 있습니다. 두 사람이 같은 필드를 서로 다르게 바꿨다면 앱은 그 사실을 보여주고 누군가가 올바른 결과를 선택하도록 도와야 합니다.

데이터에 맞는 충돌 전략을 선택하세요

충돌은 기술 문제가 아니라 먼저 제품 결정입니다: 두 사람이 동기화 전에 같은 레코드를 변경했을 때 무엇이 "정답"인지 규정하는 문제입니다.

대부분의 오프라인 앱은 세 가지 전략으로 커버됩니다:

  • 마지막 쓰기 우선(LWW): 최신 편집을 받아들이고 이전 것을 덮어씁니다.
  • 수동 검토: 멈추고 사람이 무엇을 유지할지 선택하게 합니다.
  • 필드 수준 병합: 필드별로 변경을 결합하고 같은 필드를 두 사람이 건드릴 때만 묻습니다.

LWW는 속도가 정확성보다 중요하고 틀려도 피해가 적을 때 괜찮습니다. 내부 메모, 중요하지 않은 태그, 나중에 다시 편집 가능한 드래프트 상태 등이 그런 예입니다.

수동 검토는 법적 문구, 규정 확인, 급여/청구 금액, 은행 정보, 약물 지침처럼 앱이 추측하면 안 되는 고영향 필드에 안전한 선택입니다.

필드 수준 병합은 보통 폼의 기본값입니다. 서로 다른 역할이 다른 부분을 업데이트할 때 유용합니다. 예를 들어 고객 지원은 주소를, 영업은 갱신일을 업데이트합니다. 필드 별 병합은 두 변경을 모두 유지하되, 둘 다 같은 필드를 수정하면 해당 필드만 결정을 요구합니다.

구현 전에 비즈니스에서 무엇이 "정확"한지 적어두세요. 빠른 체크리스트:

  • 어떤 필드는 항상 최신 실세계 값을 반영해야 하는가(예: 현재 상태)?
  • 어떤 필드는 역사적이라 절대 덮어써선 안 되는가(예: 제출 시각)?
  • 누가 각 필드를 변경할 수 있는가(역할, 소유권, 승인)?
  • 값이 다를 때 진실의 출처는 무엇인가(디바이스, 서버, 관리자 승인)?
  • 잘못 선택하면 무슨 일이 발생하는가(사소한 불편 vs 재정/법적 영향)?

규칙이 명확해지면 동기화 코드는 그것을 강제하는 하나의 역할만 가지면 됩니다.

화면 단위가 아니라 필드별로 병합 규칙을 정의하세요

충돌이 발생하면 보통 폼 전체가 균일하게 영향을 받지 않습니다. 한 사용자는 전화번호를 바꾸고 다른 사용자는 메모를 추가할 수 있습니다. 레코드를 전부 혹은 전무로 처리하면 잘된 작업까지 다시 하게 만듭니다.

필드 수준 병합은 각 필드의 동작을 알 수 있어 더 예측 가능합니다. UX는 차분하고 빠르게 유지됩니다.

시작하는 간단한 방법은 필드를 "대체로 자동 병합해도 안전한 항목"과 "대체로 자동 병합하면 위험한 항목"으로 나누는 것입니다.

자동 병합해도 대체로 안전한 항목: 메모와 내부 코멘트, 태그, 첨부파일(대개 합집합), 그리고 last contacted처럼 최신 값을 유지하는 타임스탬프.

자동 병합이 대체로 위험한 항목: 상태/스테이트(status), 담당자/소유자(assignee), 합계/가격, 승인 플래그, 재고 수량.

각 필드에 우선 규칙을 고르세요. 일반적인 선택지는 서버 우선, 클라이언트 우선, 역할 우선(예: 매니저가 에이전트를 덮어씀), 또는 최신 서버 버전 같은 결정론적 타이브레이커입니다.

핵심 질문은 양쪽이 같은 필드를 변경했을 때 무슨 일이 일어나는가입니다. 각 필드에 대해 다음 중 하나를 선택하세요:

  • 명확한 규칙으로 자동 병합(예: 태그는 합집합)
  • 두 값을 모두 유지(예: 메모는 작성자와 시간과 함께 이어붙임)
  • 검토로 플래그 표시(예: statusassignee는 선택이 필요)

예시: 두 명의 지원 담당이 같은 티켓을 오프라인에서 편집했습니다. 담당자 A는 status를 "Open"에서 "Pending"으로 바꿉니다. 담당자 B는 notes를 수정하고 "refund" 태그를 추가합니다. 동기화 시 notestags는 안전하게 병합할 수 있지만 status는 조용히 병합하면 안 됩니다. status만 프롬프트하고 나머지는 이미 병합된 상태로 둡니다.

나중의 논쟁을 막으려면 각 규칙을 필드별로 한 문장으로 문서화하세요:

  • "notes: 둘 다 유지, 최신 항목을 마지막에 덧붙이며 작성자와 시간을 포함한다."
  • "tags: 합집합, 양쪽에서 모두 삭제한 경우에만 제거한다."
  • "status: 양쪽에서 변경되었으면 사용자 선택이 필요하다."
  • "assignee: 매니저가 있으면 매니저가 우선, 아니면 서버 우선."

그 한 문장이 Kotlin 코드, SQLite 쿼리, 충돌 UI의 진실의 원천이 됩니다.

데이터 모델 기초: SQLite의 버전과 감사 필드

충돌을 예측 가능하게 만들고 싶다면 동기화되는 각 테이블에 작은 메타데이터 컬럼 세트를 추가하세요. 이것들이 없으면 현재 보고 있는 것이 최신 편집인지, 오래된 복사본인지, 혹은 병합이 필요한 두 편집인지 알 수 없습니다.

서버 동기 레코드마다 실용적인 최소 항목:

  • id(안정적인 기본키): 절대 재사용하지 마세요.
  • version(정수): 서버에서 성공적으로 쓸 때마다 증가합니다.
  • updated_at(타임스탬프): 레코드가 마지막으로 변경된 시각.
  • updated_by(텍스트 또는 사용자 id): 마지막 변경자.

디바이스에서는 서버에서 확인받지 않은 변경을 추적할 로컬 전용 필드를 추가하세요:

  • dirty(0/1): 로컬 변경 존재 여부.
  • pending_sync(0/1): 업로드 대기 큐에 있음.
  • last_synced_at(타임스탬프): 이 행이 마지막으로 서버와 일치한 시각.
  • sync_error(텍스트, 선택적): UI에 표시할 마지막 실패 이유.

낙관적 동시성(optimistic concurrency)은 조용한 덮어쓰기를 방지하는 가장 간단한 규칙입니다: 모든 업데이트에 당신이 편집한다고 생각한 expected_version을 포함하세요. 서버 레코드가 여전히 그 버전이면 업데이트를 수락하고 서버는 새 버전을 반환합니다. 그렇지 않으면 충돌입니다.

예: 사용자 A와 B가 둘 다 version = 7을 받았습니다. A가 먼저 동기화하면 서버는 8로 올립니다. B가 expected_version = 7으로 동기화하려 하면 서버는 충돌로 거부하고 B의 앱은 병합 작업을 하게 됩니다.

좋은 충돌 화면을 위해 공유 출발점을 저장하세요: 사용자가 원래 편집한 기준입니다. 두 가지 일반적 접근:

  • 마지막으로 동기화된 레코드의 스냅샷을 저장(하나의 JSON 컬럼 또는 병렬 테이블).
  • 변경 로그를 저장(편집 당 하나의 행 또는 필드별 행).

스냅샷이 더 간단하고 폼에는 충분한 경우가 많습니다. 변경 로그는 더 무겁지만 필드별로 정확히 무엇이 바뀌었는지 설명할 수 있습니다.

어느 방식이든 UI는 각 필드에 대해 세 가지 값을 보여줄 수 있어야 합니다: 사용자의 편집, 서버의 현재 값, 그리고 공유된 출발점.

레코드 스냅샷 vs 변경 로그: 한 가지 방식 선택하기

Build your offline-first app
Build an offline-first form flow with clear sync states and conflict handling in one project.
Try AppMaster

오프라인 우선 폼을 동기화할 때 전체 레코드를 업로드(스냅샷)할지 또는 연산 목록(변경 로그)을 업로드할지 선택할 수 있습니다. 둘 다 Kotlin과 SQLite에서 작동하지만, 두 사람이 같은 레코드를 편집했을 때 동작이 달라집니다.

옵션 A: 전체 레코드 스냅샷

스냅샷 방식에서는 저장할 때마다 최신 전체 상태(모든 필드)를 기록합니다. 동기화 시 레코드와 버전 번호를 보냅니다. 서버가 버전이 오래되었다고 판단하면 충돌이 생깁니다.

이 방식은 구현이 간단하고 읽기 빠르지만 종종 불필요하게 큰 충돌을 만듭니다. 사용자 A가 전화번호를 수정하고 사용자 B가 주소를 수정하면 스냅샷 방식은 필드가 겹치지 않아도 큰 충돌로 처리할 수 있습니다.

옵션 B: 변경 로그(연산)

변경 로그 방식에서는 전체 레코드가 아닌 무엇이 변경되었는지를 저장합니다. 각 로컬 편집은 재생 가능한 연산이 됩니다.

자주 병합하기 쉬운 연산 예:

  • 필드 값 설정(예: email을 새 값으로 설정)
  • 메모 추가(새 메모 항목을 추가)
  • 태그 추가(집합에 태그 하나 추가)
  • 태그 제거(집합에서 태그 하나 제거)
  • 체크박스 완료 표시(타임스탬프와 함께 isDone을 true로 설정)

연산 로그는 많은 경우 충돌을 줄입니다. 메모 덧붙이기는 보통 충돌을 일으키지 않고 태그 추가/제거는 집합 연산으로 합칠 수 있습니다. 단일 값 필드는 여전히 두 다른 편집이 경쟁할 때 필드별 규칙이 필요합니다.

대가는 복잡성입니다: 안정적 연산 ID, 순서(로컬 시퀀스와 서버 시간), 교환 불가능(commute하지 않는) 연산에 대한 규칙 등이 필요합니다.

정리: 성공적 동기화 후 압축

변경 로그는 계속 늘어나므로 축소(compaction) 계획을 세우세요.

일반적인 방법은 레코드별 압축입니다: 알려진 서버 버전까지의 모든 연산이 확인되면 그들을 새로운 스냅샷으로 접어 넣고 오래된 연산을 삭제합니다. 되돌리기, 감사 또는 디버깅을 위해 짧은 꼬리(tail)만 남겨두세요.

Kotlin + SQLite를 위한 단계별 동기화 흐름

Get real source code output
Export real source code when you need full control over Kotlin, SwiftUI, Go, and Vue3.
Build now

좋은 동기화 전략은 보낸 것과 받은 것을 엄격하게 관리하는 것입니다. 목표는 단순합니다: 최신 데이터를 실수로 덮어쓰지 말고 안전하게 병합할 수 없을 때는 충돌을 명확히 보여주기.

실용적인 흐름:

  1. 모든 편집을 먼저 SQLite에 쓴다. 로컬 트랜잭션으로 변경을 저장하고 레코드를 pending_sync = 1로 표시하세요. local_updated_at과 마지막으로 알았던 server_version을 저장합니다.

  2. 전체 레코드가 아닌 패치(patch)를 보낸다. 연결이 복구되면 변경된 필드만, 그리고 expected_version과 함께 레코드 id를 전송하세요.

  3. 서버가 버전 불일치를 거부하도록 하라. 서버의 현재 버전이 expected_version과 다르면 서버는 충돌 페이로드(서버 레코드, 제안된 변경, 어떤 필드가 다른지)를 반환합니다. 버전이 일치하면 패치를 적용하고 버전을 증가시키며 업데이트된 레코드를 반환합니다.

  4. 먼저 자동 병합을 적용한 다음 사용자에게 묻는다. 필드 수준 병합 규칙을 실행하세요. 메모 같은 안전한 필드와 상태, 가격, 담당자 같은 민감한 필드는 다르게 처리합니다.

  5. 최종 결과를 커밋하고 대기 플래그를 정리한다. 자동 병합이든 수동 해결이든 최종 레코드를 SQLite에 다시 쓰고 server_version을 업데이트하며 pending_sync = 0으로 설정하고 이후에 어떤 일이 있었는지 설명할 충분한 감사 데이터를 기록합니다.

예: 두 명의 영업 사원이 같은 주문을 오프라인에서 편집했습니다. A는 배송 날짜를 바꾸고 B는 고객 전화번호를 바꿉니다. 패치 방식이면 서버는 두 변경을 깔끔하게 받아들일 수 있습니다. 둘 다 배송 날짜를 바꿨다면 전체 재입력을 강제하는 대신 하나의 명확한 결정을 사용자에게 보여줍니다.

UI 약속을 일관되게 유지하세요: "Saved"는 로컬에 저장된 것을 의미합니다. "Synced"는 별개의 명시적 상태여야 합니다.

폼 충돌 해결을 위한 UX 패턴

충돌은 예외여야 하며, 정상 흐름이 되어선 안 됩니다. 안전한 항목은 자동 병합하고 정말 결정이 필요할 때만 사용자에게 물어보세요.

안전한 기본값으로 충돌을 드물게 만들기

두 사람이 다른 필드를 편집하면 모달을 보여주지 말고 자동 병합하세요. 두 변경을 모두 유지하고 작은 "동기화 후 업데이트됨" 메시지를 보여줍니다.

프롬프트는 같은 필드가 변경되었거나 한 변경이 다른 필드에 의존하는 경우(예: 상태와 상태 사유)처럼 진짜 충돌일 때만 예약하세요.

물어봐야 한다면 빠르게 끝내게 하라

충돌 화면은 두 가지 질문에 답해야 합니다: 무엇이 바뀌었는가, 그리고 무엇이 저장될 것인가. 값을 나란히 비교해서 보여주십시오: "Your edit", "Their edit", 그리고 "Saved result". 충돌 필드가 두 개뿐이라면 전체 폼을 보여주지 말고 바로 그 필드로 점프시키고 나머지는 읽기 전용으로 유지하세요.

동작은 실제로 사람들이 필요로 하는 것만으로 제한하세요:

  • 내 것 유지
  • 그쪽 것 유지
  • 최종값 편집
  • 필드별 검토(필요할 때만)

부분 병합에서는 UX가 복잡해집니다. 충돌하는 필드만 강조하고 출처를 명확히 표시하세요("Yours"와 "Theirs"). 안전한 옵션을 미리 선택해 사용자가 확인하고 넘어갈 수 있게 하세요.

사용자가 갇힌 느낌을 받지 않도록 상태를 명확히 알리세요. 예: "나가면 로컬에 버전이 유지되고 나중에 다시 동기화합니다" 또는 "이 레코드는 당신이 선택할 때까지 Needs review 상태로 남습니다". 목록에서 그 상태가 보이게 해서 충돌이 묻히지 않도록 하세요.

AppMaster로 이 흐름을 만든다면 같은 UX 접근을 취하세요: 안전한 필드를 먼저 자동 병합하고 특정 필드가 충돌할 때만 집중된 검토 단계를 보여줍니다.

까다로운 경우: 삭제, 중복, 그리고 '사라진' 레코드

Prevent silent overwrites
Set up patch-style updates and version checks so newer data can’t be overwritten silently.
Start building

대부분의 무작위 같은 동기화 문제는 세 상황에서 옵니다: 누군가가 삭제하는 동안 다른 사람이 편집함, 두 기기가 동일한 실제 항목을 오프라인에서 생성함, 또는 레코드가 사라졌다가 다시 나타남. LWW는 이런 경우에 사람들을 놀라게 하는 경우가 많아 명시적 규칙이 필요합니다.

삭제 vs 편집: 누가 이기는가?

삭제가 편집보다 강한지 결정하세요. 많은 비즈니스 앱에서는 삭제가 우선입니다. 사용자는 한 번 제거된 레코드는 계속 없어지는 것을 기대합니다.

실용적인 규칙 세트:

  • 어떤 기기에서든 레코드가 삭제되면, 그 레코드는 나중에 편집이 있어도 모든 곳에서 삭제로 간주합니다.
  • 삭제를 되돌릴 수 있어야 한다면 하드 삭제 대신 archived 상태로 변환하세요.
  • 삭제된 레코드에 편집이 도착하면 감사 기록으로 편집을 보관하되 레코드를 복원하지는 마세요.

오프라인 생성 충돌과 중복 초안

오프라인 폼은 서버가 최종 ID를 할당하기 전에 임시 ID(예: UUID)를 만들곤 합니다. 중복은 사용자가 동일한 실제 항목(같은 영수증, 티켓, 아이템)을 두 번 생성할 때 발생합니다.

자연 키(영수증 번호, 바코드, 이메일+날짜)가 있다면 이를 사용해 충돌을 감지하세요. 없다면 중복은 발생할 수 있음을 인정하고 나중에 간단한 병합 옵션을 제공하세요.

구현 팁: SQLite에 local_idserver_id를 모두 저장하세요. 서버가 응답하면 매핑을 쓰고, 큐에 남아 로컬 ID를 참조하는 변경이 없는 것이 확인될 때까지 그 매핑을 보관하세요.

동기화 후 '부활(resurrection)' 방지

부활은 기기 A가 레코드를 삭제했지만 기기 B가 오프라인 상태여서 오래된 복사를 업로드해 레코드를 다시 만드는 경우 발생합니다.

해결책은 tombstone입니다. 행을 즉시 지우는 대신 deleted_at(대개 deleted_by, delete_version도 함께)을 기록해 삭제 표시하세요. 동기화 중에 tombstone을 진짜 변경으로 취급해 오래된 비삭제 상태를 덮어쓸 수 있게 하세요.

tombstone을 얼마나 오래 보관할지 결정하세요. 사용자가 몇 주 동안 오프라인일 수 있다면 그보다 더 길게 보관하세요. 활동 중인 디바이스들이 삭제 시점 이후로 동기화된 것이 확실해지면 정리(purge)하세요.

되돌리기를 지원한다면 되돌리기도 다른 변경으로 처리해 deleted_at을 제거하고 버전을 올리세요.

데이터 손실이나 사용자 불만을 유발하는 흔한 실수

Design sync-ready data models
Model PostgreSQL data with versions and audit fields using AppMaster’s visual Data Designer.
Start building

많은 동기화 실패는 조용히 좋은 데이터를 덮어쓰는 작은 가정에서 옵니다.

실수 1: 편집 순서를 디바이스 시간에 의존

폰의 시계가 틀릴 수 있고, 시간대가 바뀌며, 사용자가 수동으로 시간을 바꿀 수 있습니다. 디바이스 타임으로 변경 순서를 정하면 언젠가 잘못된 순서로 편집을 적용하게 됩니다.

서버 발행 버전(monotonic serverVersion)을 우선하고 클라이언트 타임스탬프는 표시용으로만 사용하세요. 시간이 꼭 필요하면 서버에서 조정하고 재조정 로직을 두세요.

실수 2: 민감한 필드에 대해 우발적 LWW 허용

LWW는 간단해 보이지만 상태, 합계, 승인, 배정 같은 필드에 부딪히면 문제가 됩니다. 이런 필드에는 명시적 규칙 또는 수동 검토를 요구하세요.

안전 체크리스트:

  • 상태 전이는 자유 텍스트가 아니라 상태 기계로 다루세요.
  • 합계는 라인 아이템에서 재계산하세요. 합계 숫자만 병합하지 마세요.
  • 카운터는 승자를 고르는 대신 델타를 적용해 병합하세요.
  • 소유권/담당자는 충돌 시 명시적 확인을 요구하세요.

실수 3: 오래된 캐시 데이터로 최신 서버 값을 덮어쓰기

클라이언트가 오래된 스냅샷을 편집한 뒤 전체 레코드를 업로드하면 서버의 최신 변경이 사라집니다.

보내는 데이터의 형태를 고치세요: 변경된 필드만(또는 변경 로그) 보내고, 편집한 기준 버전을 포함하세요. 기준 버전이 뒤처져 있으면 서버가 거부하거나 병합을 강제하게 하세요.

실수 4: 누가 무엇을 바꿨는지 기록이 없음

충돌이 발생하면 사용자는 한 가지 질문을 합니다: 내가 뭘 바꿨고 다른 사람이 뭘 바꿨나? 편집자 식별과 필드별 변경 기록 없이는 충돌 화면이 추측에 의존하게 됩니다.

updatedBy, 서버 측 업데이트 시간, 최소한의 필드별 감사 추적을 저장하세요.

실수 5: 전체 레코드 비교를 강요하는 충돌 UI

사람들에게 전체 레코드를 비교하게 하면 지칩니다. 충돌은 보통 1~3개 필드입니다. 충돌 필드만 보여주고 안전한 옵션을 미리 선택해 사용자가 한 번의 결정으로 나머지를 자동 수락하게 하세요.

AppMaster 같은 노코드 도구로 폼을 만든다면 같은 목표를 가지세요: 필드 수준에서 충돌을 해결해 사용자가 전체 폼을 스크롤하며 비교하지 않게 하세요.

빠른 체크리스트와 다음 단계

오프라인 편집을 안전하게 만들려면 충돌을 오류가 아닌 정상 상태로 다루세요. 최선의 결과는 명확한 규칙, 반복 가능한 테스트, 그리고 무슨 일이 일어났는지 쉬운 언어로 설명하는 UX에서 옵니다.

다음 기본 사항을 고정하기 전에 더 많은 기능을 추가하지 마세요:

  • 레코드 유형별로 필드별 병합 규칙을 할당하세요(LWW, 최대/최소 유지, 덧붙임, 합집합, 또는 항상 묻기).
  • 서버 제어 버전과 당신이 제어하는 updated_at을 저장하고 동기화 시 검증하세요.
  • 두 기기 테스트를 실행해 둘 다 오프라인에서 같은 레코드를 편집하고 동기화 순서를 A->B, B->A로 바꿔 결과가 예측 가능한지 확인하세요.
  • 하드 충돌을 테스트하세요: 삭제 vs 편집, 서로 다른 필드의 편집 vs 편집.
  • 상태를 명확히 표시하세요: Synced, Pending upload, Needs review.

실제 폼 하나로 전체 흐름을 엔드투엔드로 프로토타입하세요. 현실적인 시나리오를 사용하세요: 현장 기술자가 전화기로 작업 노트를 업데이트하고 디스패처가 태블릿에서 같은 작업의 제목을 수정합니다. 다른 필드를 건드리면 자동 병합하고 작은 "다른 기기에서 업데이트됨" 힌트를 보여주세요. 같은 필드를 건드리면 충돌 필드만 보여주는 간단한 검토 화면으로 이동시켜 두 가지 선택과 명확한 미리보기를 제공하세요.

전체 모바일 앱과 백엔드 API를 함께 빌드할 준비가 되면 AppMaster (appmaster.io)가 도움을 줄 수 있습니다. 데이터 모델링, 비즈니스 로직 정의, 웹 및 네이티브 모바일 UI 빌드를 한 곳에서 하고, 동기화 규칙이 확실해지면 배포하거나 소스 코드를 내보낼 수 있습니다.

자주 묻는 질문

What is an offline sync conflict, in plain terms?

충돌은 두 기기가 동일한 서버 기반 레코드를 오프라인 상태에서(또는 어느 쪽도 아직 동기화하기 전) 서로 다른 방식으로 변경했고, 서버가 나중에 두 업데이트가 모두 오래된 버전을 기반으로 했음을 발견할 때 발생합니다. 시스템은 서로 다른 값이 있는 각 필드에 대해 최종 값을 결정해야 합니다.

Which conflict strategy should I choose: last write wins, manual review, or field-level merge?

대부분의 비즈니스 폼에는 기본값으로 **필드 수준 병합(field-level merge)**을 권장합니다. 서로 다른 역할이 다른 필드를 수정하는 경우가 많아 두 변경을 사용자에게 방해하지 않고 모두 유지할 수 있습니다. **수동 검토(manual review)**는 금전, 승인, 규정 준수처럼 잘못 선택했을 때 큰 문제가 되는 필드에만 사용하세요. **마지막 쓰기 우선(LWW)**은 오래된 수정본을 잃어도 되는 저위험 필드에만 사용합니다.

When should the app ask the user to resolve a conflict?

두 편집이 다른 필드를 건드리면 보통 자동으로 병합하고 UI를 보여주지 않아도 됩니다. 같은 필드를 서로 다른 값으로 변경했다면 그 필드는 결정이 필요합니다. 전체 폼이 아니라 충돌하는 필드만 보여 범위를 작게 유지하세요.

How do record versions prevent silent overwrites?

서버의 단조 증가 카운터로서 version을 다루고, 클라이언트가 각 업데이트에 expected_version을 보내도록 요구하세요. 서버의 현재 버전이 일치하지 않으면 덮어쓰지 말고 충돌 응답으로 거부합니다. 이 규칙 하나로 서로 다른 순서로 동기화하더라도 "조용한 데이터 손실"을 막을 수 있습니다.

What metadata should every synced SQLite table include?

실무에서 최소한 안정적인 id, 서버에서 관리하는 version, 그리고 updated_at/updated_by와 같은 서버 측 감사 필드를 포함하세요. 디바이스에서는 업로드 대기 여부를 나타내는 pending_sync 같은 필드와 마지막 동기된 서버 버전을 추적하세요. 이들이 없으면 충돌을 감지하거나 도움이 되는 해결 화면을 보여줄 수 없습니다.

Should I sync the whole record or only changed fields?

변경된 필드만(패치) 보내고 기준이 된 expected_version을 함께 전송하세요. 전체 레코드를 보내면 작은, 겹치지 않는 수정도 불필요한 충돌을 만들고 오래된 캐시로 최신 서버 값을 덮어쓸 위험이 커집니다. 패치는 어떤 필드에 병합 규칙이 필요한지 명확하게 해줍니다.

Is it better to store snapshots or a change log for offline edits?

스냅샷은 구현이 단순합니다: 최신 전체 레코드를 저장해 나중에 서버와 비교합니다. 변경 로그(change log)는 더 유연합니다: set field, append note 같은 연산을 저장해 최신 서버 상태 위에 재생할 수 있어 노트나 태그 같은 추가적 업데이트에서 더 잘 병합됩니다. 구현 속도를 원하면 스냅샷, 빈번한 병합과 세부 감사가 필요하면 변경 로그를 선택하세요.

How should I handle delete vs edit conflicts?

삭제가 편집보다 우선인지 미리 결정하세요. 많은 비즈니스 앱에서는 삭제가 모든 곳에서 삭제로 간주되는 것이 기대에 맞습니다. 안전한 기본은 "tombstone"을 사용해 deleted_at과 버전을 기록하는 것입니다. 이렇게 하면 오래된 오프라인 업서트가 레코드를 다시 만들어내는 일을 방지할 수 있습니다. 되돌리기를 지원하려면 하드 삭제 대신 "archived" 상태를 쓰세요.

What are the most common mistakes that cause offline sync data loss?

주요 실수들 요약:

  • 디바이스 시간을 편집 순서 기준으로 신뢰하지 마세요. 서버 발행 버전을 사용하세요.
  • 상태, 담당자, 합계 같은 민감 필드에 대해 우발적 LWW를 피하세요. 명시적 규칙이나 수동 검토를 요구하세요.
  • 전체 레코드 업로드로 오래된 데이터를 덮어쓰지 마세요; 변경 필드만 보내고 기준 버전을 확인하세요.
  • 누가 무엇을 바꿨는지에 대한 최소한의 기록(updatedBy, 필드별 감사)을 남기세요.
  • 충돌 UI는 전체 폼 비교를 강요하지 말고 충돌 필드만 보여주세요.
How can I implement a conflict-friendly UX when building with AppMaster?

“저장됨(Saved)”은 로컬에 저장된 것을 의미하고, “동기화됨(Synced)”은 별도의 명시적 상태로 보여 사용자 기대를 맞추세요. AppMaster로 빌드할 때도 같은 구조를 유지하세요: 필드별 병합 규칙을 제품 로직으로 정의하고, 안전한 필드를 자동 병합한 뒤 실제 충돌이 있는 필드만 작은 검토 단계로 보냅니다. 두 대의 기기로 같은 레코드를 오프라인에서 편집하고 서로 다른 순서로 동기화해 결과가 예측 가능한지 테스트하세요.

쉬운 시작
멋진만들기

무료 요금제로 AppMaster를 사용해 보세요.
준비가 되면 적절한 구독을 선택할 수 있습니다.

시작하다