올리브영 테크블로그 포스팅 올리브영 앱 스마트 스캐너 개선
iOS, Android

올리브영 앱 스마트 스캐너 개선

바코드로도 상품 검색이 가능하다는 사실, 알고 계셨나요?

2024.04.02

안녕하세요. 모바일앱개발팀 윌, 의지수입니다🙇‍♂️
이번 글에서는 올리브영 앱의 바코드 스캔 성능을 개선한 경험을 공유하고자 합니다.

2024 APP뿐페스티벌

올리브영은 매년 옴니채널 활성화를 위해 APP뿐페스티벌을 진행합니다. 올해 APP뿐페스티벌에서는 '올리를 찾아라' 이벤트가 진행되었습니다. 그중 하나는 오프라인 매장에서 진열된 상품의 바코드를 스캔하여 온라인몰의 상품 상세 페이지로 진입한 후 리뷰를 보면서 올리를 찾는 이벤트였습니다.

매끄러운 이벤트 진행을 위해 내부에서 사전 테스트를 진행해 보니, 특정 환경에서 앱 내 스마트 스캐너의 바코드 인식률이 높지 않았습니다. 이를 해소하기 위해 해당 기능을 담당하는 검색/탐색 스쿼드에서 모바일앱개발팀에 협업을 요청하셨습니다.

문제 정의

product01 product02 product05
product04 product03 product06

본사 지하 1층에 위치한 올리브영 플러스점에 방문하여 함께 바코드 인식 테스트를 해보니 다음과 같은 문제점들을 발견할 수 있었습니다.

  • 상품이 작아 인쇄된 바코드도 작다.
  • 바코드가 전자 라벨에 표기되어 흑백의 대조가 약하고, 바코드 막대의 높이가 작다.
  • 상품 포장지가 빛을 반사하는 재질이 많아 바코드가 반사광에 가려지는 경우가 잦다.

원활한 이벤트 참여를 위해서는 반드시 해결해야 하는 이슈였기에 모바일앱개발팀에서도 스마트 스캐너의 인식 성능을 향상시킬 수 있는 방법을 찾아보기 시작했습니다.

근본 원인 해결

iOS: Meet Vision framework

기존 iOS의 스마트 스캐너는 AVCaptureSession을 구성, AVCaptureVideoPreviewLayer를 이용하여 UI를 그렸고 AVCaptureMetadataOutputObjectsDelegate를 이용하여 바코드의 값을 읽는 구조로 만들어져 있었습니다.

@objc
private func handleZoom(_ recognizer: UIPinchGestureRecognizer) {
    let normalizedScaleInput = normalizeGestureScale(recognizer.scale)

    switch recognizer.state {
    case .began, .changed:
        do {
            try videoDeviceInput?.device.lockForConfiguration()
            videoDeviceInput?.device.videoZoomFactor = normalizedScaleInput
            videoDeviceInput?.device.unlockForConfiguration()
        } catch {
            // MARK: 설정 중 발생하는 에러 처리
        }

    case .ended, .cancelled:
        // MARK: 종료, 취소 시 처리

    default:
        break
    }
}

우선 상품과 바코드가 작은 경우에 대응하기 위해 핀치 줌 기능을 구현했습니다. 뷰에 UIPinchGestureRecognizer를 추가하고 recognizer가 올려주는 값을 적절히 가공합니다. videoDeviceInput의 videoZoomFactor 값을 가공한 값으로 설정하여 자연스러운 줌을 구현할 수 있었습니다.

기존 구조에 줌 기능을 추가 구현하고 얼마나 개선됐는지 테스트를 진행했습니다. 바코드 사이즈가 작은 경우에는 확실히 이전보다 잘 인식하였습니다. 하지만 여전히 바코드의 배경색이 하얀색이 아니어서 색의 대조가 크지 않거나, 바코드의 막대가 길쭉길쭉하지 않거나, 반사광에 바코드가 가려지는 경우가 발생하는 경우에는 인식률이 상승하지 않았습니다.

이 경우에 대응하기 위해 자료조사를 한참 하던 어느 날, 바코드나 QR코드는 카메라 하드웨어로도 인식할 수 있지만 Vision 프레임워크를 이용하여 인식률을 더 끌어올릴 수 있다는 자료를 찾았고 바로 시도 해보았습니다.

