2026. 1. 20. 06:47ㆍAI Product Building/HalloGerman
이 글의 핵심:
CSV 업로드 기능은 겉보기에는 단순하지만, 실제로는 사용자 이탈을 가장 많이 유발하는 기능 중 하나다. 파일 형식, 인코딩, 컬럼 순서, 잘못된 데이터까지 모든 것이 오류의 원인이 된다. 8편에서는 HalloGerman에서 CSV 단어장 업로드를 어떻게 설계했고, 사용자가 “실패하지 않게” 만들기 위해 어떤 UX와 방어 로직을 적용했는지 설명한다.
1. CSV 업로드 기능이 꼭 필요한 이유
단어장 앱의 핵심 가치는 “내 단어장”이다. 하지만 사용자가 앱 안에서만 단어를 하나씩 입력해야 한다면, 그 순간 진입 장벽이 생긴다.
CSV 업로드 기능은 이 장벽을 크게 낮춘다.
- 기존 엑셀 단어장을 그대로 사용
- 학원, 교재, 업무 단어장 재활용
- 대량 입력에 대한 심리적 부담 제거
문제는 CSV 업로드가 실패하면, 사용자는 대부분 “내가 뭘 잘못했는지” 알지 못한 채 앱을 떠난다는 점이다.
2. CSV 업로드 설계의 출발점: 규칙을 최소화하라
HalloGerman에서는 CSV 포맷 규칙을 최대한 단순하게 정의했다.
- 1열: 독일어 단어
- 2열: 의미
그 외 컬럼은 모두 무시한다. 헤더 유무도 상관없다. 이 단순한 규칙 하나가 성공률을 크게 높였다.
“엄격한 규칙은 개발자를 편하게 하고, 느슨한 규칙은 사용자를 살린다.”
3. 업로드 UX의 핵심은 ‘사전 안내’다
파일을 고르게 한 다음 에러를 보여주는 방식은 최악이다. 그래서 업로드 버튼을 누르기 전, 다음 정보를 미리 보여준다.
- 지원 포맷 예시 (간단한 CSV 샘플)
- 필수 컬럼 설명
- 자주 발생하는 오류 안내
이 단계에서 사용자는 이미 “어떻게 만들어야 하는지” 감을 잡게 된다.
4. 파일 선택 단계에서의 UX 포인트
Android에서는 파일 선택부터가 첫 번째 관문이다.
- .csv 확장자만 필터링
- 최근 사용 파일 우선 노출
- 선택 취소 시 아무 메시지도 띄우지 않기
취소는 오류가 아니다. 그래서 별도의 경고를 보여주지 않는다.
5. CSV 파싱은 ‘부분 성공’을 허용해야 한다
CSV 파일에는 항상 문제가 있는 행이 섞여 있다.
- 빈 줄
- 단어만 있고 의미가 없는 행
- 특수 문자 깨짐
HalloGerman에서는 “한 줄이라도 오류가 있으면 전체 실패” 방식을 사용하지 않았다.
대신 다음 원칙을 적용했다.
- 정상 행은 최대한 살린다
- 문제 있는 행은 조용히 건너뛴다
- 최종 결과만 요약해서 보여준다
6. CSV 파싱 로직의 핵심 구조
for (row in csvRows) {
val term = row.getOrNull(0)?.trim()
val meaning = row.getOrNull(1)?.trim()
if (!term.isNullOrEmpty() && !meaning.isNullOrEmpty()) {
words.add(
Word(
id = generateId(),
term = term,
meaning = meaning,
source = WordSource.CSV
)
)
}
}
여기서 중요한 점은 “에러를 던지지 않는다”는 것이다. 조건에 맞지 않으면 그냥 넘어간다.
7. 업로드 결과 피드백은 숫자로만
업로드가 끝난 뒤, 사용자에게 보여주는 메시지는 매우 단순하다.
- 총 몇 개의 단어가 추가되었는지
- 몇 개의 행이 제외되었는지
행 번호, 에러 원인 같은 정보는 보여주지 않는다. 그런 정보는 대부분 사용자에게 도움이 되지 않는다.
예시 메시지:
단어 120개가 추가되었습니다.
일부 항목은 형식 오류로 제외되었습니다.
8. 중복 단어 처리 전략
CSV 업로드에서 또 하나의 문제는 중복이다.
HalloGerman에서는 다음 전략을 사용했다.
- 기존 단어와 완전히 동일하면 추가하지 않음
- 의미가 다른 경우에는 허용
중복 경고 팝업은 띄우지 않는다. 대신 결과 숫자에만 반영한다.
9. 이 UX 설계가 만든 변화
이 방식으로 CSV 업로드를 설계한 이후, 다음과 같은 변화가 있었다.
- 업로드 실패 문의 감소
- 첫 사용 후 단어장 보유량 급증
- 앱 재방문율 상승
기능 자체보다 “실패하지 않게 만드는 설계”가 더 중요하다는 것을 다시 한 번 확인했다.
[주요 코드]
//파일 선택 런처
//NotebookDetailScreen.kt
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
try {
val wordsList = mutableListOf<NotebookWord>()
val inputStream = context.contentResolver.openInputStream(it)
val reader = BufferedReader(InputStreamReader(inputStream))
var lineCount = 0
...
} catch (e: Exception) {
Toast.makeText(context, "Error reading CSV: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
//실패 가능성이 높은 구간(파일 선택, 스트림)을 UI에서 감싼다.
//CSV 파싱 + 정제
//NotebookDetailScreen.kt
// Basic CSV parsing (comma separated)
fun sanitize(text: String): String {
return text.replace(Regex("['\"&;]"), "").trim()
}
val parts = line.split(",").map { sanitize(it) }
//사용자 CSV는 항상 “더럽다”. 여기서 한 번 정리해줘야 실패가 줄어든다.
//import 단어 생성
//NotebookDetailScreen.kt
wordsList.add(
NotebookWord(
word = parts[0],
meaning = parts[1],
pronunciation = parts.getOrNull(2) ?: "",
example1 = parts.getOrNull(3) ?: "",
source = "CSV Import",
createdAt = System.currentTimeMillis()
)
)
//import의 출처를 명확히 남기면 이후 관리/검색이 쉬워진다.
10. 다음 편 예고
9편. 학습 기록과 진도 관리 – 사용자를 지치게 하지 않는 방법
다음 글에서는 학습 기록을 어떻게 저장하고, 왜 눈에 띄게 보여주지 않았는지, 그리고 최소한의 진도 관리 UX를 어떻게 설계했는지 다룬다.
참조 라이브러리 및 도구 모음
'AI Product Building > HalloGerman' 카테고리의 다른 글
| [독일어 학습 앱 개발기 10편] 읽기 학습과 음성 인식은 왜 분리했을까 – 사용자가 말한 단어를 처리하는 구조 (0) | 2026.01.22 |
|---|---|
| [독일어 학습 앱 개발기 9편] 말하기 학습 구현 – Android 음성인식(STT)을 실제 서비스에 적용하는 방법 (0) | 2026.01.21 |
| [독일어 학습 앱 개발기 7편] 뉴스 읽기 기능 구현 – 독일어 콘텐츠를 학습 도구로 바꾸는 구조 (0) | 2026.01.19 |
| [독일어 학습 앱 개발기 6편] TTS와 음성 인식 구현 – Android에서 가장 까다로운 기능 다루기 (0) | 2026.01.18 |
| [독일어 학습 앱 개발기 5편] 단어 암기 UI 설계 – 듣기·말하기·쓰기를 하나의 흐름으로 만드는 법 (0) | 2026.01.17 |