
"10초 걸리던 약품 검색을 3초로 줄이기까지의 여정"
오늘은 개발 중인 EGoYak(이고약) 앱에서 미국 의약품 데이터를 가져오는 DailyMed API의 성능을 개선한 경험을 공유한다.
EGoYak은 한국과 미국의 의약품 정보를 검색하고 비교할 수 있는 iOS 앱이다. 사용자가 한국 약품을 검색하면, 해당 성분을 분석하여 미국에서 판매되는 유사한 약품을 찾아주는 기능이 핵심이다.
초기 버전의 검색 프로세스는 매우 복잡하고 순차적이었다.
결과: 약품 10개 검색 시 평균 10~15초 소요
사용자 입장에서는 답답함을 느낄 수밖에 없는 느린 속도였다.
성능 병목 지점을 찾기 위해 각 단계별 소요 시간을 측정했다.
let start = Date()
// ... 작업 수행 ...
let duration = Date().timeIntervalSince(start)
print("소요 시간: \(String(format: "%.2f", duration))초")
기존의 for 루프 방식은 앞선 작업이 끝나야 다음 작업이 시작되는 구조였다. 이를 Swift Concurrency의 withTaskGroup을 사용하여 개선했다.
let maxConcurrency = 4 // 동시 처리 제한
var iter = spls.makeIterator()
var medicines: [USMedicine] = []
await withTaskGroup(of: USMedicine?.self) { group in
// 초기 Task 시작
for _ in 0..<min(maxConcurrency, spls.count) {
if let spl = iter.next() {
group.addTask { await self.convertSPLToUSMedicineWithDetails(spl: spl) }
}
}
// 파이프라인 방식으로 하나 완료 시 다음 작업 추가
while let result = await group.next() {
if let medicine = result { medicines.append(medicine) }
if let spl = iter.next() {
group.addTask { await self.convertSPLToUSMedicineWithDetails(spl: spl) }
}
}
}
사용자가 동일한 약품을 다시 검색할 때 즉각적인 반응을 주기 위해 3단계 캐싱 전략을 세웠다.
기존 275줄의 HTML 파싱 로직을 XML 내 파일명 활용 및 메모리 캐싱으로 대체하여 29줄로 줄였다.
앱 종료 후에도 데이터가 유지되도록 SwiftData를 도입했다. 의약품 데이터가 동적으로 변하는 타입은 아니기 때문에 영구 캐싱을 적용하는 데 무리가 없었다.
@Model
class CachedMedicineResults {
var koreanDrugName: String
var searchResults: [USMedicine]
var cachedDate: Date
// ... 초기화 로직
}
DailyMed의 XML은 매우 방대하다. 기존의 전체 파싱 방식 대신, 필요한 섹션 코드만 타겟팅하는 FastSPLXMLParser를 직접 구현했다.
기술적 최적화 외에도 사용자가 '느끼는' 속도를 개선했다.
| 항목 | 개선 전 | 개선 후 | 향상률 |
|---|---|---|---|
| 초기 검색 속도 | 10~15초 | 2~3초 | 80% 감소 |
| 재검색 (캐시) | 10~15초 | 0.1초 | 99% 감소 |
| XML 파싱 | 1초 / 개 | 0.3초 / 개 | 70% 감소 |
| 이미지 URL 파싱 | 0.5초 | 0.001초 | 99.8% 감소 |
| 파싱 코드 라인 수 | 275줄 | 29줄 | 90% 슬림화 |
ContinuousClock 등을 이용해 정밀하게 측정하지 않았다면 어디가 병목인지 정확히 알 수 없었을 것이다.마치며
사용자에게 10초는 영겁의 시간과 같다. 이번 최적화를 통해 기술적 성장뿐만 아니라 '사용자 중심'의 사고가 개발에 얼마나 큰 영향을 미치는지 다시 한번 느꼈다.