'로또 사고 싶은날' 개발기 5편: 동행복권 오픈 API 연동을 통한 실시간 당첨 결과 파싱하기

2026. 4. 16. 07:18AI Product Building/Lotto App

반응형

 

안녕하세요! '로또 사고 싶은날' 플러터(Flutter) 앱 개발기 5회차 포스팅입니다. 지난 네 번의 포스팅을 통해 우리는 앱의 기초 아키텍처를 잡고, 메인 화면과 그 위에 올라갈 영롱한 로또 공 컴포넌트까지 완벽하게 그려냈습니다. 하지만 껍데기만 예쁘고 데이터가 없다면 그것은 반쪽짜리 앱에 불과하겠죠? 로또 앱의 심장부는 바로 '정확하고 신속한 최신 당첨 번호'입니다. 이 데이터는 누가 매주 수기로 입력하는 것이 아니라, 당연하게도 인터넷을 통해 공식 기관의 서버에서 동적으로 가져와야 합니다. 이번 편에서는 제가 동행복권 공식 서버의 공공 오픈 API와 플러터 앱을 어떻게 끈끈하게 연결(연동)시켰는지, 그리고 받아온 날것의 JSON 데이터를 어떻게 안전하게 앱 화면에 뿌려주는지에 대한 네트워크 통신 코딩 노하우를 아주 상세히 파헤쳐 보겠습니다.

👉 '로또 사고 싶은날' 구글스토어 다운로드 바로가기 👈


1. 동행복권 무료 API의 구조와 비밀 파헤치기

데이터를 가져오려면 먼저 어디서 데이터를 주는지 알아야 합니다. 다행히도 대한민국 로또 공식 수탁 사업자인 동행복권에서는 아주 심플한 형태의 무료 API를 제공하고 있습니다. 별도의 복잡한 API Key 발급이나 회원가입 절차 없이, 특정 URL 주소에 '회차 번호'를 적어 브라우저 주소창에 엔터를 치면 즉시 당첨 정보를 텍스트(JSON) 형태로 뱉어주는 매우 자비로운 구조입니다.

예를 들어, 웹 브라우저 주소창에 https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=1100라고 입력하면, 1100회차 로또 결과가 응답으로 날아옵니다. 플러터 앱 내부에서도 사용자는 버튼을 누르지만 앱 코드는 이 주소로 은밀하게 HTTP GET 요청을 날리는 원리입니다. 반환되는 데이터를 분석해보면 당첨 날짜(drwNoDate), 1등 당첨금(firstWinamnt), 그리고 대망의 당첨 번호 6개(drwtNo1~6)와 보너스 번호(bnusNo)가 깔끔한 키-값 구조로 들어있는 것을 확인할 수 있습니다. 데이터 호출 자체가 간단하다 보니 이제 관건은 "이 데이터를 어떻게 플러터의 언어(Dart 모델 객체)로 오류 없이 변환(Serialization)시킬 것인가?"가 되었습니다.

2. 플러터(Dart)에서 JSON 데이터를 안전하게 파싱(Parsing)하기

서버에서 날아온 데이터는 단순히 기나긴 문자열(String)에 불과합니다. 이를 우리가 지난 4회차에서 만든 로또 공 컴포넌트(LottoBallWidget)의 입력값으로 쏙쏙 집어넣으려면 이 문자열을 Dart가 이해할 수 있는 LottoResult라는 객체 지향적 클래스로 변환해야 합니다. 이 과정을 파싱(Parsing) 또는 직렬화/역직렬화(Serialization)라고 부릅니다.

플러터에서 가장 기본이 되는 통신 라이브러리인 http 패키지를 사용하여 서버 요청 코드를 작성합니다. 여기서 가장 주의해야 할 점은 방어적 프로그래밍(Defensive Programming)과 에러 처리입니다. 서버에서 받아온 데이터는 항상 우리가 예상한 대로 들어오지 않습니다. 아직 추첨이 되지 않은 미래의 회차를 요청했을 때는 returnValue: "fail" 이라는 응답이 날아오고, 가끔 서버 점검 중일 때는 HTML 오류 페이지 전체가 문자열로 날아오기도 합니다. 따라서 단순 파싱을 넘어, 정상 응답 코드(200)인지 확인하고 응답에 포함된 returnValue 값을 먼저 체크하여 에러 시 앱이 튕기거나 크래시(Crash)가 나지 않도록 단단하게 코드를 묶어주는 것이 '로또 사고 싶은날' 앱 서비스의 핵심 노하우입니다.

3. 최신 회차 번호를 앱 스스로 계산해 내는 알고리즘

API를 쓰다 보면 한 가지 난관에 부딪힙니다. "1100회차 결과를 달라고 URL을 쏘는 건 알겠는데, 앱을 켜면 자동으로 '이번 주 최신 회차' 결과를 가져와야 하잖아? 오늘이 도대체 몇 회차인지 앱이 어떻게 알지?" 동행복권 API는 아쉽게도 '최신 회차 번호만 알려주는 전용 URL' 구조를 따로 제공하지 않습니다. 이 문제를 해결하기 위해 약간의 수학적 달력 계산 알고리즘이 필요했습니다.

