앱에 상호작용 추가하기

앱에 상호작용 추가하기

cli 애플리케이션에 간단한 명령을 추가해 봐요. 제어 흐름(control flow), 컬렉션, 변수, 함수 등 Dart 문법의 기본기를 배워 봐요.

출처: Add interactivity to your app

본문

이 장에서는 Dart 문법을 직접 연습해 볼 거예요. 사용자 입력을 읽는 방법, 사용법 정보를 출력하는 방법, 그리고 기본적인 명령줄 상호작용을 만드는 방법을 배워 봐요.

  • if 문으로 명령 처리기(handler)를 만들어요.
  • 리스트와 변수를 다뤄 봐요.
  • 코드에서 null 가능성(nullability)을 고려해요.
  • 함수를 정의하고 호출해 봐요.
  • 사용자 입력을 읽고 처리해요.

사전 준비 (Prerequisites)

이 장을 시작하기 전에 다음을 확인해 주세요:

  • 1장을 마치고 동작하는 Dart 개발 환경을 갖췄는지 확인해요.
  • 기본 프로그래밍 개념(변수, 데이터 타입, 제어 흐름)에 익숙한지 확인해요.

작업 (Tasks)

Dartpedia 명령줄 애플리케이션에 기본 기능 몇 가지를 추가하고, 그 과정에서 Dart 문법을 탐구해 봐요.

작업 1: version 및 help 명령 구현하기 (Implement version and help commands)

  • cli/bin/cli.dartversion 명령 구현하기: 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는 가장 첫 번째 인자에 접근해요. 이걸 명령으로 사용하고 있어요.

  • versionconst로 선언돼요. 값이 컴파일 타임에 알려져 있고 런타임에 바꿀 수 없다는 뜻이에요.

  • arguments는 일반(비상수) 변수예요. 내용이 사용자 입력에 따라 런타임에 바뀔 수 있기 때문이에요.

help 인자로 애플리케이션을 실행해 주세요. 사용법 정보가 출력되는 게 보일 거예요:

dart bin/cli.dart help

또한 인자 없이 실행해 보세요:

dart bin/cli.dart

계속 사용법 정보가 표시되는 걸 볼 수 있어요. 이 시점에서는 정의하지 않은 어떤 명령도 사용법 정보를 출력해요. 지금은 예상된 동작이에요.

작업 2: search 명령 구현하기 (Implement the search command)

다음으로 문서 제목을 입력으로 받는 기본 search 명령을 구현해 봐요. 이 기능을 만들면서 List 조작, null 확인, 문자열 보간을 다루게 돼요.

  • mainsearch 명령 통합하기: 먼저 cli/bin/cli.dartmain 함수를 수정해 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라는 함수를 호출해서 애플리케이션의 핵심 로직을 실행할 거예요. 지금은 searchWikipediasearch 명령과 함께 전달된 인자들을 출력하게 해 봐요. 이 새 함수는 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>? argumentsarguments 리스트 자체가 null일 수 있다는 뜻이에요.

참고: Dart는 건전한 null 안전성(sound null safety)을 강제해요. 변수가 null일 수 있다는 것을 명시적으로 밝혀야 해요. nullable로 표시되지 않은 변수는 프로덕션에서도 결코 null이 아님이 보장돼요. null 안전성의 목적은 여러분 코드에서 null을 전혀 쓰지 못하게 막는 게 아니에요. 값이 없다는 것을 나타내는 게 유용할 수 있으니까요. 오히려 null 가능성을 고려하도록 강제해서 더 조심하게 만드는 거예요. 분석기(analyzer)와 함께 이것은 프로그래밍에서 가장 흔한 런타임 크래시 중 하나인 null-포인터 오류를 예방하는 데 도움을 줘요.

  • main 함수에서 searchWikipedia 함수 호출하기: 이제 mainsearch 명령 블록을 수정해 searchWikipedia를 호출하고, search 명령 뒤에 오는 인자들을 전달해 주세요. arguments.sublist(1)을 사용해 두 번째부터 모든 인자를 가져와요. search 뒤에 인자가 없으면 searchWikipedianull을 전달해요:
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 명령 뒤에 인자가 제공되지 않으면 inputArgsnull이 되도록 보장해요. 이는 searchWikipediaarguments 파라미터인 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 변수를 선언해요. 정적 분석이 articleTitleString이 되고 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)

이번 강의에서 만들고 배운 내용을 정리해 드릴게요.

  • ifelse 문으로 명령 처리기 만들기: help, version, search 같은 서로 다른 명령들을 라우팅하는 데 ifelse 문을 사용했어요. 이 제어 흐름 패턴은 사용자 입력에 반응하는 대화형 CLI 애플리케이션을 만드는 기본이에요.
  • 리스트와 변수 다루기: var, final, const로 변수를 선언하고, .isEmpty, .first, .sublist(), .join()으로 리스트를 다루는 법을 배웠어요. 문자열 리터럴에 값을 넣기 위해 문자열 보간($variable)도 사용했어요.
  • 코드에서 null 가능성 고려하기: nullable 타입(?), null 확인, 그리고 표현식이 null일 수 있을 때 기본값을 제공하는 null-결합 연산자(??)로 Dart의 건전한 null 안전성을 받아들였어요.
  • 함수 정의하고 호출하기: 코드를 재사용 가능한 단위로 정리하기 위해 printUsage()searchWikipedia() 함수를 만들었어요. 재사용 로직을 함수로 분리하면 main 함수를 깔끔하게 유지하고 코드를 읽고 관리하기 쉬워져요.
  • 사용자 입력 읽고 처리하기: dart:iostdin.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은 시작 인덱스를 지정하지, 끝에서 몇 개를 건너뛸지를 지정하는 게 아니에요.

더 알아보기