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

레포지토리 작성
데이터를 가지고 오는 레포지토리에 Book 레포지토리를 만들어서 구현해보자.
우선 플러터 프로젝트 내에서 HTTP 요청을 하기 위해서 http 패키지를 추가한다.
http 패키지 : 앱 내에서 http 요청을 하기 위한 다트 패키지> flutter pub add http
프로젝트의 repostories 폴더 하위에
book_repository.dart 파일을 생성한다.searchBooks라는 함수 매서드를 활용하는 BookRepository 클래스를 만들어 준다.

http 클라이언트 사용법
- 클라이언트 객체 생성: http 패키지 내의 Client 클래스를 활용할 수 있도록 자동완성을 통해 import할 것.
- client.get 메서드 안에 Uri.parse() 메서드 넣기. parse 메서드에 api url 넣고, searchBooks 메서드와 연결될 수 있도록 $query 파라미터로 연결할 것.
- 헤더 세팅: 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',
}
);
}
}- 현재 client.get의 타입은
Future<Response>임.
통신의 기본은 비동기임. 우리는 이걸 동기처럼 안에서 처리할 것이기 때문에
async와 await를 넣어준다.아직 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',
}
);
}
}- response 객체 세팅
response.body: http 요청 후 응답 데이터를 가져옴response.statusCode: http 요청 후 응답 코드를 가져옴- 응답코드가 200일 때 : body 데이터를 jsonDecode 함수 사용해서 map으로 바꾼 후 List<Book>으로 반환
- 현재 response.body의 형태
- 이하의 값들을 key로 갖는 문자열
- lastBuildDate
- total
- start
- display
- items
- ⇒ jsonDecode를 통해 Map타입의 데이터로 만듦.
- 썬더 클라이언트에서 요청했던 값을 보면 오브젝트들의 리스트들이 items라는 키값 안에 들어가 있음.
- 이것만 따로 불러와서 처리할 것.
- 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 변수에 담아줌.
- 여기까지는 실제로 이 items 배열에서 Book.fromJson 생성자를 통해 객체로 변환이 되지는 않는다.
- 여기까지 입력한 것은 “MappedIterable이라는 클래스에
items.map()함수를 넘겨줄테니 나중에 요청하면 그때 리스트(items)들을 하나씩 반복문을 돌면서 내가 넘겨준 함수를 실행시켜서 새로운 리스트로 돌려줘.”라는 내용임. - 요약: MappedIterable이라는 클래스에 함수(
e)를 넘겨주고 요청 시 이 함수(e) 기반으로 이 리스트 items를 하나씩 for문 돌면서 함수실행시켜서 개체변환해서 새로운 리스트에 담아줌. - 리스트로 변환된 상태 X
- 이
iterable에서 toList를 호출할 때 → MappedIterable 클래스 내에서 넘겨받은 함수(e)를 가지고 for문 돌면서 개체를 변환하게 됨. ⇒ final list 로 변수 담아주기. - final list의 타입을 확인하면 : List<Book> 임을 확인할 수 있다.
- final list를 return 해주자.
- 이때 return 하면 오류가 발생함. why?
- searchBooks 의 타입을 List<Book>으로 수정한다.
- 여기까지 하면 여전히 searchBooks에 오류가 표시되는데, 이것은 if문 안에서만 return해주고 있기 때문임. status 코드가 200이 아닐 때의 return 까지 설정해주면 해결됨.
- 응답코드가 200이 아닐 때: 빈 리스트로 반환
// 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;{
"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..."
},
{ ...→ map[”items”]는 도서 정보의 Set을 요소로 하는 List의 모습을 하고 있으나 그 타입은 String임. ⇒ List.from(map[’items’])를 통해 String을 List로 만들어주고, 그 리스트를 final items에 넣어주는 것. ⇒ final items의 타입은 List<dynamic>임. 여기서 다시 설명하자면, items의 타입은 리스트인데, 그 리스트의 요소들을 보자면 도서 각 권의 정보를 문자열 형태로 담고 있는 Map의 모양새를 갖고 있음.
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);
}); 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);
});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();
} 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 하려는 함수의 타입은 List<Book>인데, 현재 searchBooks의 함수 타입은 void 이기 때문.
→ 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;
}
} // 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