비동기 코드 작성하기
비동기 코드 작성하기
Dart의 비동기 프로그래밍과 HTTP 요청을 만드는 방법을 배워 봐요.
출처: 원문
본문
이 챕터에서는 Dart의 비동기 프로그래밍을 살펴보며, 여러분의 애플리케이션이 여러 작업을 동시에 수행할 수 있게 해 볼 거예요. http 패키지를 사용해 인터넷에서 위키백과 기사 요약을 가져오는 방법도 배우게 돼요.
이번 장에서 할 일
- future와 await로 비동기 코드 작성하기
- 외부 패키지 설치하고 사용하기
- Wikipedia API에서 실시간 데이터 가져오기
사전 준비
- 기본 Dart 문법과 커맨드라인 상호작용을 다룬 2장을 마쳤을 것.
dartpedia프로젝트가 설정되어 있어야 해요. - 데이터를 가져오는 수단으로서의 API(Application Programming Interface) 개념에 익숙할 것.
Tasks
이 챕터에서는 기존 dartpedia CLI 애플리케이션을 수정해서 http 패키지와 비동기 프로그래밍 기법을 사용해 기사 요약을 가져오고 표시하게 만들 거예요.
Task 1: http 의존성 추가하기
HTTP 요청을 만들기 전에 프로젝트에 http 패키지를 의존성으로 추가해야 해요.
-
프로젝트 안의
dartpedia/cli/pubspec.yaml파일을 여세요. 이 파일을 pubspec이라고 부르는데, Dart 프로젝트의 메타데이터와 의존성(http패키지 같은 것들), 그리고 asset을 관리해요. -
dependencies섹션을 찾으세요. -
dependencies아래에http: ^1.4.0(또는 최신 안정 버전)을 추가하세요.^기호는 호환되는 버전을 사용할 수 있게 해 줘요.
dependencies:
http: ^1.4.0
-
pubspec.yaml파일을 저장하세요. -
dartpedia/cli디렉토리에서 터미널에dart pub get을 실행하세요. 이 명령은 새로 추가한 의존성을 가져와 프로젝트에서 사용할 수 있게 해 줘요.
다음과 비슷한 출력이 보일 거예요:
Resolving dependencies...
Downloading packages...
+ http 1.4.0
lints 5.1.1 (6.0.0 available)
Changed 1 dependency!
1 package has newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.
Task 2: http 패키지 import하기
이제 http 패키지를 추가했으니, 그 기능을 사용하려면 Dart 파일에 import해야 해요.
-
cli/bin/cli.dart파일을 여세요. -
기존
dart:ioimport와 함께 파일 맨 위에 다음import문을 추가하세요:
import 'dart:io';
import 'package:http/http.dart' as http; // Add this line
이 줄은 http 패키지를 import하고 별칭 http를 부여해요. 이렇게 하면 http.을 사용해 http 패키지 안의 클래스와 함수를 참조할 수 있어요(예: http.Client, http.get). as http 부분은 다른 import된 라이브러리에도 비슷한 이름의 클래스나 함수가 있을 때 이름 충돌을 피하기 위한 표준 관례예요.
Task 3: getWikipediaArticle 함수 구현하기
이제 외부 API에서 데이터를 가져오는 getWikipediaArticle이라는 새 함수를 만들어 볼게요. 네트워크 요청은 비동기 연산이기 때문에 이 함수는 async가 될 거예요.
- 함수 시그니처를 정의하세요:
main함수(와printUsage함수) 아래에 다음 함수 시그니처를 추가하세요.
// ... (your existing printUsage() function)
Future<String> getWikipediaArticle(String articleTitle) async {
//You'll add more code here soon
}
위 코드에서 주목할 점:
-
Future<String>반환 타입은 이 함수가 결국엔String결과를 만들어 내지만, 비동기 연산이기 때문에 즉시는 아니라는 뜻이에요. -
async키워드는 함수를 비동기로 표시해 함수 안에서await를 사용할 수 있게 해 줘요. -
API URL을 구성하세요: 새
getWikipediaArticle함수 안에Uri객체를 만드세요.Uri는 기사 요약을 얻기 위해 호출할 Wikipedia API의 엔드포인트를 나타내요.
getWikipediaArticle 함수 안에 다음 줄을 추가하세요:
Future<String> getWikipediaArticle(String articleTitle) async {
final url = Uri.https(
'en.wikipedia.org', // Wikipedia API domain
'/api/rest_v1/page/summary/$articleTitle', // API path for article summary
);
// ...
}
- HTTP 요청을 만들고 응답을 처리하세요: 이제
package:http의 최상위get함수를 사용해 방금 구성한 URL로 HTTPGET요청을 만들어 보세요.await키워드는get호출이 완료되어http.Response객체를 반환할 때까지getWikipediaArticle의 실행을 멈춰요.
요청이 끝나면 response.statusCode를 확인해 요청이 성공했는지(상태 코드 200은 OK를 뜻해요) 확인하세요. 성공하면 가져온 데이터(여기서는 raw JSON)가 담긴 response.body를 반환하고, 실패하면 정보가 담긴 오류 메시지를 반환해요.
getWikipediaArticle 안의 Uri 구성 뒤에 다음 줄을 추가하세요:
Future<String> getWikipediaArticle(String articleTitle) async {
final url = Uri.https(
'en.wikipedia.org',
'/api/rest_v1/page/summary/$articleTitle',
);
final response = await http.get(url); // Make the HTTP request
if (response.statusCode == 200) {
return response.body; // Return the response body if successful
}
// Return an error message if the request failed
return 'Error: Failed to fetch article "$articleTitle". Status code: ${response.statusCode}';
}
Task 4: API 호출을 searchWikipedia에 통합하기
API 호출을 searchWikipedia에 통합할 거예요. 이 함수가 wikipedia 명령을 처리하는 핵심 로직을 담게 돼요.
searchWikipedia를async로 갱신하세요:searchWikipedia함수를 찾아 시그니처를async로 갱신하세요. 이제 비동기 연산을 수행할 것이기 때문이에요.
searchWikipedia 함수는 이제 이렇게 보여야 해요:
// ... (your existing main function)
void searchWikipedia(List<String>? arguments) async { // Added 'async'
final String articleTitle;
// If the user didn't pass in arguments, request an article title.
if (arguments == null || arguments.isEmpty) {
print('Please provide an article title.');
// Read input and provide a default empty string if the input is null.
articleTitle = stdin.readLineSync() ?? '';
} else {
// Otherwise, join the arguments into a single string.
articleTitle = arguments.join(' ');
}
print('Looking up articles about "$articleTitle". Please wait.');
print('Here ya go!');
print('(Pretend this is an article about "$articleTitle")');
}
// ... (your existing printUsage() function)
위 코드에서 주목할 점:
-
void searchWikipedia(List<String>? arguments) async: 함수가 이제async예요.getWikipediaArticle을 호출할 것이고, 그것도async함수라 그 결과를await해야 하기 때문에 필수적이에요. -
null이나 빈 문자열 입력을 거부하세요: 지금은 사용자가dart bin/cli.dart search를 실행하고 제목을 입력하지 않은 채 Enter를 누르면stdin.readLineSync()가 빈 문자열을 반환해요. 사용자가 Control+D나 Control+Z로 파일 끝(EOF) 신호를 보내면stdin.readLineSync()가null을 반환하고,?? ''가 빈 문자열로 대체해요. 어느 쪽이든 CLI는 빈 문자열을 기사 제목으로 해 Wikipedia에 가짜 요청을 해요.
?? '' 대체를, 유효한 입력이 없을 때 메시지를 출력하고 함수를 종료하는 검사로 바꾸세요.
void searchWikipedia(List<String>? arguments) async {
final String articleTitle;
if (arguments == null || arguments.isEmpty) {
print('Please provide an article title.');
// Read input without the `?? ''` fallback.
final inputFromStdin = stdin.readLineSync();
if (inputFromStdin == null || inputFromStdin.isEmpty) {
print('No article title provided. Exiting.');
return; // Exit the function if there's no valid input.
}
articleTitle = inputFromStdin;
} else {
// Otherwise, join the arguments into a single string.
articleTitle = arguments.join(' ');
}
print('Looking up articles about "$articleTitle". Please wait.');
print('Here ya go!');
print('(Pretend this is an article about "$articleTitle")');
}
getWikipediaArticle을 호출하고 결과를 출력하세요:searchWikipedia끝에 있는 두 개의 placeholderprint줄을getWikipediaArticle호출과 결과의 단일print로 바꾸세요.
void searchWikipedia(List<String>? arguments) async {
final String articleTitle;
if (arguments == null || arguments.isEmpty) {
print('Please provide an article title.');
final inputFromStdin = stdin.readLineSync();
if (inputFromStdin == null || inputFromStdin.isEmpty) {
print('No article title provided. Exiting.');
return;
}
articleTitle = inputFromStdin;
} else {
articleTitle = arguments.join(' ');
}
print('Looking up articles about "$articleTitle". Please wait.');
// Call the API and await the result.
var articleContent = await getWikipediaArticle(articleTitle);
print(articleContent); // Print the full article response (raw JSON for now)
}
위 코드에서 주목할 점:
await getWikipediaArticle(articleTitle):getWikipediaArticle은async함수이므로 그 결과를await해야 해요. 이는getWikipediaArticle이 반환한Future<String>이 기사 내용을 담은String으로 해결될 때까지searchWikipedia함수를 멈춰요.print(articleContent): 가져온 기사 요약을 raw JSON 문자열로 콘솔에 출력해요.
Task 5: main을 갱신해 searchWikipedia 호출하기
마지막으로 wikipedia 명령이 사용될 때 새 searchWikipedia 함수를 호출하도록 main 함수를 갱신해요.
main함수에서 현재search명령을 처리하는else if블록을 찾으세요. 명령 이름을search에서wikipedia로 바꾸고 함수 호출도 갱신하세요.
샘플 코드에서 main은 searchWikipedia 호출을 await하지 않아요. 즉 main 자체는 async로 표시할 필요가 없어요.
main 함수는 이제 이렇게 보여야 해요:
// ... (existing const version declaration and printUsage function)
void main(List<String> arguments) {
if (arguments.isEmpty || arguments.first == 'help') {
printUsage();
} else if (arguments.first == 'version') {
print('Dartpedia CLI version $version');
} else if (arguments.first == 'wikipedia') { // Changed to 'wikipedia'
// Pass all arguments *after* 'wikipedia' to searchWikipedia
final inputArgs = arguments.length > 1 ? arguments.sublist(1) : null;
searchWikipedia(inputArgs); // Call searchWikipedia (no 'await' needed here for main)
} else {
printUsage(); // Catch all for any unrecognized command.
}
}
arguments.sublist(1):arguments리스트에서 두 번째 요소(인덱스 1)부터 시작해 모든 요소를 추출해요. 이는wikipedia명령 자체를 효과적으로 제거해서searchWikipedia가 실제 기사 제목 인자만 받게 해 줘요.searchWikipedia(inputArgs):searchWikipedia를 직접 호출해요.main은searchWikipedia가 끝난 뒤에 할 일이 없으므로main에서await할 필요가 없어요. 따라서main은async일 필요가 없어요.
Task 6: 애플리케이션 실행하기
이제 http 요청을 구현하고 애플리케이션에 통합했으니 테스트해 볼게요.
- 터미널을 열고 다음 명령을 실행하세요:
dart run bin/cli.dart wikipedia "Dart_(programming_language)"
- 애플리케이션이 Wikipedia API에서 "Dart" 기사의 요약을 가져와 raw JSON 응답을 콘솔에 출력하는지 확인하세요. 대략 이런 모습일 거예요:
Looking up articles about "Dart_(programming_language)". Please wait.
{
"type": "standard",
"title": "Dart (programming language)",
"displaytitle": "<span class=\"mw-page-title-main\">Dart (programming language)</span>",
"namespace": {
"id": 0,
"text": ""
}
// ... (rest of the JSON output will be present but truncated here)
}
- 다음으로 인자 없이 실행해 보세요. 예를 들어 프롬프트가 뜨면 "Flutter_(software)"를 입력해 보세요.
dart run bin/cli.dart wikipedia
Please provide an article title.
Flutter_(software)
Looking up articles about "Flutter_(software)". Please wait.
{
"type": "standard",
"title": "Flutter (software)",
"displaytitle": "<span class=\"mw-page-title-main\">Flutter (software)</span>",
"namespace": {
"id": 0,
"text": ""
}
// ... (rest of the JSON output will be present but truncated here)
}
이제 외부 API에서 실제 데이터를 가져오는 기본 wikipedia 명령을 성공적으로 구현했어요!
Review
무엇을 해냈나요
이번 레슨에서 만들고 배운 내용을 정리해 볼게요.
-
future, async, await 숙달하기
Future는 나중에 사용할 수 있게 되는 값을 나타낸다는 걸 배웠어요.async키워드는 함수를 비동기로 표시하고,await는Future가 완료될 때까지 실행을 멈춰요. -
http 패키지를 의존성으로 추가하기
pubspec.yaml을 수정해http패키지를 추가하고,dart pub get을 실행해 가져온 뒤 별칭(as http)으로 import했어요. 이 단계를 따르면 pub.dev의 어떤 외부 패키지든 추가할 수 있어요. -
Wikipedia에서 실시간 데이터 가져오기
package:http의get함수로 HTTP GET 요청을 하고, URI를 구성하고, 상태 코드를 확인하고, 응답 본문을 반환하는getWikipediaArticle()을 만들었어요. 이제 여러분의 CLI가 웹에서 실제 데이터를 가져와요!
Quiz
이해를 확인해 봐요
Dart에서 함수를 비동기로 표시하는 데 쓰는 키워드는 무엇인가요?
async— 맞아요!async키워드는 함수를 비동기로 표시해 비동기 연산을 기다리기 위해await를 쓸 수 있게 해 줘요.await— 아니에요.await는 비동기 함수 안에서 결과를 기다리는 데 쓰여요. 함수 자체를 비동기로 표시하지는 않아요.future— 아니에요.Future는 나중에 오는 값을 나타내는 타입이지, 함수를 표시하는 키워드가 아니에요.asynchronous— 아니에요.asynchronous는 설명적이긴 하지만 Dart 키워드가 아니에요. Dart는 함수를 비동기로 표시하는 더 짧은 키워드를 사용해요.
Dart에서 Future<String>은 무엇을 나타내나요?
- 미래의 어떤 시점에 사용할 수 있게 되는 문자열 값 — 맞아요!
Future는 약속과 같아요. 아직은 없지만 비동기 연산(네트워크 요청 같은)이 끝나면 얻게 되는 값을 나타내요. null일 수도 있는 문자열 — 아니에요. nullable 문자열의 타입은String?이에요.Future<String>은 async 연산이 끝난 뒤 나중에 사용할 수 있게 되는 문자열 값을 나타내요.- 시간이 지나며 커질 수 있는 문자열 리스트 — 아니에요. 커질 수 있는 문자열 리스트는
List<String>이에요.Future<T>는 컬렉션이 아니라 T 타입의 단일 값을 감싸요. - 별도의 메모리 위치에 저장된 문자열 — 아니에요.
Future타입은 메모리 위치나 저장 방식과는 관련이 없어요. 일상 언어에서 "future"가 무슨 뜻인지 생각해 보세요.
Future 앞에 await를 두면 어떤 일이 벌어지나요?
Future가 완료될 때까지 함수를 멈추고, 그다음 풀린(unwrapped) 값을 반환한다 — 맞아요!await는Future가 완료될 때까지 함수의 실행을 멈춰요.- 너무 오래 걸리면
Future를 취소한다 — 아니에요.await는 아무것도 취소하거나 타임아웃을 설정하지 않아요. 연산이 얼마나 걸리든 그저 기다릴 뿐이에요. - 더 나은 성능을 위해 별도 스레드에서
Future를 실행한다 — 아니에요. Dart는 이런 방식으로 스레드를 자동 사용하지 않아요.await는 Dart의 단일 스레드 이벤트 루프 모델 안에서 동작해요. Future를 동기 연산으로 변환한다 — 아니에요. 연산은 여전히 비동기로 남아요.await는 코드 작성 방식을 바꾸지 실행 방식을 바꾸지 않아요.
await로 HTTP 응답을 기다리는 동안 애플리케이션의 다른 부분은 어떻게 되나요?
- 다른 비동기 연산과 이벤트 핸들러가 계속 실행될 수 있다 — 맞아요! 이것이 async 프로그래밍의 핵심 장점이에요. 한 연산을 기다리는 동안 Dart의 이벤트 루프가 다른 작업을 처리해 앱의 반응성을 유지해요.
- 응답이 올 때까지 모든 게 멈춘다 — 아니에요. 모든 게 멈춘다면 async는 아무 이점도 없을 거예요. 비동기 프로그래밍이 왜 존재하는지 생각해 보세요.
- Dart가 기다림을 처리하기 위해 자동으로 새 스레드를 시작한다 — 아니에요. Dart는 단일 스레드 이벤트 루프 모델을 사용해요.
await는 새 스레드를 만들지 않고 이 모델 안에서 동작해요. - 응답이 너무 오래 걸리면 프로그램이 종료된다 — 아니에요. 프로그램은 async 연산을 기다린다고 종료되지 않아요. 종료를 일으킬 내장 타임아웃은 없어요.
다음 레슨
다음 챕터에서는 코드를 재사용 가능한 라이브러리와 패키지로 정리하는 데 집중할 거예요. 커맨드라인 인자를 처리하는 별도의 패키지를 만들어 애플리케이션의 구조와 유지보수성을 개선하도록 리팩터링할 거예요.