앱에 상호작용 추가하기
앱에 상호작용 추가하기
cli 애플리케이션에 간단한 명령을 추가해 봐요. 제어 흐름(control flow), 컬렉션, 변수, 함수 등 Dart 문법의 기본기를 배워 봐요.
본문
이 장에서는 Dart 문법을 직접 연습해 볼 거예요. 사용자 입력을 읽는 방법, 사용법 정보를 출력하는 방법, 그리고 기본적인 명령줄 상호작용을 만드는 방법을 배워 봐요.
if문으로 명령 처리기(handler)를 만들어요.- 리스트와 변수를 다뤄 봐요.
- 코드에서 null 가능성(nullability)을 고려해요.
- 함수를 정의하고 호출해 봐요.
- 사용자 입력을 읽고 처리해요.
사전 준비 (Prerequisites)
이 장을 시작하기 전에 다음을 확인해 주세요:
- 1장을 마치고 동작하는 Dart 개발 환경을 갖췄는지 확인해요.
- 기본 프로그래밍 개념(변수, 데이터 타입, 제어 흐름)에 익숙한지 확인해요.
작업 (Tasks)
Dartpedia 명령줄 애플리케이션에 기본 기능 몇 가지를 추가하고, 그 과정에서 Dart 문법을 탐구해 봐요.
작업 1: version 및 help 명령 구현하기 (Implement version and help commands)
cli/bin/cli.dart에version명령 구현하기: CLI의 현재 버전을 출력하는version명령을 처리하는 로직을 추가해요. 첫 번째 인자가version인지 확인하는if문을 사용해요.version상수도 필요해요.
먼저 main 함수 위에 버전용 const 변수를 선언해 주세요. const 변수의 값은 설정된 후에는 절대 바뀌지 않아요:
const version = '0.0.1'; // Add this line
다음으로 version 인자를 확인하도록 main 함수를 수정해 주세요:
void main(List<String> arguments) {
if (arguments.isEmpty) {
print('Hello, Dart!');
} else if (arguments.first == 'version') {
print('Dartpedia CLI version $version');
}
}
$version 문법은 문자열 보간(string interpolation)이라고 해요. 변수 이름 앞에 $ 기호를 붙여서 변수의 값을 문자열 안에 직접 넣을 수 있게 해 주죠.
version명령 테스트하기:version인자로 애플리케이션을 실행해 주세요:
dart bin/cli.dart version
이제 다음과 같이 보일 거예요:
Dartpedia CLI version 0.0.1
인자 없이 앱을 실행하면 여전히 "Hello, Dart!"가 보여요.
printUsage함수 추가하기: 출력을 더 사용자 친화적으로 만들기 위해 사용법 정보를 표시하는 별도 함수를 만들어요. 이 함수는main함수 밖, 아래에 배치해 주세요:
void printUsage() { // Add this new function
print(
"The following commands are valid: 'help', 'version', 'search <ARTICLE-TITLE>'"
);
}
search는 결국 Wikipedia에서 검색하게 될 명령이에요.
help명령 구현하고main다듬기: 이제else if문으로help명령을 통합하고, 기본 동작을 정리해printUsage함수를 호출하게 해요.
main 함수를 이렇게 수정해 주세요:
void main(List<String> arguments) {
if (arguments.isEmpty || arguments.first == 'help') {
printUsage(); // Change this from 'Hello, Dart!'
} else if (arguments.first == 'version') {
print('Dartpedia CLI version $version');
} else {
printUsage(); // Catch-all for any unrecognized command.
}
}
-
if/else구조와 변수 이해하기: 이제main함수에 제어 흐름을 구현했으니, 추가된 코드를 살펴볼게요. -
arguments.isEmpty는 명령줄 인자가 하나도 제공되지 않았는지 확인해요. -
arguments.first는 가장 첫 번째 인자에 접근해요. 이걸 명령으로 사용하고 있어요. -
version은const로 선언돼요. 값이 컴파일 타임에 알려져 있고 런타임에 바꿀 수 없다는 뜻이에요. -
arguments는 일반(비상수) 변수예요. 내용이 사용자 입력에 따라 런타임에 바뀔 수 있기 때문이에요.
help 인자로 애플리케이션을 실행해 주세요. 사용법 정보가 출력되는 게 보일 거예요:
dart bin/cli.dart help
또한 인자 없이 실행해 보세요:
dart bin/cli.dart
계속 사용법 정보가 표시되는 걸 볼 수 있어요. 이 시점에서는 정의하지 않은 어떤 명령도 사용법 정보를 출력해요. 지금은 예상된 동작이에요.
작업 2: search 명령 구현하기 (Implement the search command)
다음으로 문서 제목을 입력으로 받는 기본 search 명령을 구현해 봐요. 이 기능을 만들면서 List 조작, null 확인, 문자열 보간을 다루게 돼요.
main에search명령 통합하기: 먼저cli/bin/cli.dart의main함수를 수정해search명령을 처리하는else if분기를 포함시켜 주세요. 지금은 자리 표시자 메시지만 출력해요:
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 == 'search') {
// Add this new block:
print('Search command recognized!');
} else {
printUsage();
}
}
- 새 명령 테스트하기:
search명령으로 애플리케이션을 실행해 주세요:
dart bin/cli.dart search
다음과 같이 보여야 해요:
Search command recognized!
searchWikipedia함수 정의하기:search명령은 결국searchWikipedia라는 함수를 호출해서 애플리케이션의 핵심 로직을 실행할 거예요. 지금은searchWikipedia가search명령과 함께 전달된 인자들을 출력하게 해 봐요. 이 새 함수는main아래에 배치해 주세요:
// ... (your existing main function)
void searchWikipedia(List<String>? arguments) { // Add this new function and add ? to arguments type
print('searchWikipedia received arguments: $arguments');
}
// ... (your existing printUsage() function)
앞선 코드에서 눈여겨볼 점:
List<String>? arguments는arguments리스트 자체가null일 수 있다는 뜻이에요.
참고: Dart는 건전한 null 안전성(sound null safety)을 강제해요. 변수가 null일 수 있다는 것을 명시적으로 밝혀야 해요. nullable로 표시되지 않은 변수는 프로덕션에서도 결코 null이 아님이 보장돼요. null 안전성의 목적은 여러분 코드에서 null을 전혀 쓰지 못하게 막는 게 아니에요. 값이 없다는 것을 나타내는 게 유용할 수 있으니까요. 오히려 null 가능성을 고려하도록 강제해서 더 조심하게 만드는 거예요. 분석기(analyzer)와 함께 이것은 프로그래밍에서 가장 흔한 런타임 크래시 중 하나인 null-포인터 오류를 예방하는 데 도움을 줘요.
main함수에서searchWikipedia함수 호출하기: 이제main의search명령 블록을 수정해searchWikipedia를 호출하고,search명령 뒤에 오는 인자들을 전달해 주세요.arguments.sublist(1)을 사용해 두 번째부터 모든 인자를 가져와요.search뒤에 인자가 없으면searchWikipedia에null을 전달해요:
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 == 'search') {
// Add this new block:
final inputArgs = arguments.length > 1 ? arguments.sublist(1) : null;
searchWikipedia(inputArgs);
} else {
printUsage();
}
}
앞선 코드에서 눈여겨볼 점:
-
final변수는 한 번만 설정할 수 있고, 코드에서 다시 바꿀 의도가 없을 때 사용해요. -
arguments.sublist(1)는arguments리스트의 첫 번째 요소(즉search) 다음부터 모든 요소를 담은 새 리스트를 만들어요. -
arguments.length > 1 ? ... : null;은 조건부(삼항) 연산자예요.search명령 뒤에 인자가 제공되지 않으면inputArgs가null이 되도록 보장해요. 이는searchWikipedia의arguments파라미터인List<String>?의 동작과 일치해요. -
인자와 함께
searchWikipedia테스트하기: 명령줄에서 테스트 문서 제목으로 애플리케이션을 실행해 주세요:
dart bin/cli.dart search Dart Programming
다음과 같이 보여야 해요:
searchWikipedia received arguments: [Dart, Programming]
이번에는 추가 인자 없이 같은 명령을 실행해 보세요:
dart bin/cli.dart search
다음과 같이 보여야 해요:
searchWikipedia received arguments: null
stdin명령으로 누락된 문서 제목과 사용자 입력 처리하기: 사용자가 명령줄에 문서 제목을 넣지 않으면 prompt를 띄워 물어보는 게 더 사용자 친화적이에요. 여기에는stdin.readLineSync()를 사용해요.
먼저 cli/bin/cli.dart 파일 맨 위에 필요한 import를 추가해 주세요:
import 'dart:io'; // Add this line at the top
dart:io는 Dart SDK의 핵심 라이브러리로, 파일, 디렉터리, 소켓, HTTP 클라이언트와 서버 등을 다루는 API를 제공해요.
이제 searchWikipedia 함수를 갱신해 주세요:
void searchWikipedia(List<String>? arguments) {
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.');
// Await 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('Current article title: $articleTitle');
}
앞선 코드 블록은 몇 가지 핵심 개념을 소개해요:
final String articleTitle변수를 선언해요. 정적 분석이articleTitle이String이 되고 null이 아닐 것임을 감지하게 해 줘요.if/else문이 검색용 명령줄 인자가 제공됐는지 확인해요.- 인자가 없으면 사용자에게 물어보고,
stdin.readLineSync()로 입력을 읽으며, 입력이 없을 때를 안전하게 처리해요. - 인자가 있으면
arguments.join(' ')으로 결합해 하나의 검색 문자열로 만들어요.
앞선 코드에서 눈여겨볼 점:
-
stdin.readLineSync() ?? ''는 사용자 입력을 읽어요.stdin.readLineSync()는 null을 반환할 수 있는데, null-결합 연산자(??)가 입력이 null일 때 빈 문자열('')을 대체값으로 제공해요. 변수가 null이 아닌 문자열임을 보장하는 간결한 방법이에요. -
arguments.join(' ')는arguments리스트의 모든 요소를 공백을 구분자로 사용해 하나의 문자열로 합쳐요. 예를 들어['Dart', 'Programming']은"Dart Programming"이 돼요. 여러 단어로 된 명령줄 입력을 하나의 검색 구문으로 취급하는 데 아주 중요해요. -
Dart 정적 분석은
articleTitle이 print 문이 실행될 때 초기화가 보장됨을 감지할 수 있어요. 함수 본문에서 어떤 경로를 타든 변수는 non-nullable이에요. -
searchWikipedia를 마무리해 모의 검색 결과 출력하기:searchWikipedia를 갱신해 프로그램이 무언가를 찾은 것처럼 보이는 메시지를 표시해요. 지금 당장 모든 것을 실제로 만들지 않고도 완성된 프로그램이 무엇을 할지 확인하는 데 도움이 돼요. 이 메시지는 프로그램을 실행할 때 검색 쿼리를 포함할 때만 볼 수 있어요.
예: dart bin/cli.dart search Dart Programming.
void searchWikipedia(List<String>? arguments) {
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.');
// Await 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")');
}
- 두 시나리오로 최종 테스트 실행하기:
이제 문서 시뮬레이션을 설정했으니 searchWikipedia 함수를 몇 가지 다른 방식으로 테스트해 볼게요:
dart bin/cli.dart search Dart Programming
다음과 같이 보여야 해요:
Looking up articles about "Dart Programming". Please wait.
Here ya go!
(Pretend this is an article about "Dart Programming")
인자 없이 실행해 보세요(prompt가 뜨면 "Flutter Framework"를 입력하세요):
dart bin/cli.dart search
Please provide an article title.
Flutter Framework
이제 사용자 입력 처리를 갖춘 기본 search 명령을 성공적으로 만들었어요. 출력에서 여러 단어로 된 명령줄 입력을 하나의 검색 구문으로 올바르게 취급해요.
복습 (Review)
배운 내용 (What you accomplished)
이번 강의에서 만들고 배운 내용을 정리해 드릴게요.
if와else문으로 명령 처리기 만들기:help,version,search같은 서로 다른 명령들을 라우팅하는 데if와else문을 사용했어요. 이 제어 흐름 패턴은 사용자 입력에 반응하는 대화형 CLI 애플리케이션을 만드는 기본이에요.- 리스트와 변수 다루기:
var,final,const로 변수를 선언하고,.isEmpty,.first,.sublist(),.join()으로 리스트를 다루는 법을 배웠어요. 문자열 리터럴에 값을 넣기 위해 문자열 보간($variable)도 사용했어요. - 코드에서 null 가능성 고려하기: nullable 타입(
?), null 확인, 그리고 표현식이null일 수 있을 때 기본값을 제공하는 null-결합 연산자(??)로 Dart의 건전한 null 안전성을 받아들였어요. - 함수 정의하고 호출하기: 코드를 재사용 가능한 단위로 정리하기 위해
printUsage()와searchWikipedia()함수를 만들었어요. 재사용 로직을 함수로 분리하면main함수를 깔끔하게 유지하고 코드를 읽고 관리하기 쉬워져요. - 사용자 입력 읽고 처리하기:
dart:io의stdin.readLineSync()로 터미널에서 입력을 읽었어요. 이것은 명령줄 인자의 대안이나 추가로 대화형 prompt를 가능하게 해 줘요.
퀴즈 (Quiz)
이해도를 확인해 봐요 (Check your understanding)
Q1. 컴파일 타임에 값이 알려진 Dart 상수 변수를 선언하는 데 사용하는 키워드는 무엇인가요?
const— 맞아요!const는 컴파일 타임 상수를 선언해요. 값은 컴파일 타임에 알려져 있어야 하고 결코 바뀌지 않아요.var— 아니에요.var로 선언한 변수는 다시 할당할 수 있고 컴파일 타임에 알려질 필요가 없어요.final— 아니에요.final은 한 번만 초기화할 수 있는 변수를 만들지만, 그 값은 컴파일 타임이 아니라 런타임에 결정돼요.static— 아니에요.static은 상수 선언이 아니라 클래스 수준 멤버에 사용돼요. 다만 클래스 수준 상수도 반드시static이어야 해요.
Q2. CLI 애플리케이션에서 stdin.readLineSync()의 주된 목적은 무엇인가요?
- 사용자로부터 한 줄의 텍스트 입력을 읽는 것. — 맞아요!
stdin.readLineSync()는 표준 입력에서 텍스트 한 줄을 읽어 프로그램이 사용자 입력을 받을 수 있게 해 줘요. - 콘솔에 출력을 인쇄하는 것. — 아니에요. 출력 인쇄는
print(),stdout.write()같은 다른 메서드로 해요. - 명령을 실행하는 것. — 아니에요. 명령 실행은
Process.run()같은 메서드로 해요. - 파일이 존재하는지 확인하는 것. — 아니에요. 파일 존재 확인은
File.existsSync()같은 메서드로 해요.
Q3. arguments가 ['search', 'Dart', 'Programming']일 때 arguments.sublist(1)은 무엇을 반환하나요?
['Dart', 'Programming']— 맞아요!sublist(1)은 첫 번째 요소를 건너뛰고 인덱스 1부터 모든 요소를 담은 새 리스트를 만들어요.['search']— 아니에요.sublist(1)은 인덱스 1(두 번째 요소)부터 시작하는 요소들을 반환하지, 첫 번째 요소만 반환하지는 않아요.['search', 'Dart']— 아니에요. 이 답은'search'를 포함하는데,sublist(1)은 인덱스 1부터 시작하므로 인덱스 0의 요소는 제외해요.['Programming']— 아니에요. 이것은 마지막 요소뿐이에요. 인자1은 시작 인덱스를 지정하지, 끝에서 몇 개를 건너뛸지를 지정하는 게 아니에요.