로또 1회차의 추첨일은 2002년 12월 7일 (토요일 오후 8시 40분경)이었습니다. 따라서 스마트폰 기기의 현재 시간(DateTime.now())과 기준일인 1회차 추첨 시간 사이의 시간 간격을 계산하고, 그 값(밀리초 또는 일 단위)을 7일(1주일)로 나누면 오늘이 정확히 역대 몇 번째 회차인지 앱이 스스로 알아낼 수 있습니다! 저는 LottoDateCalculator라는 유틸리티 클래스를 별도로 만들어, 앱 구동 즉시 현재 주의 회차 번호를 계산한 뒤 그 번호를 API 파라미터로 던지게끔 통신 아키텍처를 설계했습니다. 이는 아주 가볍지만 완벽하게 동작했습니다.

4. 서버 부하 줄이기 ও 성능 향상: 데이터 캐싱(Caching) 전략 - 코드로 보기

마지막으로 다룰 핵심은 최적화입니다. 매주 로또 당첨 번호는 토요일 밤에 딱 한 번 바뀝니다. 즉, 일요일에 앱을 켜든 수요일에 켜든 데이터는 완전히 동일합니다. 그런데 사용자가 앱 화면을 전환할 때마다 매번 똑같은 번호를 달라고 동행복권 서버에 네트워크 요청을 날리면, 데이터 요금 낭비는 물론이고 로딩 동그라미(인디케이터)를 계속 봐야하는 사용자 경험 저하를 유발합니다.

그래서 저는 3회차에서 구축한 로컬 DB(Hive)를 활용해 최초 1회만 API를 통신해서 데이터를 가져오고, 그 즉시 스마트폰 기기 내부에 저장(Caching)해 두는 방식을 택했습니다. 이후에는 무조건 로컬 DB에서 0.01초 만에 데이터를 꺼내오며, 토요일 오후 9시경만 지난 경우에만 강제로 네트워크를 태워 최신 DB를 업데이트하는 분기 처리를 구현했습니다.

아래 코드는 이 모든 철학이 응집된, 실제 앱 내부 ApiService 파일의 핵심 통신 로직입니다.

// lib/services/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:my_feel_lotto/models/lotto_result_model.dart';
import 'package:my_feel_lotto/core/constants.dart'; // 기본 API URL 선언된 곳

class ApiService {
  /// 특정 회차(drwNo)의 로또 당첨 정보를 동행복권 서버로부터 가져옵니다.
  /// 네트워킹 실패 또는 파싱 오류 시 Exception을 반환하여 UI단에서 에러팝업을 띄우게 합니다.
  Future<LottoResultModel?> fetchLottoResult(int drwNo) async {
    // 요청할 최종 URL 생성 (예: https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=1100)
    final String url = '${Constants.baseLottoApiUrl}$drwNo';

    try {
      // 5초 타임아웃 제한을 걸어, 로또 사이트 폭주로 응답이 늦어지면 앱이 무한 대기하는 현상 방지
      final response = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 5));

      if (response.statusCode == 200) {
        // 한글 깨짐 방지를 위해 UTF-8 명시적 디코딩 후 Map 객체로 변환
        final Map<String, dynamic> jsonData = jsonDecode(utf8.decode(response.bodyBytes));

        // returnValue 값이 'fail' 인 경우 (아직 추첨 전인 미래 회차 요청 시)
        if (jsonData['returnValue'] == 'fail') {
          return null; // 데이터 없음 처리
        }

        // 정상 데이터라면, Map을 우리가 만든 Dart 클래스(Model)로 완벽 맵핑!
        return LottoResultModel.fromJson(jsonData);
      } else {
        throw Exception('통신에 실패했습니다. (상태 코드: ${response.statusCode})');
      }
    } catch (e) {
      // 그 외 알 수 없는 네트워크 끊김이나 타임아웃 에러 통합 핸들링
      print('🛠 API Fetch Error: $e');
      throw Exception('서버에 연결할 수 없습니다. 인터넷 상태를 확인해 주세요.');
    }
  }
}

// 참고: LottoResultModel 내부의 fromJson 팩토리 로직 일부
// factory LottoResultModel.fromJson(Map<String, dynamic> json) {
//   return LottoResultModel(
//     drwNo: json['drwNo'],
//     drwNoDate: json['drwNoDate'],
//     firstWinamnt: json['firstWinamnt'],
//     bnums: [json['drwtNo1'], json['drwtNo2'], json['drwtNo3'], 
//             json['drwtNo4'], json['drwtNo5'], json['drwtNo6']],
//     bonusBnum: json['bnusNo'],
//   );
// }

이렇게 네트워크 통신(http) 모듈을 단단하게 구축해 두면, 앞서 설계한 뷰모델(ViewModel) 계층에서 이 기능을 호출하기만 하면 메인 UI에 역대 번호가 마법처럼 쏙쏙 채워집니다. 서버의 데이터가 마침내 제 앱의 화려한 로또 공 위젯 숫자로 빙의되는 감동적인 순간이죠!

다음 주 6회차 포스팅에서는 대망의 핵심 기능, '카메라를 이용한 QR 코드 스캐너 기능 구현'Google ML Kit를 어떻게 플러터에 올려 초고속 스캔을 완성했는지, 기술의 정수를 다루어 보겠습니다. 기대해 주세요!

💡 토요일 밤, 끊김 없고 신속한 실시간 앱의 성능과 애니메이션을 직접 테스트해 보고 싶다면?
👉 [로또 사고 싶은날 - 렉 없는 로또 당첨 확인 바로가기]

```

반응형