책 검색 앱 (5) — 레포지토리 작성 & 테스트

Eungae's avatar
Jan 16, 2026
책 검색 앱 (5) — 레포지토리 작성 & 테스트

레포지토리 작성

데이터를 가지고 오는 레포지토리에 Book 레포지토리를 만들어서 구현해보자.
 
우선 플러터 프로젝트 내에서 HTTP 요청을 하기 위해서 http 패키지를 추가한다.
http 패키지 : 앱 내에서 http 요청을 하기 위한 다트 패키지
> flutter pub add http
 
프로젝트의 repostories 폴더 하위에 book_repository.dart 파일을 생성한다.
searchBooks라는 함수 매서드를 활용하는 BookRepository 클래스를 만들어 준다.
notion image
 
http 클라이언트 사용법
  1. 클라이언트 객체 생성: http 패키지 내의 Client 클래스를 활용할 수 있도록 자동완성을 통해 import할 것.
  1. client.get 메서드 안에 Uri.parse() 메서드 넣기. parse 메서드에 api url 넣고, searchBooks 메서드와 연결될 수 있도록 $query 파라미터로 연결할 것.
  1. 헤더 세팅: client-id와 키값을 각각 Map타입으로 넣어준다.
import 'package:http/http.dart'; class BookRepository { void searchBooks(String query) { // searchBooks 메서드 호출 시 검색해야 될 쿼리를 넘겨받을 수 있도록 파라미터 추가하기. -> url에 $query로 연결해줄 것 final clinent = Client(); // client 객체를 선언하는데, Client()라는 패키지 내 클래스로 설정한다. -> http 패키지가 import될 수 있도록 자동완성 내 Client를 선택할 것. clinent.get(Uri.parse( 'https://openapi.naver.com/v1/search/book.json?query=$query'), // get메서드 안에 Uri 클래스와 .parse 스태틱 함수를 활용할 것. parse 함수 내에는 api url을 그대로 넣어준다. $query를 입력하여 searchBooks 메서드와 연결. headers: { // client.get 요청할 때 headers 파라미터가 필요함. Map 타입으로 헤더 넣어줄 것. 'X-Naver-Client-Id': 'reRrRQXlGEgsdFD3YVfe', 'X-Naver-Client-Secret': 'IyHwQEOGxH', } ); } }
  1. 현재 client.get의 타입은 Future<Response> 임.
    1. 통신의 기본은 비동기임. 우리는 이걸 동기처럼 안에서 처리할 것이기 때문에 asyncawait를 넣어준다.
      아직 Response 타입이 뭔지 정확하게 알 수 없으므로 final response 로 변수 선언 해둔다.
import 'package:http/http.dart'; class BookRepository { void searchBooks(String query) async { final clinent = Client(); final response = await clinent.get(Uri.parse( 'https://openapi.naver.com/v1/search/book.json?query=$query'), headers: { 'X-Naver-Client-Id': 'reRrRQXlGEgsdFD3YVfe', 'X-Naver-Client-Secret': 'IyHwQEOGxH', } ); } }
  1. response 객체 세팅
    1. response.body: http 요청 후 응답 데이터를 가져옴
    2. response.statusCode : http 요청 후 응답 코드를 가져옴
      1. // Get 요청 시 성공 => 200 // 응답코드가 200일 때 -- body 데이터를 jsonDecode 함수 사용해서 map으로 바꾼 후 List<Book> 으로 반환 if(response.statusCode == 200) { Map<String, dynamic> map = jsonDecode(response.body); } // -> http 요청하여 받은 응답데이터(response.body)를 JSON구조의 데이터로 바꿔줘(jsonDecode). 이 구조를 map이라는 파라미터로 받을 건데, 이 파라미터의 타입은 Map이야. // 200이 아닐 때 -- 빈 리스트 반환 response.body; response.statusCode;
      2. 응답코드가 200일 때 : body 데이터를 jsonDecode 함수 사용해서 map으로 바꾼 후 List<Book>으로 반환
        • 현재 response.body의 형태
          • { "lastBuildDate": "Mon, 12 Jan 2026 03:20:10 +0900", "total": 5062, "start": 1, "display": 10, "items": [ { "title": "Lieben kennt kein Alter (Harry)", "link": "https://search.shopping.naver.com/book/catalog/57232975287", "image": "https://shopping-phinf.pstatic.net/main_5723297/57232975287.20251015111529.jpg", "author": "", "discount": "31210", "publisher": "tredition", "pubdate": "20251010", "isbn": "9783384709325", "description": "Harry ist ein uberzeugter Junggeselle. Sein Leben ist sehr geregelt und ruhig. Bei einem Golfturnier lernt er Frieda kennen, die Ehefrau eines Golfkollegen. Frieda will in ihrem letzten Lebensabschnitt all..." }, { ...
          • 이하의 값들을 key로 갖는 문자열
            • lastBuildDate
            • total
            • start
            • display
            • items
          • ⇒ jsonDecode를 통해 Map타입의 데이터로 만듦.
            • → map[”items”]는 도서 정보의 Set을 요소로 하는 List의 모습을 하고 있으나 그 타입은 String임. ⇒ List.from(map[’items’])를 통해 String을 List로 만들어주고, 그 리스트를 final items에 넣어주는 것. ⇒ final items의 타입은 List<dynamic>임. 여기서 다시 설명하자면, items의 타입은 리스트인데, 그 리스트의 요소들을 보자면 도서 각 권의 정보를 문자열 형태로 담고 있는 Map의 모양새를 갖고 있음.
        • 썬더 클라이언트에서 요청했던 값을 보면 오브젝트들의 리스트들이 items라는 키값 안에 들어가 있음.
        • 이것만 따로 불러와서 처리할 것.
        if(response.statusCode == 200) { Map<String, dynamic> map = jsonDecode(response.body); final items = List.from(map['items']); // Map타입의 인스턴스 map 내에 key값이 'items'인 놈의 value들을 추출해서 리스트로 만들고, 그걸 items라는 final 파라미터에 담을거야. items.map((e) return Book.fromJson(e); });
        • List<dynamic> items에 들어있는 도서 정보를 활용하기 위해 .map을 통해 각각의 도서 정보를 디코딩(= 문자열 상태에서 데이터셋으로 변환)함. 이때 .map은 MapptedIterable 클래서 메서드임. (Iterable<T> map<T>(T toElement( E e)) ⇒ MappedIterable<E, T>(this, toElement);
        • 이때, 반환하는 형태는 Book 클래스에 fromJson 메서드에 따라 반환할 것.
        • 현재 items는 List<dynamic> 타입임. List에서 map을 호출하면 (items.map ← 이거 말하는 거임) return타입이 Iterable이라고 됨.
          • items.map 위에 커서 올려놓으면 타입 확인 가능.
          • command + 클릭 하여 iterable.dart의 내용 확인.
            • MappedIterable 클래스를 리턴해주는 것을 확인할 수 있음.
        • items.map을 final iterable 변수에 담아줌.
          • if(response.statusCode == 200) { Map<String, dynamic> map = jsonDecode(response.body); final items = List.from(map['items']); final iterable = items.map((e) { return Book.fromJson(e); });
        • 여기까지는 실제로 이 items 배열에서 Book.fromJson 생성자를 통해 객체로 변환이 되지는 않는다.
          • 여기까지 입력한 것은 “MappedIterable이라는 클래스에 items.map() 함수를 넘겨줄테니 나중에 요청하면 그때 리스트(items)들을 하나씩 반복문을 돌면서 내가 넘겨준 함수를 실행시켜서 새로운 리스트로 돌려줘.”라는 내용임.
          • 요약: MappedIterable이라는 클래스에 함수(e)를 넘겨주고 요청 시 이 함수(e) 기반으로 이 리스트 items를 하나씩 for문 돌면서 함수실행시켜서 개체변환해서 새로운 리스트에 담아줌.
          • 리스트로 변환된 상태 X
        • iterable에서 toList를 호출할 때 → MappedIterable 클래스 내에서 넘겨받은 함수(e)를 가지고 for문 돌면서 개체를 변환하게 됨. ⇒ final list 로 변수 담아주기.
          • if(response.statusCode == 200) { Map<String, dynamic> map = jsonDecode(response.body); final items = List.from(map['items']); final iterable = items.map((e) { return Book.fromJson(e); }); final list = iterable.toList(); }
          • final list의 타입을 확인하면 : List<Book> 임을 확인할 수 있다.
        • final list를 return 해주자.
          • if(response.statusCode == 200) { Map<String, dynamic> map = jsonDecode(response.body); final items = List.from(map['items']); final iterable = items.map((e) { return Book.fromJson(e); }); final list = iterable.toList(); return list; }
          • 이때 return 하면 오류가 발생함. why?
            • → 우리가 return 하려는 함수의 타입은 List<Book>인데, 현재 searchBooks의 함수 타입은 void 이기 때문.
        • searchBooks 의 타입을 List<Book>으로 수정한다.
          • → async 가 걸려 있으므로 이 타입을 Future로 감싸준다.
            import 'dart:convert'; import 'package:book_search/data/models/book.dart'; import 'package:http/http.dart'; class BookRepository { Future<List<Book>> searchBooks(String query) async { final clinent = Client(); final response = await clinent.get(Uri.parse( 'https://openapi.naver.com/v1/search/book.json?query=$query'), headers: { 'X-Naver-Client-Id': 'reRrRQXlGEgsdFD3YVfe', 'X-Naver-Client-Secret': 'IyHwQEOGxH', } ); // Get 요청 시 성공 => 200 // 응답코드가 200일 때 -- body 데이터를 jsonDecode 함수 사용해서 map으로 바꾼 후 List<Book> 으로 반환 if(response.statusCode == 200) { Map<String, dynamic> map = jsonDecode(response.body); final items = List.from(map['items']); final iterable = items.map((e) { return Book.fromJson(e); }); final list = iterable.toList(); return list; } // 200이 아닐 때 -- 빈 리스트 반환 response.body; response.statusCode; } }
        • 여기까지 하면 여전히 searchBooks에 오류가 표시되는데, 이것은 if문 안에서만 return해주고 있기 때문임. status 코드가 200이 아닐 때의 return 까지 설정해주면 해결됨.
      3. 응답코드가 200이 아닐 때: 빈 리스트로 반환
        1. // 200이 아닐 때 -- 빈 리스트 반환 return [];
          아래에 적어 둔 response.body; 와 response.statusCode; 는 더 이상 사용되지 않으므로 삭제한다.
           
여기까지하면 레포지토리 작성 완료. 📍git push 필수.
 

레포지토리 테스트

테스트 폴더에 book_repository_test.dart 파일 생성
import 'package:book_search/data/repositories/book_repository.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { test('BookRepository test', () async { BookRepository bookRepository = BookRepository(); final books = await bookRepository.searchBooks('harry'); expect(books.isEmpty, false); for (var book in books) { print(book.toJson()); } }); }
bookRepository 변수에 searchBooks 메서드 적용하여 ‘harry’라는 키워드를 검색한 결과를 List<Book> 타입의 books 리스트에 담은 것.
→ 아까 ‘harry’라고 썬더 클라이언트에서 검색했을 때 나온 결과 값이 있으므로 books의 리스트는 비어 있으면 안 됨. books.isEmpty라는 메서드의 예상값이 false라고 넣었을 때 좌우가 일치하면 문제 없음.
→ print(book.toJson());을 활용하여 ‘harry’라고 검색했을 때 나오는 JSON 목록들을 다시 확인함.
 
테스트 완료.
📍git push.
Share article

나새끼메이커