func captureOutput(
    _ output: AVCaptureOutput,
    didOutput sampleBuffer: CMSampleBuffer,
    from connection: AVCaptureConnection
) {
    guard
        let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
    else {
        return
    }

    let imageRequestHandler = VNImageRequestHandler(
        cvPixelBuffer: pixelBuffer,
        orientation: .right
    )

    let detectBarcodeRequest = VNDetectBarcodesRequest { [weak self] request, error in
        guard
            error == nil
        else {
            // MARK: 에러 처리
        }
        self?.processVision(request)
    }

    do {
        try imageRequestHandler.perform([detectBarcodeRequest])
    } catch {
        // MARK: 에러 처리
    }
}

AVCaptureVideoDataOutputAVCaptureSession에 연결하고, AVCaptureVideoDataOutputSampleBufferDelegate를 연결해 프리뷰에 보여지는 프레임 이미지의 데이터를 받아옵니다. 이 이미지 데이터를 픽셀 버퍼의 형태로 변환 합니다. VNDetectBarcodeRequest 객체를 만들어 픽셀 버퍼를 담아 VNImageRequestHandler 객체에 전달, Vision 프레임워크에 바코드 인식을 요청합니다. 인식 결과값을 받았을때는 VNBarcodeObservation 형태로 변환 하여 바코드 데이터를 추출, 화면을 이동시킵니다.

Android: ZXing에서 CameraX + MLKit으로 넘어가기

기존 안드로이드의 스마트 스캐너는 ZXing 라이브러리를 이용하여 구현하고 있었는데요. 안드로이드에서 카메라 기능 사용시 CameraX 사용을 권장하는 점, MLKit이 구글에서 밀고있는 Vision API인 점으로 보아 ZXing을 걷어내기로 결정하였습니다.

먼저 ZXing을 걷어내면서 그동안 커스텀의 제약이 있어 100% 반영하지 못했던 스마트 스캐너의 본 디자인을 입힐 수 있게 되었습니다! CameraXPreviewView로 전체 화면을 카메라로 만들고 Canvas에 Round Corner 형태의 사각형이 가운데 위치하도록 직접 그려 PreviewView의 overlay에 뷰를 추가하였습니다. 또한, CameraXPreviewViewLifecycleCameraController를 지정하여 라이프 사이클을 바인딩했고 카메라 사용이 끝날때 unBind()를 호출하여 바인딩을 해제해주었습니다. 줌 기능 같은 경우에는 CameraX의 기본 카메라를 사용하기 때문에 따로 설정할 필요가 없었습니다.

제일 중요한 부분인 카메라 이미지를 실시간으로 받아와서 분석해주는 코드를 한 번 살펴보겠습니다. ImageAnalysis의 Analyzer는 MLKitAnalyzer 사용하여 구글에서 제공하는 이미지 분석을 사용하도록 하였습니다.

val options = BarcodeScannerOptions.Builder()
            .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
            .build()

cameraController.setImageAnalysisAnalyzer(
        Executors.newSingleThreadExecutor(),
        MlKitAnalyzer(
            listOf(BarcodeScanning.getClient(options)),
            CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
            ContextCompat.getMainExecutor(this)
        ) { result: MlKitAnalyzer.Result? ->
                // 이미지 분석을 통해 받은 결과값 활용
        }
    ...
)

MLKitAnalyzer의 첫번째 인자로는 감지할 이미지 객체들을 리스트 형태로 넣어주면 됩니다. 스마트 스캐너는 바코드만을 감지하기에 바코드 스캔 객체로만 구성하였습니다. MLKit의 Detector들을 참고하여 여러 형태의 이미지를 감지하도록 구현할 수 있고 바코드 스캔 객체 중에서도 감지가 필요한 특정 바코드 포맷 타입을 BarcodeScannerOptions에 지정할 수 있습니다. 두번째 인자로는 좌표값에 반영될 옵션을 지정합니다. 별다른 좌표 변환이 필요하지 않은 경우 COORDINATE_SYSTEM_VIEW_REFERENCED 기본 옵션을 사용하면 됩니다. 마지막으로는 실행할 Executor를 명시해주면 됩니다.
이렇게 간단한 구현으로 여러 케이스의 바코드 스캔 인식률을 개선하였습니다!

함께하는 필드 테스트, 더 쾌적할 수 있도록 추가 개선점 찾기

각 플랫폼에 맞는 Computer Vision을 구현하여 근본 원인을 해결한 후, 검색/탐색 스쿼드분들과 올리브영 플러스점에 다시 내방하여 테스트를 진행했습니다. 다양한 제품의 바코드들과 매대의 전자 라벨 바코드를 직접 스캔해보면서 월등히 나아진 인식 성능을 체감할 수 있었습니다. 하지만 여기서 멈추지 않고 고객님들이 더 쾌적하게 이벤트에 참여하실 수 있도록 기획자 개발자 할 것없이 함께 추가 개선점들을 찾았습니다.

  • 실시간 바코드 인식 결과를 화면에 표시하도록 UI 추가
  • 바코드 인식 성공 후 상품 화면 이동시 햅틱 피드백 추가
  • QR코드와 바코드가 함께 인식되는 경우 바코드를 우선 적용하도록 개선

추가 개선

iOS

스캐너 인식 영상

비전 프레임워크에서 받은 인식 결과에는 바코드의 데이터 뿐 아니라 상대 좌표도 포함되어 있습니다. 이 상대 좌표를 VNImageRectForNormalizedRect를 이용하여 절대좌표로 복원한 후 적절히 가공하여 카메라 프리뷰 레이어 위로 실시간 인식 결과를 표시하는데 사용했습니다.

또한 한 화면에 QR코드와 바코드가 동시 인식되는 경우에는 바코드의 값을 먼저 사용할 수 있도록, VNBarcodeObservation의 symbology를 기준으로 정렬하여 바코드의 값이 먼저 사용될 수 있도록 구현했습니다.

// MARK: 정렬하기 위해서는 VNBarcodeObservation에 Comparable을 구현해야 합니다.
extension VNBarcodeObservation: Comparable {
    static func < (lhs: VNBarcodeObservation, rhs: VNBarcodeObservation) -> Bool {
        lhs.symbology < rhs.symbology
    }
}
extension VNBarcodeSymbology: Comparable {
    static func < (lhs: VNBarcodeSymbology, rhs: VNBarcodeSymbology) -> Bool {
        lhs.rawValue < rhs.rawValue
    }
}

Android

우선 CameraSelector의 인식할 카메라 타입을 후면 카메라로만 명시하였고 QR코드와 바코드가 함께 인식되는 경우를 계산하기 위해서 앞에서 지정한 바코드 옵션 코드를 확인했습니다. 앞에서 모든 바코드 타입이 인식되도록 설정했기에 Barcode 클래스의 각 타입의 포맷값을 확인했습니다. QR코드는 일반 바코드보다 큰 값을 가지고 있어서 인식한 바코드들을 오름차순 정렬하고 첫 번째 값만 SharedFlow로 반환되도록 적용하였습니다.

반환된 바코드 값이 collect되는 시점에서 인식 결과가 라인으로 화면에 표시될 수 있도록 직접 drawLine을 그려 뷰를 추가하도록 구현했습니다. 또한 인식된 바코드 boundingBox의 가운데 값을 계산하여 바코드가 가로, 세로 모양 상관없이 정가운데 라인이 표시되도록 하였습니다. 바코드 인식 결과값은 계속해서 emit, collect 되기 때문에 직접 throttleFirst의 역할을 하는 코루틴 함수를 만들어 2초 동안 반환된 flow 중 첫 번째로 collect한 값만 인식되도록 하였습니다. 최종적으로 바코드 인식 후 상품 상세 화면으로 넘어갈 때는 공통으로 사용하는 진동 피드백을 적용하였습니다!

마무리

이렇게 추가 개선 사항 구현까지 마무리한 후, 테스트를 완료하여 APP뿐페스티벌 시작 전 3월에 앱 v3.11.0에 개선된 스마트 스캐너를 배포할 수 있었고 24년 APP뿐페스티벌은 평시 대비 +5,296%의 스캐너 사용 수를 기록하며 잘 마무리되었습니다! (크래시가 한 번도 발생하지 않은 것도 자랑하고 싶었어요)

이렇게 공들인 스마트 스캐너, 지금 바로 올리브영에 방문해서 사용해 보시면 어떨까요?

AVFoundationVisionMLKit
올리브영 테크 블로그 작성 올리브영 앱 스마트 스캐너 개선
💪
의지수 |
Android App Engineer
의지로 앱개발을 하고있습니다 :)