코코아 앱 따라하기 – 2

지난 글에서 만든 TTS 앱을 조금 개선해보자. 이 앱에는 Start, Stop 두 개의 버튼이 있다. 그런데 두 버튼은 모두 활성화된 상태가 된다. 따라서 앱이 텍스트를 읽고 있지 않은 경우에도 stop 버튼을 클릭할 수 있고, 또 앱이 텍스트를 읽고 있는 중간에 start 버튼을 반복적으로 클릭할 수 있게 된다.

이 상황을 멋지게 해결하기 위해서는 버튼의 활성화 상태를 그 때 그 때 바꿔주는 것이다. 즉 앱이 읽는 중간에는 start 버튼이 비활성화 되어야 하고, 앱이 읽고 있지 않을 때에는 반대로 stop 버튼이 비활성화 되는 것이다.

버튼의 활성화 상태

버튼의 활성화 상태를 변경하는 방법에는 두 가지 방법이 있다.  우리가 앱에 추가한 Push Button은 NSButton 객체이다. 버튼 클래스는 enabled라는 프로퍼티를 가지고 있다.(BOOL 타입이다.) 따라서 -setEnabled: 를 호출해서 코드 상에서 활성화 상태를 변경하거나, 인터페이스 빌더의 속성 인스펙터에서 Enabled 값을 변경해줄 수 있다. 단, 인터페이스 빌더에서의 조작은 앱의 초기 상태만 결정할 뿐, 런타임에 동적으로 속성값을 변경하기 위해서는 아웃렛을 통한 프로퍼티 액세스를 해야한다.

stop 버튼의 초기상태

일단 stop 버튼의 초기 상태는 비활성화 상태여야 한다. 프로젝트를 열고 MainMenu.xib 파일을 선택하자. stop 버튼을 클릭하고 오른쪽 인스펙터에서 그림과 같이 속성 인스펙터 탭을 선택하고 Enabled 값을 체크 해제한다.

이 변경의 효과는 인터페이스 빌더 내에서 바로 확인할 수 있다. 그리고 AppController의 코드 상에서 이 값을 제어하기 위해서는 무엇이 필요할까? 바로 해당 클래스 내에서 두 버튼에 대한 참조점, 즉 아웃렛이 필요하다.

AppController.m 파일로 가서 @interface 부분에, 이전 시간에 했던 것과 같이 두 개의 NSButton에 대한 IBOutlet 을 정의한다.

@property (weak, nonatomic) IBOutlet NSButton* startButton; @property (weak, nonatomic) IBOutlet NSButton* stopButton;

그리고 -startSpeaking:-stopSpeaking:이 호출될 때 각각의 현재 상황에 맞게 버튼의 활성화 여부를 업데이트한다.

- (IBAction)startSpeaking:(id)sender
{
  NSString *words = [self.field stringValue];
  if([words length] > 0) {
    [self.speech startSpeaking:words];
    [self.startButton setEnabled:NO];
    [self.stopButton setEnabled:YES];
  }
}

- (IBAction)stopSpeaking:(id)sender
{
  [self.speech stopSpeaking];
  [self.startButton setEnabled:YES];
  [self.stopButton setEnabled:NO];
}

그리고, 잊지 말아야 할 것. 새로 추가한 아웃렛이 있으면 반드시 인터페이스 빌더에서 연결하는 처리를 해야한다. 연결하는 방법은 지난 시간에 언급했으니 생략하겠다. 연결까지 마무리했다면 다시 빌드하고 실행해보자.

읽기가 끝나는 시점을 알아내기

문제가 있다. 텍스트를 입력하고  Start 버튼을 누르면, 의도했던 대로 두 버튼의 상태가 변경되면서 음성이 흘러나온다. 그런데 문제는 stop  버튼을 클릭하지 않으면 읽기가 끝나도 버튼의 상태가 원래대로 돌아오지 않는 것이다.

음성합성기가 읽기를 끝내는 시점에 AppController에게 무언가 메시지를 보내어 읽기가 끝났다는 것을 알려주어야 할 것이다. 그런데 음성 합성기는 애플이 만든 클래스이고, 이 클래스는 우리가 만든 AppController라는 클래스를 본적도 없을 것이다. 그러면 어떻게 이 문제를 해결할 수 있을까?

애플은 이런 문제를 해결할 수 있도록 델리게이트 패턴1을 사용하라고 알려준다. 음성합성기인 NSSpeechSynthesizer 클래스도 delegate라는 프로퍼티를 가지고 있다. 이 프로퍼티의 타입은 id<NSSynthesizerDelegate> 인데, id는 타입에 무관한 클래스를 의미하므로, “타입이 무엇이든 NSSynthesizerDelegate라는 프로토콜을 따르고 있는 클래스“이면 된다는 것이다. 프로토콜은 실제 구현없이, 어떠한 메소드를 가지고 있을 것이라는 약속이므로 음성합성기는 그 자신의 델리게이트의 구체적인 클래스를 알지 못하더라도 특정한 이벤트가 발생하는 시점에 델리게이트에게 메시지를 보내는 것이다.

NSSpeechSynthesizerDelegate에는 음성 합성기가 말하기를 완료하거나 중단했을 때 호출받는 -speechSynthesizer:didFinishSpeaking: 이라는 메소드가 정의되어 있다.  AppController가  이 프로토콜을 따르도록 하고 해당 메소드를 구현하여 이 문제를 해결해 보자.

먼저 AppController.h 파일을 열고 다음과 같이 클래스 선언 부분 뒤에 <NSSpeechSynthesizerDelegate>를 추가해준다.

@interface AppController: NSObject <NSSpeechSynthesizerDelegate>
//                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

그리고 다시 AppController.m 파일로 돌아와서, @implementation 블럭 안에 다음과 같이 해당 메소드를 구현해주면 된다.

#pragma mark - NSSpeechSynthesizerDelegate
- (void)speechSynthesizer:(NSSpeechSynthesizer*)sender 
        didFinishSpeaking:(BOOL)finishedSpeaking
{
  // 이 때 finishedSpeaking이 YES이면 완료, NO이면 중단인데
  // 여기서는 굳이 따질 필요가 없다.
  [self.startButton setEnabled:YES];
  [self.stopButton setEnabled:NO];
}

그리고 이 메소드를 작성했다면 -stopSpeaking:에서 버튼의 활성화 여부를 조정하기 위해 추가한 코드는 삭제하도록 한다. 이제 다시 앱을 빌드하고 실행해보자.  stop버튼을 눌러서 중단하거나, 읽기가 끝났을 때에는 항상 위 델리게이트 메소드를 호출받기 때문에 항상 자동으로 버튼의 상태가 원래대로 돌아오게 된다.

목소리 변경하기

목소리를 변경하는 기능을 어떻게 넣을 것인지를 상상해보자. 예를 들어 기존에 만들어 놓은 UI 하단에 목소리 리스트가 쭉 표시되고, 여기서 원하는 목소리를 클릭한 후에 Start 버튼을 클릭하면 그 목소리로 텍스트를 읽어주도록 하는 것이다. 목소리의 리스트를 표시하고, 특정한 행을 클릭했을 때 목소리를 바꾸는 일 역시 테이블 뷰(NSTableView)를 사용하면 구현할 수 있다. 그리고 테이블 뷰는 조금 전 사용한 델리게이트 패턴을 적극적으로 사용하는 코코아 컴포넌트의 전형적인 예이다.

이 기능을 추가하기 위해서 사용가능한 목소리의 목록을 얻어야 하고, 또 음성 합성기의 목소리를 변경하는 기능에 대해서도 조사해보아야 한다. NSSpeechSynthesizer의 클래스 레퍼런스 문서를 보면 다음의 두 메소드를 찾을 수 있다.

  • +availableVoices :: 사용가능한 모든 목소리의 식별자를 NSArray 타입으로 반환한다.
  • -setVoice: :: 목소리를 주어진 식별자의 것으로 변경한다.

이 때 주의해야 할 점은 목소리의 식별자는 내부적으로 문자열이기는 하지만, 범용 식별자 타입으로 macOS의 환경 설정에서 볼 수 있는 목소리 이름으로 표시되지는 않는다. 목소리의 식별자로부터 해당 목소리의 이름을 구하기 위해서는 +attributesForVoice: 를 사용해서 해당 식별자의 목소리의 속성이 담긴 사전을 얻고, 여기에서 NSVoiceName 이라는 키를 사용해서 목소리의 표시용 이름을 얻을 수 있다.

이러한 정보들은 모두 해당 클래스 레퍼런스를 통해서 알 수 있는 것들이니, 가능하면 직접 레퍼런스 문서를 항상 찾아보는 습관을 들이도록 하자.

사용 가능한 목소리의 리스트

AppController에 사용가능한 전체 목소리의 리스트를 참조하는 allVoices 라는 NSArray 타입의 프로퍼티를 하나 만들도록 한다. 그리고 표시용 이름을 위한 별도의 리스트 프로퍼티를 하나 만들도록 하자. AppController.m 의 @interface 부분에 다음을 추가한다.

이 부분은 사실 만드는 사람 마음인데, 테이블 뷰 데이터 소스 메소드에서 매번 표시용 이름을 만들어주어도 되고, 미리 표시용 이름 리스트를 만들어주어도 된다.

@property (copy, nonatomic) NSArray* allVoices;
@property (copy, nonatomci) NSArray<NSString*>* voiceNames;

각각의 프로퍼티의 초기화 코드는 다음과 같다.

- (NSArray*) allVoices
{
  if(!_allVoices) {
    _allVoices = [NSSpeechSynthesizer availableVoices];
  }
  return _allVoices;
}

- (NSArray<NSString*>*) voiceNames
{
  if(!_voiceNames) {
    NSMutableArray *vs = [NSMutableArray arrayWithCapacity:[self.voices count]];
    for (id voice in self.allVoices) {
      [vs addObject:[[NSSpeechSynthesizer attributesForVoice: voice] objectForKey:NSVoiceName]];
    }
    _voiceNames = [vs copy];
  }
  return _voiceNames;
}

테이블 뷰의 데이터소스/델리게이트

이제 AppController를 테이블 뷰의 데이터소스와 델리게이트가 되도록 준비하자. 먼저 AppController.h 파일로 이동해서 클래스 정의 부분에 NSSpeechSynthesizerDelegate외에 NSTableViewDataSource와 NSTableViewDelegate 프로토콜을 추가한다. 상속과 달리 프로토콜은 하나의 클래스가 여러 개의 프로토콜을 따르는 것이 가능하며, < … > 속에 각 프로토콜을 컴마로 구분해서 넣어주면 된다.

/// AppController.h
@interface AppController : NSObject <NSSpeechSynthesizer, 
  NSTableViewDataSource, NSTableViewDelegate>

데이터소스 필수 메소드

데이터소스는 테이블 뷰의 각 셀에 표시될 데이터를 제공해주는데 필요한 메소드들을 정의하고 있다. 여기에는 여러 메소드들이 있지만, 필수적으로 다음 두 개의 메소드가 필요하다. 참고로, NSTableView는 iOS의 UITableView와 그 원리는 거의 비슷한데 몇 가지 다른 점이 있다. 기본적으로 하나의 테이블에 여러 개의 칼럼이 존재할 수 있고, 각 셀을 구성하는 뷰가 아닌, 셀의 데이터가 표시해야 하는 값을 제공한다.

현재는 NSTableView도 UITableView처럼 각각의 셀을 뷰 기반으로 구성할 수 있다. 이 때 iOS와 같이 각 셀을 표현할 뷰를 제공하는 역할은 데이터소스가 아닌 델리게이트가 담당한다.

전통적으로 NSTableView는 레이아웃 용도가 아닌 엑셀과 비슷한 값 위주의 표를 구현하는데, 사용되어 왔음을 염두에 두자. 이 예제에서는 뷰 기반이 아닌 셀 기반의 테이블 뷰를 사용할 것이다.

  • -numberOfRowsInTableView:  : 테이블 뷰 내의 행의 수를 결정한다.
  • -tableView:objectValueForTableColumn:row: :각 열과 행에서 표시할 값을 제공한다.

이제, 테이블 뷰 데이터소스 메소드들을 작성해보자. 테이블 뷰의 행 수는 allVoices 배열의 원소의 개수와 같으며, 여기서 사용할 테이블 뷰는 단일 행이므로 칼럼에 상관없이 row에 맞춰서 해당 위치의 voiceNames 원소를 리턴하면 된다.

#pragma mark - TableView DataSource

- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView
{
  return [self.allVoices count];
}

- (id)tableView:(NSTableView*)tableView
  objectValueForTableColumn:(NSTableColumn*)tableColumn
  row:(NSInteger)row
{
  return [self.voiceNames objectAtIndex:row];
}

이제 화면에 테이블 뷰를 추가해보자. 인터페이스 빌더에서 윈도 크기를 아래로 늘리고 테이블 뷰를 하나 윈도 위로 드래그해서 추가한다.

위와 같이 대략적인 크기를 조절한 다음에, 테이블 뷰를 선택한 상태에서 속성 인스펙터를 보자. 이 때 주의해야할 점이 있다. 기술적으로 테이블 뷰는 스크롤뷰와 클립뷰 내부에 들어있는 상태이다. 따라서 테이블 뷰를 선택하기 위해서는 테이블 뷰 영역을 클릭하여 선택하는 것으로는 (클릭을 해 나가면서 그 내부의 요소를 점차 선택하게 되지만 알아보기 힘들다.) 조작이 어렵기 때문에 화면 왼쪽의 독을 늘려서 독에서 계층 구조를 통해서 테이블 뷰를 정확하게 선택해야 한다.

테이블 뷰를 제대로 선택했다면 속성 인스펙터는 아래와 같이 보일 것이다. 이 중에서 Content Mode 값은 “Cell Based”로 변경하고 열의 수는 1로 조정한다. 그런 다음 테이블 뷰에서 첫 번째 열의 폭을 충분히 큰 크기로 변경해둔다.

그리고 테이블 뷰의 칼럼 헤더 부분을 더블클릭해서 레이블을 Voice라고 변경해주자.

 

이제 아웃렛연결이 필요하다. NSTableView에는 delegatedataSource가 이미 IBOutlet으로 정의되어 있다. 따라서 테이블 뷰로부터 AppController로 연결을 만들어야 한다. 이 역시 캔버스 상에서는 선택이 어려우므로 독을 이용한다. 독에서도 ctrl + 드래그를 이용해서 아래와 같이 AppController로 연결할 수 있다. 이 때 delegate와 dataSource를 모두 연결해야 하므로 두 번 연결해야 한다.

이제 앱을 빌드하고 실행해보자. macOS가 지원하는 음성의 종류가 이렇게 많았다니!하면서 깜짝 놀랄 것이다. 하지만 모든 목소리가 한국어를 말하지는 않는다. 한국어로 말하는 음성은 Yuna를 비롯한 세 개 음성이며, 그외에는 영어 및 그외 각 언어에 대응한다. (숫자를 써서 읽도록 해보면 그 목소리에 해당하는 언어로 읽는다.)

하지만 아직 목소리를 변경하는 기능은 당연히 동작하지 않는다. 어떻게 이것을 처리할 수 있을까? 테이블 뷰에서 특정한 행을 클릭하면 해당 행이 선택된다. 이 것은 테이블 뷰의 ‘선택된 행(selection)이 변경되는 사건‘이다. 테이블 뷰는 역사적인 여러 이유로 이 이벤트의 동작은 타깃 액션이 아닌 노티피케이션 기반으로 처리한다. 즉 AppController가 특정한 테이블 뷰의 델리게이트가 되면 테이블 뷰는 자동으로 자신의 델리게이트가 테이블 뷰가 발송하는 노티피케이션을 수신하도록 등록하게 된다.

따라서 테이블 뷰의 선택영역이 변경되면 델리게이트에서는 -tableViewSelectionDidChanage: 라는 메소드가 호출된다. 이 메소드의 인자는 노티피케이션 객체인데 이 객체의 object라는 프로퍼티가 바로 노티피케이션을 발송한, 즉 클릭이 일어난 테이블 뷰가 된다. 이를 이용해서 테이블 뷰를 판별하는 방법도 있지만, 보통은 AppController가 테이블 뷰에 대한 아웃렛을 가지도록 하는 것이 더 선호된다. (어차피 object 가 바로 그 테이블 뷰인지 확인하는 절차가 필요하기 때문이다.)

AppController.m 상단의 인터페이스 부분에 다음과 같이 아웃렛을 하나 더 추가한다.

@property (weak, nonatomic) NSTableView* tableView;

그리고 다음과 같이 델리게이트 메소드를 작성하자.

#pragma mark - TableView Delegate

-tableViewSelectionDidChange:(NSNotification*)notification
{
  NSInteger i = [self.tableView selectedRow];
  [self.speech setVoice:[self.allVoices objectAtIndex:i]];
}

그리고 절대 빼먹으면 안되는 과정! AppController → NSTableView 로의 아웃렛 연결이다. 조금 전과 동일하게 독에서 연결해준다. 연결까지 끝났으면 다시 빌드하고 실행해보자. 이번에는 텍스트를 입력하고 start를 클릭해보고, 다시 다른 목소리를 선택해서 start해보자. 목소리가 바뀌는 것이 확인되는가?

기본 목소리가 보이게 하기

이 앱에서 한가지 문제가 있는데, 만약 여러분의 맥이 한국어로 설정되어 있다면 기본 목소리는 Yuna가 된다. 그런데 목소리의 리스트는 기본적으로 알파벳순으로 정렬되어 있다. (참고로 이건 테이블 뷰가 자동으로 정렬해주는 것이 아니라, 처음부터 목소리 리스트가 이 순서대로 만들어져 있기 때문이다.) 문제는 다른 목소리를 선택해보기 전까지는 테이블 뷰내의 Yuna 항목은 선택되어 있지를 않다. 또, Yuna는 알파벳순으로 매우 뒤쪽에 위치하기 때문에 처음부터 보이질 않는다.

앱이 시작되었을 때 기본 목소리가 선택된 상태로 테이블 뷰 내에 보인다면 좋겠다. 이 부분을 처리해보겠다.

앱이 런칭하는 과정에 nib 파일을 로딩하는 과정이 포함된다고 했다. nib 파일 로딩이 끝나서, 해당 파일 내의 객체 그래프 복원이 완료되면, 인터페이스 빌더에서 추가된 모든 객체는 코코아 런타임으로부터 -awakeFromNib 이라는 메시지를 받게 된다.우리의 AppController 역시 인터페이스 빌더를 통해서 생성/초기화되기 때문에 화면이 만들어진 직후에 이 메소드를 호출받을 것이다. 따라서 이 메소드를 이용해서 테이블 뷰에서 기본 목소리가 선택된 상태로 있게 하고, 화면에 보이게도 해보자.

  • nib 파일 내의 모든 객체는 화면이 구성되는 시점에 -awakeFromNib 이라는 메소드를 받게 된다.
  • 음성합성기의 기본 목소리는 defaultVoice 라는 클래스 메소드에 정의되어 있다.
  • NSTableView는 -selectRowIndexes:byExtendingSelection: 이라는 메소드를 통해서 선택 영역을 코드상에서 변경할 수 있다.
  • -scrollRowToVisible: 을 통해서 n 번째 행이 화면내에 들어오도록 스크롤 할 수 있다.

이 내용을 종합해서 아래와 같은 코드를 추가해본다.

#pragma mark - ETC
- (void)awakeFromNib
{
  id currentVoice = [NSSpeechSynthesizer defaultVoice];
  NSInteger i = [self.allVoices indexOfObject:currentVoice];
  NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:i];
  // 테이블뷰에서 행 선택
  [self.tableView selectRowIndexes:indexes byExtendingSelection:NO];
  // 스크롤
  [self.tableView scrollRowToVisible:i];
}

자 이제 다시 앱을 빌드하고 실행해보자. 아래와 같이 Yuna 의 목소리가 선택된 것이 보일 것이다.

여기까지해서 기본적인 코코아 앱을 만들고, 컨트롤러를 작성해서 상태에 따라 UI를 제어하는 기본적인 동작과, 테이블 뷰를 기본적인 레벨에서 구성하는 방법을 살펴보았다. 특히 델리게이트 패턴은 이렇게 기본적인 앱에서도 여러차례 등장할 만큼 중요한 패턴이라는 점도 알게되었다.

연재를 마무리하는 다음 글에서는 이 TTS앱을 다시 처음부터 재작성해 볼 것이다. 그러면서 iOS에는 없는 코코아바인딩이라는 기술을 사용해서 컨트롤러의 상태와 UI를 양방향으로 코드 없이 연결하는 방법에 대해서 살펴볼 예정이다. 코코아 바인딩을 사용하면 버튼의 상태를 제어하기 위한 메소드 호출 및 프로퍼티 선언을 생략할 수 있으며, 극단적으로는 테이블 뷰와 관련된 코드 역시 대부분 제거할 수 있음을 보일 예정이다.

 


  1. 프로토콜에 관한 글을 참고하자. 

코코아 앱 따라하기 – 1

iOS용 앱을 만드려는 사람들은  Objective-C나 Swift를 배워야 한다. Swift는 신생언어이니 좀 예외적이긴 하지만, iOS 용 앱을 만들고자 하는 사람들의 수요가 늘어나면서 국내에도 코코아터치에 관한 책들은 정말 많이 발간되었다. 다만 이런 책들은 대부분 iOS앱에 초점을 맞추다보니 사실 국내에는 제대로 된 Objective-C 용 교재나 macOS용 앱을 위한 코코아 관련 책이 거의 전무한 실정이다. 애론 힐리가스 아저씨가 쓴 코코아 프로그래밍 책이 그나마 괜찮은 편이지만, 워낙 오래된 책이라1 마운틴 라이언이 출시되면서 앱킷의 API 가 대거 변경되었는데, 이러한 내용이 반영되지 못하고 있다.

이런 좋은 책들을 대체하려는 글을 쓰려는 것은 아니고, 아주 간단한 코코아 앱을 만드는 과정을 따라가면서 macOS용 앱을 만드는 것은 iOS용 앱을 만드는 것과 어떻게 다른지를 살펴보고, 코코아 앱의 핵심 개념이나 MVC, 델리게이트와 같은 디자인 패턴들이 어떻게 적용되는지를 살펴보겠다. 이 강좌는 우선 Objective-C를 기준으로 설명되며(맥북이 너무 오래돼서 성능이 매우 좋지 않아서….) 스토리보드,  Swift 를 사용하는 버전에 대해서는 다음에 내가 하고 싶을 때 추가로 작성해 볼 생각이다.

만들어보고자 하는 앱은 아주 간단한 TTS 앱으로, 사용자가 입력한 텍스트를 소리 내어 읽어주는 앱이다. 뭔가 대단하고 화려한 기술을 펼칠 것은 아니고, 사실 코코아에서 음성합성기를 기본적으로 제공하기 때문에, 이걸 살짝 포장하는 수준의 간단한 앱이다. 우리는 이 음성합성기를 우선 가장 기본적인 구성 요소로 앱을 만들어 보고, UI를 조금 더 충실하게 다듬은 다음, 사용자가 목소리를 변경할 수 있게 하는 기능을 추가해볼 예정이다. 그리고 그 이후에는 그 때까지 만들었던 내용을 다 뒤집어 엎어서 아예 새로운 방식으로 접근하는 방법도 알아볼 것이다.

여기서 사용된 Xcode 버전은 9.0이다.

프로젝트 만들기

Xcode를 실행하고 새로운 Cocoa App 프로젝트를 생성한다. 프로젝트의 이름은 SpeechSynth-v1 으로 한다. 언어는 Objective-C 를 선택하고 스토리보드나 코어데이터를 사용하지 않을 것이며 그 외에 어떤 것도 추가할 필요가 없다. 따라서 아래와 같이 모든 선택 사항에 대해 체크를 해제한 채로 프로젝트를 생성하자.

 

기본 UI 구성 및 앱 컨트롤러 추가하기

프로젝트를 생성하면, 프로젝트 내에는 기본적으로 필요한 몇 개의 파일이 미리 생성되어 있다. 대표적으로 AppDelegate와 (Objective-C 에서 클래스 1개는 보통 헤더파일(.h)과 구현부파일(.m)이 한 쌍을 이룬다.) MainMenu.xib 라는 인터페이스 빌더용 데이터 파일이 생성되어 있다.

앱 델리게이트는 앱의 구동과 종료시에 시스템이 애플리케이션에게 결정을 요구하거나, 혹은 특정 라이프 사이클 이벤트에 대해 이벤트 핸들러 메소드를 호출하는데, 이러한 처리를 대행해주는 클래스이다. 인터넷에서 찾아 볼 수 있는 많은 튜토리얼들은 (아마도 귀찮아서 그런지) 별도의 앱 컨트롤러를 만들지 않고 델리게이트가 앱 컨트롤러 역할을 담당하는 경우가 많은데, 별로 권장하지 않는다. 우리는 여기서는 일단 앱 델리게이트는 건드리지 않을 것이다. (그렇다고 필요없는 클래스는 아니니 절대 삭제해서는 안된다.)

 

프로젝트가 시작되면 Xcode 맨 왼쪽에 프로젝트 네비게이터에서 MainMenu.xib를 선택한다. 그러면 Xcode의 화면이 인터페이스 빌더로 전환된다.

 

인터페이스 빌더의 화면 구성은 위와 같다. 맨 왼쪽에는 네비게이터 패널이 있다. 그 왼쪽에는 nib 파일에 추가되어 있는 클래스들이 표시되는 독(Dock)이 있다. 화면 가운데는 UI 요소들을 편집하는 영역이다. 화면의 오른쪽에는 인스펙터와 오브젝트 팔레트가 있다. 인스펙터는 선택된 객체의 여러 속성값들을 조절하는 곳이고, 오브젝트 팔레트는 미리 정의되어 있는 Cocoa UI 클래스들을 검색하고 드래그 앤 드롭으로 인터페이스 빌더에 추가할 수 있는 곳이다.

버튼 추가하기

기본적으로 인터페이스 빌더에는 메인 메뉴가 덜렁, 표시되는데 독을 잘 보면 아래에 window가 있다. 이것을 선택하면 메인 윈도우가 짠 하고 화면에 나타난다.

 

오른쪽 팔레트에서 Push Button을 드래그해서 윈도우 안으로 끌고 가자. 이렇게 드래그하면 반투명 파란색으로 특정 객체가 표시되는데, 지금은 윈도의 메인 뷰에 버튼이 추가된다는 것을 알려주는 것이다. 이 상태에서 버튼을 드롭하면 윈도 안에 버튼이 추가된다.

추가된 버튼을 더블 클릭하면 레이블 이름을 변경할 수 있다. 두 개의 버튼을 추가하고 (혹은 한 개를 추가한 다음, opt + 드래그로 복제해도 된다.) 각각의 이름을 Start, Stop으로 변경해준다.

텍스트 필드 추가하기

앱이 읽어줄 텍스트를 직접 입력할 텍스트 필드도 추가해보자. 객체 팔레트의 아래쪽에는 검색창이 달려있다. 여기에 text라고 입력하면 자동으로 팔레트의 내용이 필터링된다.

그 중에서 Text Field를 선택하고 윈도에 드래그해서 추가한다. 그리고 각각의 크기와 위치를 적절하게 조정해서 아래와 같이 정돈해보자.

앱 컨트롤러 추가

UI 상에서 버튼을 클릭하는 등의 액션을 처리하고, UI 요소의 상태를 제어할 컨트롤러는 별도의 클래스로 추가할 것이다. cmd + N 을 눌러서 새로운 파일을 추가한다. 파일의 종류는 Cocoa Class를 선택한다.

아래와 같이 클래스의 이름은 AppController로 하고 NSObject의 서브 클래스가 되도록 한다. 선택을 마친 후 저장 위치는 기본 위치 (프로젝트 폴더 내)에서 생성하도록 하자. 참고로 클래스를 이렇게 추가하면, 프로젝트 네비게이터에서는 폴더 위치와 상관없이 파일이 나타난다. (네비게이터 상의 계층 구조는 실제 디렉토리/파일구조와 다른, 프로젝트 상의 파일들의 구성이다.)

 

헤더 편집

새로운 클래스 파일을 생성하고나면, AppController.m 파일이 편집기에서 자동으로 열린다. 먼저 헤더쪽에서 수정해야 하는 부분이 있으니, 네비게이터에서 AppController.h 파일을 선택해서 헤더 파일을 먼저 편집하자. 헤더 파일은 아래와 같이 3줄의 코드가 들어있다.

#import <Foundation/Foundation.h>
@interface AppController: NSObject
@end

여기서 <Foundation/Foundation.h> 부분을 <Cocoa/Cocoa.h> 로 변경한다. 우리가 곧 사용하게 될 음성 합성기 클래스는 파운데이션이 아닌 코코아 클래스이기 때문에, 프레임워크 헤더를 변경해주어야 한다. 파일을 저장하고 (나중에 빌드할 때 자동으로 저장해주므로 굳이 저장할 필요는 없긴 하다만, 분야를 막론하고 중간 저장은 꼭 습관화해야 한다.) 헤더파일은 여기까지 하면 됐고, 다시 AppController.m 파일로 돌아가자. 이 파일에는 다시 아래와 같은 3줄의 코드가 있다.

#import "AppController.h"
// <- 요 위치에 편집 시작
@implementation AppController
@end

내부인터페이스 – 프로퍼티 작성

구현의 가장 첫 단계는 앱 컨트롤러가 해야 하는 일을 정의하는 것이다. 컨트롤러는 기본적으로 다음의 일을 처리해야 한다.

  1. Start 버튼이 클릭되면 입력된 문자열을 음성합성기에 넘겨서 읽도록 한다.
  2. Stop 버튼이 클릭되면 읽던 동작을 멈추도록 한다.

따라서 앱 컨트롤러는 음성합성기를 하나 생성해서 가지고 있어야 한다. 그리고 사용자가 입력한 텍스트를 얻기 위해서 UI상에 추가한 텍스트 필드에 대한 참조점을 가지고 있어야 한다. 이 둘은 모두 선언 프로퍼티로 작성될 텐데, 프로퍼티 선언은 @interface  영역에서 한다. 그런데 이 부분은 아까 헤더 파일에 들어있던 것 아닌가? 새로운 프로퍼티를 만들어야 하거나 편집해야 할 때마다 헤더와 m파일을 오가는 것이 귀찮기도 하고해서 하나의 파일 내에서 내부 인터페이스를 정의하기로 한다. (기술적으로 이것은 익명 카테고리를 사용하는 것이다.) 이 부분을 정의하기 위해서 @implentaion 위쪽에 다음의 코드를 추가하자.

@interface AppController ()

@end

다시 이 @interface ~ @end 사이에 아래 두 프로퍼티 선언과 액션메서드 선언을 추가한다.

@property (weak, nonatomic) IBOutlet NSTextField* field;
@property (strong, readonly, nonatomic) NSSpeechSynthesizer* speech;
- (IBAction)startSpeaking:(id)sender;
- (IBAction)stopSpeaking:(id)sender;

이 두 프로퍼티는 각각 다음과 같이 설명된다.

  1. field는  인터페이스 빌더 상에서 추가한 텍스트 필드를 참조할 이름이다.
  2. speechfield에 입력된 텍스트를 읽어주는 일을 담당할 객체이다.

보통 특정 클래스의 프로퍼티를 초기화할 때에는 -init 메소드에서 초기화하는데, 그러기보다는 getter 메소드가 맨 처음 호출될 때 초기화하는 전략을 쓰도록 하겠다. 다음의 코드를 @implementation ~ @end 사이의 영역에 추가한다.

#pragma mark - Properties
- (NSSpeechSynthesizer *)speech {
  if(!_speech) {
    _speech = [[NSSpeechSynthesizer alloc] init];
  }
  return _speech;
}

그런데 _speech는 어디서 튀어나온 것일까? 프로퍼티는 일반적으로 getter/setter 두 개의 접근자 메소드와 객체나 값을 저장할 스토리지 변수로 구성되는데, LLVM 컴파일러는 프로퍼티에 대해서 자동으로 그 이름 앞에 언더스코어를 붙인 변수를 자동으로 생성하고 nil로 초기화해준다.2 따라서 해당 인스턴스 변수가 nil 이면 초기화 작업을 진행하고 그 변수의 값을 리턴하도록 하는 것이다. 이러한 방식으로 프로퍼티를 작성하면 해당 프로퍼티는 느긋한(Lazy) 프로퍼티가 되는데, 예를 들어 자주 쓰이지 않거나 초기화하는데 많은 자원이 들어가는 프로퍼티라면 가능한 해당 프로퍼티가 필요한 시점에 초기화되도록하여 앱의 초기 구동 시간을 단축하는데 도움이 될 수 있다.

그리고, field는 별도로 초기화하지 않는다. 이 프로퍼티의 초기화는 코드가 아닌 인터페이스 빌더를 통해서 하게되니 조금 기다리자.

액션 메소드 구현하기

다음은 액션 메소드를 작성할 차례이다. 다음 두 개의 메소드를 역시 @implementation ~ @end  사이에 추가한다.

#pragma mark - Actions
- (IBAction)startSpeaking: (id)sender
{
  NSString *words = [self.field stringValue];
  if([words length] > 0) {
    [self.speech startSpeaking:words];
  }
}

- (IBAction)stopSpeaking: (id)sender
{
  [self.speech stopSpeaking];
}

-startSpeaking: 메소드는 나중에 Start 버튼이 클릭되면 호출될 메소드이다. 텍스트 필드에 입력된 문자열을 가져와서 그 길이가 0보다 크면 해당 텍스트를 음성합성기로 전달해서 읽도록 한다.  -stopSpeaking:은 Stop 버튼이 클릭되면 호출되며, 읽기를 중단하는 기능을 수행한다.

IBAction 이라는 키워드는 void와 같은 뜻이다. 다만, 이 키워드를 사용한 메소드는 Xcode에 의해서 인터페이스 빌더와 연결가능한 액션 메소드라는 의미가 된다.

컨트롤러와 UI 요소 연결하기

이상으로 작성해야할 코드는 다 작성했다. 하지만 아직 끝난 것은 아니다. 가장 중요한 과정이 남아있다. 바로 컨트롤러의 IBOutlet, IBAction을 실제로 인터페이스 빌더에서 연결해주는 작업을 해야한다. 초보자들이 가장 많이 하는 실수가 이 부분이다. 앱을 빌드하고 실행했을 때 버튼 클릭에 반응이 없거나, UI 요소가 제대로 값을 표현하지 못한다면 가장 먼저 이 부분이 제대로 수행되었는지를 반드시 확인하자.

다시 MainMenu.xib 파일을 선택해서 인터페이스 빌더를 연다. 오른쪽 팔레트에서 Object를 입력해서 Object 객체를 찾는다. 이 객체는 메인 윈도가 아닌 왼쪽의 독으로 가져가야 한다.

 

추가한 Object를 선택하고 다시 오른쪽 위의 인스펙터를 보자. 세번째 탭이 Identity Inspector인데, 여기서 클래스의 종류를 정의해줄 수 있다. AppController라고 클래스의 이름을 입력해준다. 이렇게 되면 방금 독에 추가한 객체의 클래스 종류가 확인되면서, 인터페이스 빌더는 해당 클래스 객체의 IBAction 메소드나 IBOutlet 프로퍼티를 인식하여 연결할 수 있는 부분들을 자동으로 감지할 수 있게 된다.

 

우리가 작성했던 코드를 잠깐 생각해보자. 코드 내에서 앱 컨트롤러의 두 프로퍼티 중에서 speech는 직접 최초 액세스시에 생성되도록 초기화 코드를 작성했다. 그런데 speech에 대해서는 별도의 초기화 메소드를 작성하지 않았다. 그런데 잘 생각해보면 프로젝트 어디에도 AppController 클래스의 인스턴스를 생성하는 코드가 들어있지 않다. 그렇다면 이 앱 컨트롤러는 어디서 생성되어 초기화되는 것일까?

정답은 nib 파일이다. 프로젝트를 컴파일하고 만들어진 앱을 실행하면, 앱은 런칭 시점에 자동으로 초기 nib 파일을 로딩하고, 그 내부에 정의된 객체들을 읽어들여서 인스턴스를 만들고 연결 등의 초기 세팅을 설정한다. 방금과 같이 AppController도 이제 nib 파일에 들어가 있게 되니 앱이 로딩될 때 자동으로 nib 파일로부터 로딩되어 인스턴스가 생성될 것이다.

아웃렛과 액션 연결

왼쪽의 Dock에서 AppController를 마우스 우클릭으로 드래그 (control 키를 누른 채로 왼쪽 드래그를 해도 같은 동작이다.)하면 파란색 선이 끌려온다. 이 선을 텍스트 필드로 가져가면 아래와 같은 상태가 된다.

 

이 때 마우스를 놓으면 까만색 작은 팝업이 뜨고, 여기서 field 를 선택하고 클릭한다. 그러면  앱 컨트롤러의 field 프로퍼티가 인터페이스 빌더 내의 텍스트 필드와 연결된 것이다. 이는 인터페이스 빌더 내에서 앱 컨트롤러의 field 라는 프로퍼티에 대해서 실제로 화면에 그려놓은 텍스트 필드 객체로 초기화하는 것과 동일한 의미가 된다.

액션 메소드 연결은 반대 방향이어야 한다. 같은 방식으로 Start버튼에서 control + 드래그해서 AppController로 마우스를 가져가보자.

 

이번에는 팝업에 두 개의 메소드가 표시될 것이다. 그 중에서 -startSpeaking:을 선택한다. 같은 방법으로 Stop 버튼을 AppController의 -stopSpeaking: 과 연결해주자.

이제 이 번 시간에 해볼 모든 과정이 끝났다. 연결까지 제대로 성공했다면 상단에 있는 플레이(?) 버튼을 클릭해서 앱을 빌드하고 실행해보자.

정리

가장 기본적인 코코아 앱을 구성하는 방법을 살펴보았다. 컨트롤러가 UI요소로부터 어떤 값을 액세스하거나 상태를 변경해주기 위해서는 프로퍼티 아웃렛을 작성해야 하고, 또 UI 요소에서 발생한 이벤트를 처리하기 위해서는 액션 메소드를 작성해야 한다. 그리고 이렇게 작성된 아웃렛과 액션은 인터페이스 빌더상에서 해당 UI 요소와 연결되어야 하기 때문에 앱 컨트롤러 객체도 인터페이스 빌더에 추가되어야 하고, 그 내부에서 아웃렛/액션 연결을 만들어주었다.

사실 여기까지의 과정은 iOS용 앱을 만드는 과정과 크게 다르지 않다. iOS 앱은 초기화면이 되는 root view가 있고, 이를 제어할 rootViewController가 프로젝트에서 기본적으로 주어지는데, 이 뷰 컨트롤러가 해당 뷰에서의 모든 기능을 컨트롤하는 역할을 한다. macOS는 환경의 차이로 큰 화면 내에서 앱의  UI는 하나의 뷰가 아닌 여러 개의 뷰로 쪼개어질 수 있기 때문에 앱의 전반적인 관리를 수행할 앱 컨트롤러가 필요한 것이다. (물론 각 윈도우마다 윈도 컨트롤러를 만들거나, 뷰 마다 뷰 컨트롤러를 붙이는 것도 가능하다.)

다음 글에서는 이 앱을 조금 다듬고, 목소리를 변경할 수 있는 기능을 추가하는 방법에 대해서 알아보도록 하자.

참고자료


  1. 국내 번역본 기준으로 Xcode 3 일 때 번역서가 출간되었다. (그리고 곧 Xcode 4가 나오게 되고….) 
  2. 선언 프로퍼티 참고. 

파이썬은 처음이라 – 함수는 처음이라

함수는 일련의 동작을 수행하는 코드를 묶어서 재사용하기 쉽도록 호출가능한 단위로 만든 것을 말한다. 수학적인 관점에서 함수는 어떠한 입력이 주어졌을 때, 그 입력값을 사용하여 어떤 연산을 처리하고 다시 그 결과를 되돌려주는 계산상자에 비유되기도 한다. 파이썬의 함수는 이러한 두 특성을 모두 가지는 코드 덩어리이다.

함수의 수학적인 정의를 말로 풀어내지 않더라도 “계산상자”나 혹은 자판기 같은 것을 생각하면 함수가 어떤 역할을 하는지 쉽게 비유할 수 있다. 커피 자판기를 함수로 본다면 동전을 넣으면 커피가 나오는 함수에 비유할 수 있다. 물론 우리 모두는 커피 자판기 내부에 미리 넣어놓은 커피와 물 그리고 컵이 있고, 자판기에 동전을 넣으면 컵에 커피와 뜨거운 물이 담겨서 커피가 나온다는 사실은 알고 있다. 하지만 중요한 것은 동전을 넣으면 커피가 나온다는 사실, 즉 입출력에 대한 것이며, 함수에서 그 내부에 어떤 동작이 실제로 수행되는지는 사실 중요하지 않다.1

프로그램 역시 사용자로부터 정해진 형식의 데이터를 입력받아, 처리된 결과를 출력한다는 측면에서 하나의 함수라 볼 수 있다. 그리고 그 내부의 처리 과정에 있어서 입력값은 더 작은 단위의 함수에 의해서 중간값으로 변환되고, 다시 그 중간값이 또 다른 함수에 의해서 변환되는 과정을 거친 후 최종적으로 출력될 값이 된다. 2 즉 함수는 프로그래밍 코드를 재사용하는 단위인 동시에 프로그램을 구성하는 빌딩블럭(building block)으로도 볼 수 있다.

함수의 요소

사실 어떤 함수를 작성하는 것은 문법만 맞춘다면야 뭐 어떤 코드 블럭을 집어넣든 그것은 프로그래머의 마음이기 때문에 딱히 함수는 이렇게 정의해야 한다는 법칙 같은 것은 없다. 하지만 가능하다면 함수는 “어떤 일을 하는 단위”로 정의하고, 여기서 일은 앞서 설명한 바와 같이 입력을 받아 처리하고 출력값을 내놓는 범위로 정하면 된다. 따라서 함수의 정의는 함수가 갖추어야 할 요소들을 정의하는 것에서 출발한다.

  • 함수의 이름 : 함수의 이름은 여느 식별자와 마찬가지로 고유해야 한다.
  • 함수의 입력 : 함수의 입력은 함수가 실행될 때 전달받는 인자들이 된다. 인자는 하나 이상이거나, 경우에 따라서는 인자가 필요없는 경우도 있다.
  • 함수의 출력 : 함수가 어떤 값을 출력할지를 결정한다. 경우에 따라서 함수의 출력값이 없는 경우도 있을 수 있다. 이 경우에 함수는 암묵적으로 None을 리턴하게 된다.

함수 정의 문법

함수를 정의하는 문법은 다음과 같다.

def some_func( arg1, arg2 ):
^^^ ^^^^^^^^^ ^^^^^^^^^^^^ ^
 1    2         3          4
  5 블럭...
  6 return result
  1. 함수의 정의는 def 키워드로 시작한다.
  2. 함수의 이름을 선언하고
  3. 괄호 속에 함수가 받아들일 인자의 이름을 선언한다. 인자가 2개 이상인 경우 컴마로 구분하며, 인자가 필요없는 경우에는 빈 괄호를 사용한다.
  4. 선언부의 끝은 콜론으로 끝나며, 이는 그 다음줄 부터는 들여쓰기를 적용하는 블럭이란 의미이다.
  5. 함수가 실제로 처리할 코드 블럭을 작성한다.
  6. 함수의 끝은 return 문으로 끝난다. return 문은 함수가 리턴해야 할 값을 표현식으로 정의해준다. 리턴 값 없이 return 만 사용하면 return None으로 해석된다. 만약 return 문이 없는 함수는 함수의 마지막 줄에 암묵적으로 return None이 있는 것으로 간주한다.

간단한 함수를 정의하는 예를 몇가지 살펴보자.

## 두수의 합을 계산하는 함수
def add(x, y):
  return x + y

## 두수의 곱을 계산하는 함수
def mul(x, y):
  return x * y

## 정수 N을 입력 받아 1~N까지의 합을 계산하는 함수
## 단, N < 1 이면 0을 리턴한다. 
def accumlate(n):
  if n < 1:
    return 0 ## return 문을 만나면 함수의 실행은 여기서 종료된다.
  result = 0
  for i in range(n):
    result = reslut + i + 1
  return result

## 아래 함수는 메시지를 출력만 한다.
def print_tag(msg, tagname):
  print("<" + tagname + ">", msg, "</" + tagname + ">")
  ## 명시적인 리턴구문이 없으므로 여기까지 실행되면
  ## 함수의 실행이 종료된다. 

함수의 호출

함수의 내용을 실행하는 것을 함수를 호출한다고 표현한다. 함수의 호출은 함수의 이름 뒤에 괄호를 붙인다. 함수가 인자를 필요로 하는 경우에는 함수에 정의된 순서대로 인자를 콤마로 구분하여 넣어준다. 함수을 호출하는 문법은 그 자체로 하나의 표현식으로 취급되며, 이 표현식은 함수의 리턴값으로 평가된다. 우리는 이 글 이전에도 몇 가지 기본 함수를 사용하는 것을 예제를 통해 접해본 바 있다.

a = add(3, 4)
## a => 7
print(a)
## 7

## accumluate(10)은 그 자체로 결과값으로 평가되는 표현식이므로
## 다른 함수의 인자로 전달할 수 있다. 
print(accumlate(10))
## 55

print(int(input()) + 3)
## 입력된 숫자에 3을 더한 값을 출력한다. 

기본함수 input()은 키보드로부터 한줄의 문자열을 입력받는 함수이다. 따라서 input() 이라고 쓴 표현식은 키보드의 입력이 들어오게 되면 그 내용으로 구성된 문자열로 평가된다.

함수 호출과 흐름

파이썬 스크립트는 그 자체로 소스코드인 동시에 프로그램이기도 하다. 스크립트가 실행되면 소스코드의 맨 윗줄부터 파이썬 해석기에 의해서 실행되기 시작한다. 만약 어떠한 함수 호출도 사용하지 않는 프로그램을 작성했다면, 프로그램의 실행 방향은 코드의 위에서 아래로 흐르게 된다. 프로그램의 시작과 동시에 발생하는 실행 흐름을 메인 루틴(main routine)이라고 한다. 메인 루틴이 진행되는 과정에서 어떤 함수를 호출하게 되면 무슨 일이 생길까?

  1. 함수 호출 구문을 만나면, 현재 실행위치를 ‘어딘가’에 저장해두고 함수의 블럭 시작 위치로 실행 위치가 옮겨간다.
  2. 이 때, 전달된 인자값을 복사하여 가져가게 된다.
  3. 전달된 인자값은 함수 내에서 통용되는 변수가 되고, 이 값들을 이용해서 함수의 코드들이 실행된다.
  4. 리턴문을 만나거나 함수의 끝에 다다르면 1.에서 저장해두었던 위치로 돌아간다. 만약 리턴되는 값이 있다면 이 값을 가지고 가게된다.

즉, 함수를 호출하게 되면 메인 루틴이 잠시 중단되고 또 다른 별개의 실행 흐름이 시작된다. 이는 마치 고속도로의 1차선으로 달리다가, 함수를 호출하는 동안 2차선으로 차선을 변경한 후, 함수의 실행이 끝나면 다시 1차선으로 되돌아가는 것과 비슷하다고 하겠다. (물론 고속도로에서 차선을 바꾼다고, 원래 위치로 점프하는 것은 아니지만…) 만약 함수 내에서 다시 다른 함수를 호출한다면? 2차선에서 3차선으로, 3차선에서 4차선으로 계속해서 차선을 바꿔 “내려가게” 되고, 각 단계에서의 실행이 종료되면 다시 차선을 거슬러 올라가 1차선으로 돌아가게 된다. 따라서 메인 루틴을 중지하고 별개의 루틴으로 진입하게 된다는 점에서 함수의 실행 흐름을 서브 루틴(sub routine)이라고도 부른다.

메인루틴과 서브루틴은 절차지향적인 프로그래밍 관점에서의 비선형적인 실행 흐름을 이야기할 때 쓰는 표현이니, 그냥 그렇게 부르더라하는 정도로 이해하면 되겠다. 우리가 주목해야 할 점은 값 즉, 데이터이다. 입력 혹은 출력이 없는 몇몇 함수들을 예외적으로 둔다면, 데이터는 함수의 입력으로 들어가서, 함수의 내부에서 변환되어 출력으로 나오게 된다. 즉 함수는 그 외부에서 보았을 때 입력값을 변형하여 출력하는 장치로 볼 수 있다. 자판기라는 것을 전혀 본 적이 없는 사람의 입장에서 커피 자판기는 반짝거리는 쇠조각을 커피로 바꾸는 마법의 상자에 다름없듯이 말이다. 즉 “값을 조작하는 변환기”라는 관점에서 함수를 이해하고 있는 것이 앞으로 우리가 이야기하려는 관점에서는 매우 중요하다.

함수의 인자

함수의 인자를 정의하는 방법에 대해서 다시 생각해보자.

  1. 어떤 함수들은 인자를 받지 않은 경우가 있다.
  2. 어떤 함수들은 하나 혹은 그 이상의 고정된 인자를 받는다.
  3. 어떤 함수의 인자들은 있는 경우도 있고 없는 경우도 있다. (input(), print() 등)
  4. 어떤 함수들은 1개 이상의 정해지지 않은 개수의 인자를 받는다
  5. 어떤 함수들은 인자에 이름을 붙여야 하는 경우가 있다.

이중에서 함수의 인자가 고정된 경우는 앞서 소개한 문법을 사용해서 정의하는 것이 가능하다. 그렇다면 선택적 인자(있어도 되고 없어도 되는) 와 가변 인자(한 개 일수도, 여러 개 일 수도 있는)는 어떻게 정의할 수 있을까?

기본값을 갖는 인자

함수의 인자를 정의할 때, 디폴트 값을 정의할 수 있다. 인자에 디폴트 값을 정의하는 경우에는 호출하는 표현에서 해당 인자를 생략하면, 지정한 디폴트값을 사용한다.

def greet(name="unnamed"):
  print("hello, ", name)

greet('Tom')
# "hello, Tom"
greet()
# "hello, unnamed

위 함수에서와 같이 name 이라는 인자를 선언하면서 name="unnamed"라고 기본값을 지정해주었다. 이렇게 선언하면 해당 인자는 생략이 가능한 인자가 된다.  두 개 이상의 인자를 갖는 함수에서 일부 인자들만 기본값을 갖는다면, 기본값을 갖는 인자들을 항상 뒤쪽에 배치해야 한다. 왜냐하면 함수를 호출할 때, 인자값을 순서대로 넣기 때문이다.

## 동작하지 않는 예제!!
def some_func(a, b=1, c):
  return a + b + c

some_func(1, 2, 3) 
## a->1, b->2, c->3 임을 알 수 있다. 
some_func(1, 2)
## a->1 이지만 2는 b인가? c인가?

함수를 호출했을 때, 함수의 내부에서는 괄호안에 들어온 값들을 순서대로 매칭하려고 시도한다. 따라서 기본값이 없는 인자들을 구분할 수 있는 방법은 오로지 인자의 순서이다. 그렇기 때문에 인자 목록의 중간에는 디폴트 값을 갖는 인자를 넣을 수 없다. 파이썬에서는 이렇게 디폴트 값을 갖도록 선언한 인자를 ‘키워드 인자’라고 따로 구분해서 부른다. 그 이유는 다음의 가변인자에 대해 설명한 후에 풀어나가겠다.

가변 인자

두 개의 정수를 받아서 그 중에서 큰 값을 리턴하는 my_max()라는 함수를 정의한다고 생각해보자.

def my_max(a, b):
  if a > b:
    return a
  return b

함수 자체는 간단한데, 경우에 따라서는 3개의 값 중에서 가장 큰 값을 찾아야 하는 경우가 있을 것이다. 물론 그 때는 my_max(a, my_max(b, c))와 같은 식으로 b와 c중에서 큰 값을 찾고 그것을 다시 a와 비교해서 세 수 중의 최대값을 찾는 방법도 있을 것이다. 혹은 세 개의 수에 대해서 최대 값을 찾는 또 다른 함수를 정의해야 할 필요가 있을지도 모르겠다.

def my_max3(a, b, c):
  return my_max(a, my_max(b, c))

세 수 중에서 최대값을 구하는 구현에는 여러 가지가 있을 수 있다.

  1. 세 수 a, b, c 에 대해서 먼저 a > b  일 때, a  > c 이면 a가 최대값이고,  그렇지 않다면 c가 최대값이다. 다시 b >= a 일 때 b > c 이면 b 가 최대값이고 그렇지 않다면 c 가 최대값이다.
  2. 세 수 중에서 두 수의 최대값을 찾는다. 그리고 그 값과 나머지 한 값 중에서 최대값을 찾으면 그것이 세 수 중의 최대값이다.

위 두 명제는 세 수에 대해서 최대값을 찾는 방법을 설명한 글이다. 어떤 글이 더 간결하고 이해하기 쉬운가? 위 my_max3()은 두 번째 문장을 그래도 코드로 옮겨놓았으며, 그만큼 간결하고 실수를 통해서 버그가 발생할 여지도 줄였다. 이것이 함수로 함수를 만드는 관점이 가지는 힘이다.

그런데, 그러다보면 4 개, 5개, 6개의 수에 대해서 최대값을 찾아야 하는 경우도 빈번하게 발생할 수 있을 수 있고, 그 때마다 인자를 달리하는 다른 함수들을 매번 작성하기는 번거롭다. 파이썬에서는 이렇게 인자의 개수가 정해지지 않은 함수를 정의하는 방법이 있다.

def some_func(*args):  #1
  pass

def my_max_n(a, b, *cs):  #2
  ...

바로 인자의 이름 앞에 *3을 붙이는 것이다. 이렇게하면 some_func(1, 2), some_func(1, 2, 3), some_func(1, 2, 3, 4)와 같이 인자를 얼마든지 많이 넣을 수 있다. 그리고 각각의 인자는 함수의 내부에서 리스트와 비슷하게 args[0], args[1],.. 과 같은 식으로 참조할 수 있다.

두 번째 my_max_n(a, b, *cs) 의 의미는 a와 b는 반드시 필수적으로 넣어야 하는 인자이며, 그 이후 자리는 가변인자들로 넣어도 그만, 안넣어도 그만인 셈이다. (실제 my_max_n() 함수의 구현에 관해서는 튜플에 대한 내용을 배운 다음에 설명하는 것이 좋을 것 같다.)

언패킹

가변 인자는 선언하고자 하는 경우에 고정 인자와 키워드 인자(방금 말했던 디폴트 값이 있는 인자) 사이에 위치해야 한다. 키워드 인자에 대해서도 모든 기본값을 정의하기 어렵거나, 특정한 환경 설정과 관련된 함수의 경우에 인자가 수십개가 넘어가는 경우가 있어서 일일이 인자를 정의하기 힘든 경우가 있다. 이 때는 가변 키워드 인자를 정의할 수 있는데, **변수명으로 선언할 수 있다. 이렇게 선언된 가변 키워드 인자는 함수 내에서  나중에 배우게 될 사전으로 취급된다.

정리하자면 인자의 정의 순서는 고정인자 > 가변인자 > 키워드인자 > 가변키워드인자 의 순으로 정의해주면 된다.

보너스 – 반환값이 2개인 함수

C와 같은 언어를 먼저 접해본 경험이 있는 사람이라면 이 지점이 상당히 당황스러울 수 있는데, 파이썬에서 함수는 하나의 리턴값만을 반환하는 것이 아니라, 2개 혹은 그 이상의 값을 한꺼번에 반환하는 것이 가능하다. 바로 가변 인자에서 잠깐 언급한 튜플(tuple)을 사용하는 방법이다. 표준 내장함수 중에서 divmod()라는 함수가 있는데, 이 함수는 두 수를 받아서 나눈 몫과 그 때의 나머지를 계산해서 한 번에 리턴한다. 예를 들면 다음과 같은 식이다.

def divmod(x, y):
  return x // y, x % y

리턴해야 하는 값이 2개 이상이어야 하는 경우는 언뜻 생각하기에 엄청 예외적일 수 있다고 생각되겠지만, “그 이전에 항상 다른 방법으로 해결해왔기 때문에” 그렇게 느끼는 것일 뿐, 이 패턴이 유용한 경우는 생각보다 매우 많다.

이렇게 해서 기본적으로 함수를 정의하고 호출하는 방법에 대해서 살펴보았다. 앞으로 많은 예제들은 함수를 정의하고 함수와 함수를 연계하는 식으로 문제를 해결해나가는 방법을 소개할 것이기 때문에 함수와 관련된 문법은 자연스럽게 익숙해질 것으로 생각된다. 또한 그러한 접근 방식을 통해서 보다 분명하고 간결하게 문제를 해결하는 힘을 기를 수 있기를 기대해 본다.


  1. 이것은 일종의 추상화이다. 어떤 함수에 대해서 입력의 형식과 출력의 형식이 정해져 있다면, 실제 함수의 구현은 함수외부의 입장에서는 관심사가 아니다. 예를 들어 자연수 N을 입력으로 주면 1~N까지의 자연수의 합을 계산하는 함수가 있다고 가정해보자. 여기서 중요한 것은 10을 넣으면 55가 계산되어 나온다는 점이며, 그 내부의 구현이 루프를 돌면서 누적값을 더해나가든, 삼각수 공식을 사용하여 계산하든하는 점은 함수를 사용하는 입장에서는 몰라도 된다는 점이다. 이것은 반대로 함수를 구현하는 입장에서도 중요한데, 입력과 출력의 형식만 똑같이 유지한다면 함수 내부의 구현 코드를 어떻게 바꾸든 그것은 그 함수를 사용하는 부분의 전체 코드를 변경할 필요가 생기지 않는다는 점이다. 결국 함수에서 그 내부와 외부가 공유해야 하는 정보는 함수의 입력과 출력의 형식이다. 
  2. 수학에서 함수를 합성하여 제 3의 함수를 만들 수 있는 것처럼, 프로그램을 함수로 보는 이 관점에서 결국 프로그램은 프로그램 내부에 정의된 함수들을 정교하게 합성한 합성함수로 간주할 수 있다. 
  3. 이렇게 변수 이름앞에 *가 붙은 것을 언팩 연산자라고 하며, 이는 컴마로 쓰여진 일련의 값들을 튜플로 바인딩하는 연산자이다. 

(연재) 파이썬은 처음이라 – 문법은 처음이라 : 예제편

지난 번 문법에 대해 설명하면서, 각 문법의 패턴에 한정해서 설명하면서 코드 소개를 가능한 피했었다. 이번 글에서는 각 구문별 코드의 예제와 각각의 코드가 어떤식으로 실행되는지를 설명하는 시간을 갖도록 하겠다. 또 이 작성하는 예제코드에는 몇 가지 기본 입출력 관련 함수가 등장한다. 내장 함수에 대한 토픽을 따로 마련할 생각이지만, 워낙 기본적인 함수나 메소드들은 진도에 무관하게 그 때 그 때 소개하도록 하겠다. 먼저 기본적인 입출력 함수와 더불어서 간단한 바인딩 구문을 소개해보겠다.

먼저 두 개의 함수를 소개한다. input, print 라는 이름의 함수이다. 함수에 대한 자세한 설명은 일단 미루도록 하고, 파이썬에서 함수는 어떤 입력값을 받아서 내부에서 처리한 출력값을 반환하는 장치를 말한다. 파이썬 문법에서 함수는 그 이름 뒤에 괄호를 붙여서 표현하며, 인자값(arguments)을 받는 함수는 그 괄호안에 인자를 넣어서 호출한다.  input 함수는 인자 없이도 호출이 가능한 함수인데, 사용자로부터 입력된 한줄의 문자열을 리턴한다. 즉 input() 이라고 표현하면, 이 또한 표현식이며, 그 표현식은 사용자가 키보드를 두드려서 엔터까지 친, 사용자로부터 입력받은 문자열로 평가된다. input() 함수가 가장 기본적인 입력 함수라 한다면, 출력을 위해서는 print()라는 함수가 있다. print() 함수는 인자로 전달된 값을 문자열로 바꿔서 그 내용을 화면에 출력한다. print() 함수는 입력값이 뭐가되었든, 그 내용을 출력하고 표현식 자체는 None으로 평가된다.

 

name = input() #1
print(name) #2

이 두 라인의 간단한 코드는 하나의 완전한 프로그램이며 각각의 라인은 다음과 같이 해석된다.

  1. 첫 번째 라인의 구문 구조는 바인딩 구문(binding statement)이며, input() 함수의 결과값에 name이라는 이름을 붙인다.
  2. name의 값을 출력하기 위해 print 함수에게 name을 인자로 전달한다.

위 소스 코드를 print_name.py 라는 이름으로 저장하고 실행해보자. 프로그램을 실행하면 처음에 아무일도 일어나지 않는다. 단지 커서가 깜빡거릴 뿐이다. 하지만 우리는 이 때 콘솔에 키보드로 어떤 문자열을 입력할 수 있다. 아무 글자들이나 입력한 후에 엔터키를 누르면 방금 입력했던 내용이 그대로 출력된다.

첫번째 라인에서 input() 함수를 평가하는데에는 제법 긴 시간이 걸린다. 왜냐하면 이 함수는 “우리가 키보드로 입력한 값”으로 평가되어야 하기 때문에 실제로 엔터키를 입력하는 시점까지는 평가를 지연하고 기다리게 된다. 무엇이되었든 글자들을 입력하고 엔터키를 치게되면 다음과 같은 일들이 벌어진다.

  1. 바인딩 구문은 우변이 먼저 평가된다. input() 함수가 평가되면서 우리가 입력한 문자열 값이 된다.
  2. 좌변으로 바인딩되면서 name 이라는 이름이 그 문자열을 가리키게 된다.

어떤 이름이 값을 “가리킨다” 혹은 “참조한다”는 표현이 너무 추상적이고 어렵다면, 어떤 종이 상자에 문자열을 집어넣고 거기에 name 이라고 쓰여진 이름표를 붙여두었다고 생각하면 된다. (실제로 이 비유는 파이썬의 바인딩에 대한 엄청나게 적절한 비유이다.)

두번째 라인을 보자. 두 번째 라인은 그냥 표현식 하나가 달랑 쓰여 있다. print()라는 함수를 평가하는데, 괄호 안에 name 이라는 이름을 집어 넣었다. print() 함수는 “주어진 값을 문자열로 변환해서 화면에 출력한다”고 했다. 사실 값을 받는 것이 아니라, 값의 이름표를 받는다고 생각하면 된다. print() 함수의 내부에서는 name 이라는 이름표를 가지고, 좀 전에 문자열을 담아두었던 상자를 찾고, 그 내용물을 화면으로 출력한다.

이 두라인이 연속적으로 평가된, 최종 결과는 None 이다. 이 프로그램은 어떤 값도 만들어내지 않았지만, 프로그램의 외부 세계에 대해서는 “어떤 일”을 하였다. 프로그램 외부의 시선으로 바라봤을 때, 이 두줄 짜리 프로그램은 다음과 같은 일을 했다.

키보드를 두드린다 ==> { 마법같은 프로그램 } ==> 화면에 출력된다.

바인딩 구문을 사용하는 몇 가지 예를 더 보도록 하자.

x = 51
y = 43
z = (x + y) / 2  
print(z) 

프로그램을 실행하면 47.0이 출력된다.  첫 줄부터 세번째줄까지 3개 라인은 각각 바인딩 구문이다. 51 이라는 정수값에 x라는 이름을, 47이라는 정수값에 y라는 이름을 붙인 후, (x + y) / 2 라는 표현식을 평가하고 그 값을 z라는 이름에 바인딩했다. (그 결과는 47.0) 그리고 마지막 라인은 이 z를 출력하기 위해 print 함수를 호출했다.

정수를 입력받기

키보드를 통해서 입력 받은 숫자를 더하여 출력하는 프로그램을 작성해보자. 바로 위에서 input과 print 함수를 소개했고, 정수값은 더하기 연산을 할 수 있으니 간단하게 만들 수 있을 것 같다. 다음과 같이 말이다.

a = input()
b = input()
c = a + b
print(c)

뭔가 좀 이상하다. 프로그램을 실행하고 처음에 1을 입력하고 엔터, 그리고 2를 입력하고 엔터를 하면 3이 나와야 할 것 같지만 실제로는 12가 출력된다. 어디가 잘못되었을까? 바로 input()을 평가한 값의 타입을 잊었었던 것이다.

input 함수는 문자열을 리턴한다고 했다. 즉 input()의 표현식은 문자열타입 값으로 평가된다. 결국 1, 2를 각각 입력했다면 c = a + b 라는 구문은 c = "1" + "2" 이므로 "12"로 평가되고 그것이 출력된 것이다. 따라서 입력된 숫자를 가지고 정수값을 만들어줄 어떤 마법이 필요하다. 이 때 사용하는 마법은 바로 int() 함수이다.1 int() 함수는 정수로 바뀔 수 있는 어떤 것을 넣어주면 그것을 정수로 바꾼 값을 내놓는 함수이다.2  여기서 우리가 입력하는 글자들이 모두 숫자라면 실제로 정수를 만들어준다. 만약 “apple”, “banana”와 같은 단어를 입력하면? 글쎄 나는 그것을 정수로 바꾸는 방법을 모른다. 아마 파이썬 해석기도 그 방법을 모를 것이다. 숫자로 해석될 수 없는 문자를 집어넣으면 ValueError가 났다면서 프로그램이 중단될 것이다.

a = input()
x = int(a)
b = input()
y = int(b)
z = x + y
print(z)

이번에는 아주 잘 동작한다. 그런데 잠깐 여기를 보자. input()이라는 표현식은 문자열이라고 했다. 여기서는 그 값을 a라는 이름에 붙인 후 곧바로 다시 int()함수에 인자로 전달했다. 즉 함수의 인자로 전달 되는 것은 값이므로 이름 대신에 표현식을 그대로 쓸 수 있다. 즉, int(input()) 이라고 쓸 수 있다는 것이다. 같은 방식으로 z라는 이름을 보면, 이 이름은 중간 계산 결과를 가리키는 이름이다. 이 경우 z를 원래의 a + b로 치환하여  print(a + b) 라고도 쓸 수 있다.

a = int(input())
b = int(input())
print(a + b)

그렇다면, 다음과 같이도 쓸 수 있는 것 아닌가?

print(int(input()) + int(input())

맞다. 다만 이건 괄호가 너무 난무해서 약간 읽기가 힘들어진다. (사람에 따라서는 뭐 이정도는 쉬울지도 모르겠다.) 어떻게 쓰던지 모두 “정상적인” 코드이며 동작에 문제는 없다. 대신에 파이썬에서는 “간결하고 분명하고 읽기 좋은 것”이 가장 좋은 것이다. 어느쪽을 선호하든지 그것은 본인의 몫이다. 다만 첫번째 코드처럼, 축약할 수 있는 부분들까지 전부 무시하고 일일이 바인딩하고 쓰는 것은 그렇게 좋은 습관은 아니다. 그것이 매우 읽기 쉬운 것은 사실이나, 간단한 정보를 전달하기 위해서 너무 너저분하게 쓴다는 느낌을 준다.

그러면 어느 수준으로 축약하는 것이 좋을까? 그것은 “지금 어떤일을 하고 있는가”를 적절히 표현하는 수준에서 정하는 것이 맞다. 위 코드가 하는 일을 말로 표현하면 “두 개의 정수를 입력 받아, 그 합을 출력한다”이다. 따라서 두 개의 정수를 입력 받고, 더해서, 출력하는 수준을 코드로 표현하면 된다. 그리하면 세 번째의 세 줄로 표현된 코드가 가장 적당해 보인다.

IF문

IF문은 특정한 조건식에 따라서 프로그램의 진행 방향이 달라지는 분기문이라고 했다. 간단한 몇 가지 예를 들어보자. 다음은 입력한 값이 홀수인지 짝수인지를 판단하여 그 결과를 출력하는 프로그램이다.

n = int(input('숫자를 입력하세요')) #1
if n % 2 == 0: #2
  print("짝수입니다.") #3
else:
  print("홀수입니다.") #4

위 코드의 로직은 다음과 같다.

  1. 맨 처음에는 키보드로부터 수를 하나 입력받고, 계산을 위해서 정수로 변환했다.
  2. 짝수는 2로 나눈 나머지가 0인 수이다. 나머지 연산자를 써서 n % 2 를 구하고 그것이 0과 같은지 검사한다.
  3. 그 결과가 참이라면 “짝수입니다.”를 출력한다.
  4. 그 결과가 거짓이라면 “홀수입니다.”를 출력한다.

조건에 따라 분기한 후 다시 다른 조건으로 분기하는 경우도 생각할 수 있다. 예를 들어 2의 배수이면서 5의 배수인 수가 있다면 이 수는 2와 5의 공배수이며 즉 10의 배수가 된다. 이 과정을 표현해보자. 그리고 짝수가 아닌 경우에도 5의 배수가 된다면 이를 출력해보자.

n = int(input())
if n % 2 == 0:
  if n % 5 == 0:  #1
    print('10의 배수입니다.') #2
  print('짝수입니다.')  #3
elif n % 5 == 0:  #4
  print('5의 배수입니다'.) 
else: #5
  print('그냥 홀수입니다.')

위 코드가 실행되는 과정을 살펴보자.

  1. 먼저 n이 짝수로 판정된다면 다시 5의 배수인지를 검사받는다.
  2. 짝수이면서 5의 배수이므로 “10의 배수입니다”가 출력된다.
  3. 이 문장은 5의 배수 조건문의 블럭이 아니다. 따라서 10의 배수여부에 상관없이 짝수라면 항상 출력된다. 만약 n이 10의 배수라면 “10의 배수입니다.”와 “짝수입니다.”가 나란히 출력될 것이다.
  4. 짝수가 아니라면 5의 배수인지 다시 검사한다.
  5. 짝수도 아니고 5의 배수가 아니라면 “그냥 홀수입니다”가 출력된다.  4번에서 “5의 배수입니다”가 출력되었다면, 이 문장은 출력되지 않을 것이다.

ELIF 절을 사용해야 하는 가장 대표적인 문제는 주어진 점수에 대해서 점수 구간을 두고 평점을 계산하는 프로그램이다.

어떤 학교의 평점은 A, B, C, D, F로 나뉘어집니다. 이 평점은 시험 점수의 구간에 따라 구해집니다. 각 구간은 다음과 같습니다.

  • 50점 미만인 경우 F를 받습니다.
  • 50점 이상 65점 미만인 경우 D를 받습니다.
  • 65점 이상 75점 미만인 경우 C를 받습니다.
  • 75점 이상 85점 미만인 경우 B를 받습니다.
  • 85점 이상인 경우 A를 받습니다.

먼저 특정 구간에 대해 어떤 값이 있는지를 어떻게 검사하는지 살펴보자. “50점 이상 65점 미만”인 경우를 어떻게 체크할까?

  • 점수값을 N 이라 하자
  • “50점 이상”은 N >= 50 으로 표기할 수 있다. 혹은 방향을 바꿔서 50 <= N 으로 표기할 수 있다.
  • “65점 미만”은 N < 65로 표기할 수 있다.
  • 50점 이상, 65점 미만은 위의 두 조건을 동시에 만족해야 한다. 예를 들어 70점은 65점 미만의 조건을 만족하지 않기 때문에 이 범위에 들지 않는다.
  • 각각의 부등식은 참/거짓으로 판단되며, 두 부등식을 동시에 만족해야 하는 경우 and를 사용해서 결합한다.
  • 따라서 N >= 50 and N < 65 라고 쓸 수 있다.
  • 그런데, 파이썬의 부등식은 연쇄적으로 결합할 수 있다. 따라서 50 <= N < 65 로 쓰는 것 역시 허용된다.

그렇다면 다음과 같이 if, elif, else를 써서 코드를 작성할 수 있을 것이다.

n = int(input())
if n < 50:
  print("F")
elif 50 <= n < 65:
  print("D")
elif 65 <= n < 75:
  print("C")
elif 75 <= n < 85:
  print("B")
else:
  print("A")

다만, 하나의 구간에 대해서 50 <= n < 65를 보는 것은 맞는데, 이 로직을 따라가보면 우선 50점 미만인지 체크한 다음에 두 번째로 50이상 65미만 조건을 체크한다. 만약 N이 50 미만이었다면 앞의 IF 절에서 분기했기 때문에 50이상이라는 조건을 이미 만족하는 상태로 ELIF 절을 만난다. 따라서 각 ELIF 절의 맨 왼쪽 부등식은 사실은 항상 참이 되므로 필요없는 구문이된다. 즉 elif n < 65: 만 있어도 이 전체 로직은 문제없이 작동할 것이다.

WHILE 문

WHILE 문은 특정 조건을 만족하는 한 루프를 계속 반복하라는 구문이다.  WHILE 문의 블럭 내에서는 특정한 값을 변경하는 동작을 포함하며, 보통 이 변경되는 값 중의 하나를 루프의 종료 조건으로 사용한다. WHILE문을 이용해서 1부터 10까지의 자연수의 합을 구해서 출력하는 코드를 작성해보겠다.

s = 0
i = 1
while i <= 10:
  s += i
  i += 1
print(s)

이 코드에 대한 설명이다.

  • 먼저 s는 0으로 초기화하는데, 각 자연수를 누적해서 더한 값을 가리키는 이름이다.
  • i는 매번 증가하는 자연수가 된다. 그 초기값은 1이다.
  • while i <= 10: 을 통해서 i 가 10 이하인 경우에 계속 반복한다는 것을 지정한다.
  • s 는 i 만큼 증가하고,
  • 다시 i는 1씩 증가한다.
  • i 가 10일 때 s 에 한 번 더해진 후, i += 1 을 거쳐서 11이 될 것이다.
  • i가 11이 되면 조건을 더 이상 만족하지 않기 때문에 블럭은 더 이상 실행되지 않고 건너뛰게 된다.
  • s가 출력된다.

WHILE문의 조건이 블럭 외부에 있을 때도 있지만, 블럭 내부에서 정의되는 값이라면 조건절에 걸 수 가 없다. 예를 들어 5보다 큰 수가 입력되는 동안 입력된 값을 출력하려고 한다면,

n = int(input())
while n > 5:
  print(n)
  n = int(input())

똑같은 구문을 WHILE 블럭 안과 밖에 같이 써야 한다. 이것은 단지 WHILE의 조건절에 필요하기 때문에 이렇게 쓴 것이다. 이런 경우에는 무리해서 종료 조건값을 앞에서 정의하지 않고 while True: 로 무한 루프를 만들고, 반복 블럭 내에서 if + break로 탈출하는 패턴을 사용한다.

while True:  #1
  n = int(input())  #2
  if n <= 5:  # 3
    break
  print(n)  #4
  1. while True: 에서 보듯 조건은 항상 참이므로 이 루프는 무한루프를 돌게 된다.
  2. n은 블럭 내에서 처음 선언된다.
  3. 조건식을 통해서 break로 빠져나가는 길을 만들어 놓고
  4. 조건을 만족하지 않으면 출력한 후에 다시 while로 돌아간다.

CONTINUE 구문의 경우 잘 쓰면 코드를 깔끔하게 하는데 도움이 될 수 있다.  아래 코드가 어떤 일을 하는지 살펴보자.

i = 0
while i < 50:
  i += 1
  n = int(input())
  if n < 10:
    continue
  m = int(input())
  print(m + n)
  1. i 와 i < 50 으로 보아, 이 반복은 50번 동안 반복하게 된다.
  2. 루프를 시작하면 시행 카운터인 i 값을 올리고,
  3. 정수 하나를 입력받는다. (n)
  4. 입력받은 정수가 10보다 작으면 이후 작업을 생략하고 whie로 돌아간다.
  5. 입력받은 정수가 10이상이면, 또 다른 정수를 하나 더 입력받고
  6. 그 합을 출력한다. 그리고 다시 while로 돌아간다.

WHILE 문 정리

while문은 반복되는 참인동안 반복되는 if문이라고 생각하면 된다. 마지막 예제와 같이 특정 횟수만큼 반복되는 경우에는 for 문을 사용하는 것을 추천한다. 코드 자체가 간결해지는 효과도 있으면서, 흔히 하기 쉬운 실수를 방지할 수 있다. while 문을 사용할 때 흔히 하기 쉬운 실수로는 위의 경우에 i += 1 을 쓰는 것을 깜빡하고 빼먹어서 무한 루프를 돌게 된다거나, 종료 조건에서 i < 50 이라고 써야 할 부분을 i <= 50 이라고 쓰거나 혹은 그 반대로 써서 정확한 횟수만큼 반복하지 못하는 경우들이 있다.

따라서 예외적으로 전체 반복횟수를 알 수 없는 경우에 while문을 쓸 것.  그리고 가급적이면 탈출 조건을 명확하게 하기 위해서 중도에 탈출하는 무한루프의 형태로 사용하는 것을 권한다. 따라서 보다 권장되는 코딩 습관에 따라 위 코드를 다시 쓰면 다음과 같다. (그리고 억지로 continue문을 썼지, 이 문제는 continue가 없어도 된다.)

i = 0
while True:
  ## 탈출조건을 확인하고
  if i >= 50:
    break
  ## 탈출이 아닌 경우 조건 변수값을 변경한다.
  ## 이후, 반복 코드를 작성한다.
  i += 1
  n = int(input())
  if i > 10:
    m = int(input())
    print(m + n)

FOR 구문

FOR문은 파이썬에서 가장 유용하며 중요한, 강력한 반복구문이다. 파이썬에는 여러가지 타입의 “반복가능한” 값들이 있는데, FOR 문은 이 반복가능한 값에 대해서 매 반복을 시행하는 구문이다. 파이썬의 반복가능한 값에 대해서는 별도의 토픽에서 따로 설명하도록 하고 여기서는 간단한 FOR 문의 예제 몇 가지를 보도록 하자.

FOR문에서 일반적으로 가장 흔히 만나는 함수가 하나 있는데, 바로 range() 함수이다. 이 함수는 특정한 정수값 사이의 범위를 만드는데, 이 때 만들어지는 값이 반복가능한 값이다. range() 함수는 다음과 같은 세 가지 사용 방법이 있다.

  • range(10) : 0, 1, 2,… ,9까지의 10개의 값 범위를 만든다. 중요한 것은 10으로 주어지는 종료값은 범위에 포함하지 않으며, 범위의 시작이 0이라는 점이다. 따라서 범위에는 종료값은 포함되지 않지만, 전체 범위의 정수값은 종료값과 같은 개수를 가진다. (종료값이 범위에 포함되지 않는 것은 파이썬 여러 문법에서 일관적으로 나타나는 특성이다)
  • range(3, 10) :  이렇게 쓰이는 경우에 첫번째 값인 3은 시작값이며, 두 번째 값인 10은 종료값이다. 이 때에도 10은 포함되지 않으므로 이 범위는 실제로 3, 4, 5, 6, 7, 8, 9 가 해당한다.
  • range(3, 10, 2) : 종료값 뒤에 값을 하나 더 쓰면 이것은 간격으로 취급된다. 이 범위는 3, 5, 7, 9로 시작값으로부터 종료값보다 작은 범위 내에서 간격만큼 늘어난다. 참고로 간격이 음수로 주어지는 경우에는 시작값이 종료값보다 커야 한다. range(10, 3, -2)는 10, 8, 6, 4의 범위를 가리킨다.

즉, 복잡한 기술적인 개념이 없이, range() 함수는 특정한 범위의 연속적인 수열을 만들어내는 함수라고 생각하면 된다. 그 결과는 수열의 각 항에 대해서 반복가능한 값이다. 따라서 for 문은 range() 함수와 흔히 같이 쓰인다. 다음은 0~9까지의 정수를 출력하는 코드이다.

for i in range(10):
  print(i)

이 코드는 이렇게 이해하면 된다.

  1. range(10) 에 의해서 0, 1, 2, … , 8, 9의 정수들이 준비된다.
  2. i 는 매번 각각의 정수값이 되어 print(i)는 이를 출력하게 된다.
  3. 준비된 값들을 모두 사용하고나면 for 루프가 종료된다.

만약 이 코드와 동일한 동작을 하는 while 문 코드를 쓴다면 다음과 같을 것이다.

i = 0
while i < 10:
  print(i)
  i += 1

for i in range(10): 이라는 표현이 낯설어서 어렵게 느껴질 수 있겠지만, 이 FOR문은 WHILE문보다 훨씬 간결하다. 종료조건과 조건값의 갱신을 매번 수동으로 지정할 필요가 없으며, 따라서 이 과정에서 발생할 수 있는 실수의 여지가 없어진다.  FOR문을 쓰기 위해서는 IN 절에 사용할 반복가능한 어떤 값이 필요하지만, 이미 존재하고 있는 값을 쓰거나, range()를 이용해서 간단하게 정의할 수 있기 때문이다.

for 문의 본체가 되는 블럭은 해당 for 문에 의해서 반복되며, 블럭 내에는 또다른 if, while, for 문이 올 수 있다.  다음의 예는 구구단을 2단부터 9단까지 출력하는 예이다.

for x in range(2, 10): #1
  print(x, '단')  #2
  for y in range(2, 10): #3
    print(x, '*', y, '=', x * y) # 4
  print() #5
  1. x는 2~9사이의 범위이다. 이 코드에서 x는 2단, 3단, 4단..의 단을 의미한다.
  2. 각 단을 출력하기에 앞서서 ‘2 단’과 같이 현재 출력할 단의 이름을 한 번 출력한다. print() 함수는 컴마로 구분되는 여러 개의 인자를 넣어줄 수 있다. 각 인자는 공백으로 분리되어 순서대로 출력된다.
  3. y는 다시 2~9사이의 범위이다. 이 반복문은 각 단에 대해서 매번 반복되며, 각 단의 대표 숫자를 몇 배 할 것인지를 나타내는 값이다.
  4. print() 문을 사용해서 각 라인을 출력한다. x는 현재단의 값으로 고정되며, y는 각 단에서 2~9로 바뀌어나간다. x * y를 계산하여 그 결과까지 출력하게 된다.
  5. 각 단의 출력이 끝나면 구분을 위해서 빈 줄을 하나 출력할 것이다. print()에 아무런 값이 전달되지 않으면 빈 줄만 출력된다.

이 와 똑같은 동작을 하는 코드를 while 문을 쓴다면 어떨까? 먼저 각자가 한 번 코드를 작성해서 출력해보자. 그리고 단 한번에 제대로 출력되는 코드를 작성하였는지 체크해보자.

x = 2
while x < 10:
  print(x, '단')
  y = 2    ## 매 단마다 y를 초기화해야 한다. 이 위치가 틀리면 어이없는 결과가 출력된다.
  while y < 10:
    print(x, '*', y, '=', x * y)
    y += 1  ## 우주끝까지 출력하고 싶지 않다면 y를 잘 업데이트하라
  x += 1    ## 같은 이유로 x도 빼먹지 않고 업데이트를 잘해야 한다.

특히 이처럼 반복문이 중첩되는 경우에 WHILE 문이라면 조건을 변경시키는 코드의 위치가 잘못되거나, 아예 이런 코드를 빼먹기 쉽다. 따라서 앞으로는 피할 수 없는 상황이 아니면 WHILE 문은 쓰지 않는 것으로 알고 있자.

FOR – IN 문에서 사용되는 반복가능한 값에는 range 객체(range() 함수가 리턴하는 값)외에도 리스트나, 문자열, 집합, 사전…등 단일 값이 아닌 여러 원소의 집합으로 간주되는 값이 올 수 있다. 이러한 “반복가능한” 값을 파이썬에서는 연속열, 즉 시퀀스(sequence)라고 한다. 시퀀스는 그 정확한 타입을 막론하고 반복가능한(iterable) 공통적인 특성을 갖는다. 어떤 리스트 A의 각 원소에 대해서 그 제곱의 값을 출력하는 코드를 보자.

A = [3, 7, 2, 9, 4]
for a in A:
  print(a * a)

반복문의 관점에서 for 는 어떤 연속열의 각 원소에 대해 동일한 작업을 반복하는 것으로 볼 수 있는데, 한 편으로는 다음과 같이 생각할 수 있다.

  • 리스트나 그외 다른 연속열들은 결국 하나의 값이며, for 문은 이러한 연속열 하나에 대해서 어떤 코드 블럭을 그 내부로 밀어넣어 모든 원소가 동일한 변형을 적용받도록 하는 연산이다.
  • range에 대해서 생각해보면, 이는 기본적으로 하나(혹은 두 세 개)의 정수값으로부터 여러 개의 정수값을 만들어내는 역할을 담당한다. 즉 하나의 값이 여러 개의 값으로 populating되게 하는 문맥으로 이해할 수 있다.

  1. 앞으로 함수와 같이 실행가능한 이름을 언급할 때에는 ()를 뒤에 붙이도록 하겠다. 
  2. 여기서 함수는 어떤 값 -(변환)-> 다른 값으로 만드는 변환장치의 역할을 한다. 너무나 기초적인 비유라 보통 이 중요성을 잊기 쉬운데 여기에 주목하는 습관을 가지도록 하자. 

LiveScript 살펴보기 – 03 함수

LS에서 함수는 일반 문법 편에서 잠깐 언급했듯이 화살표를 써서 간단히 정의할 수 있다. 이 함수 표현에서 중요한 점 두 가지는 첫 째 우변은 하나 이상의 표현식이라는 점과 표현식이 순서대로 나열되는 경우 맨 마지막 표현식의 결과가 자동으로 리턴된다는 것이다.

함수

LS는 함수형 프로그래밍 언어의 스타일을 많이 도입했다고 하였다. 비록 LS가 진짜 순수한 함수형 언어는 아니지만, 함수형 언어의 스타일을 도입한다는 것은 LS내의 함수라는 것은 가급적 아래와 같은 특징을 갖도록 디자인되어야 한다는 것이다.

  1. 순수성 : 함수의 결과값이 순수하게 파라미터에만 의존할 것. 따라서 입력된 인자가 같다면 항상 리턴될 출력값도 같음을 보장한다.
  2. 간결성 : 사실 억지로 만든 말이기는 한데, 함수에 대한 연산 (커링, 바인딩, 파이핑, 합성)이 다양하고, 함수 자체가 1급 시민인 점(이는 JS로부터 자연스럽게 물려받는 특징이다.)에 착안하여, 가능한 간결하고 명료한 함수들을 정의하고, 이러한 함수들을 조합하여 필요한 함수를 생성하는 방법을 지향하는 것이다.

이전에 예에서 리스트의 최대값을 찾는 getMax 함수를 잠깐 언급한 바, 있는데 이 함수는 JS로 쓴다면 아래와 같을 것이다.

function getMax(arr) {
  if(arr.length < 1) { return null }
  var x = arr[0];
  var i = 1;
  var result = x;
  while(i < arr.length) {
    if (x < arr[i] ) {
      x = arr[i];
      result = x;
    }
    i += 1;
  }
  return result;
}

그리고 LS에서는 다음과 같이 쓴다.

get-max = (arr) -> arr.reduce (>?)

너무 심하게 부풀려서 비교한다고 생각할 수 있는데, 물론 JS에서도 함수형 스타일로 맵/필터/리듀스를 할 수 있는 API가 Array 타입에 존재하고 있다. 따라서 위 LS 코드를 JS로 쓴다면 (그것은 마치 함수형 스타일의 JS 코드겠지만) 다음과 같이 쓸 수 있다.

var getMax = function(arr){ 
  return arr.reduce(function(x, y){ 
    return x > y ? x : y; 
  }
);

다만, 여기서 말하고자 하는 것은 LS로 쓰면 함수를 엄청 짧은 코드로 쓸 수 있다는 점이 아니라, “두 수 중에서 큰 수를 판단하는 함수”를 리스트의 각 원소에 대해서 순차적으로 적용하면 리스트 내에서 가장 큰 수를 찾을 수 있다”는 간단한 아이디어에 관한 것이다. 두 개의 연산(함수)에 대해 이해하고 그것을 연관지어 원하는 기능을 쉽게 조합할 수 있는 것이 함수형 스타일의 가장 큰 특징이라 할 수 있겠다.

리턴

함수의 본체가 하나의 표현식인 경우에는 LS의 함수 정의 문은 one-liner로 작성된다. 두 개 이상의 표현식이 함수의 본체를 구성하는 경우에는 -> 뒤에서 줄을 바꾸고 들여써서 블럭을 구분하여 표기할 수 있다.

times = (x, y) -> x * y
sum = (arr) ->
  s = 0
  for i in arr
    s += i
  s

마지막 표현식은 자동으로 리턴되므로, 명시적으로 리턴이 없는 함수를 정의하고 싶다면 !-> 기호를 써서 정의한다.

호출

함수 호출 시, ()를 생략한다. 또한 인자들은 callable하지 않다면 콤마를 생략하고 나열할 수 있다. 인자를 받지 않는 함수는 !를 써서 호출됨을 표시한다. 그리고 인자 뒤에 오는 and, or, xor, 한 칸 띈 후의 ., ?. 등은 모두 암묵적으로 호출 구문을 완료한다. 따라서 공백을 기준으로 체이닝 표기를 간단히 할 수 있다.

$ \h1 .find \a .text! #=> h1 a 의 내용
# $('h1').find('a').text()

f!
[1 2 3].reverse!slice 1 #= [2, 1]

익명함수의 경우, 인자가 없는 케이스에는 do를 -> 에 쓰면 이를 호출한다. 간단하게 익명함수를 이용해서 여러 표현식을 묶은 블럭을 만들고 실행하는 방법으로 이해.

do -> 3 + 2 
#=> 5
#(function(){
#  return 3 + 2;
#})();

do를 이름이 있는 함수1와 썼을 때, 그리고 do가 표현식에 쓰인게 아니라면 이름지은 함수는 유지된다. 즉, do를 평가하기 전에 함수를 참조할 수 있고, do 문을 만났을 때 한 번 더 실행하는 것이다.

i = 0
f 9 #f를 한 번 실행. i == 1
i #=> 1
do function f x # 여기서 한 번 더 실행
  ++i
  x
i # => 2

축약된 객체 블럭 문법은 함수 호출 구문에서 사용할 수 없다. 단, 한 줄에 쓰여진 리터럴은 인식된다. 함수의 인자 중 하나로 객체 블럭을 쓰고자 한다면 do를 이용해서 블럭을 명시해야 한다.

func
  a:1
  b:2
## 컴파일 되지 않음

## 대신 이렇게 쓸 수 있다.
func a:1, b:2

## 보통은 do를 사용한다.
func do
  a: 1
  b: 2

이처럼 do는 함수 호출 시에 요긴하게 많이 쓰인다. 특히 개별 인자를 각 라인의 표현식으로 사용하려할 때 유용하다.

함수의 중위표현

파라미터를 2개 받는 함수는 백팃으로 둘러 싸서 중위연산자처럼 쓸 수 있다.

add = (x, y) -> x + y
2 `add` 3 #=> 5

g = (a, b) ->
  add ...     # 함수 내에서 쓰이는 `...`은  인자를 그대로 넘긴다는 의미로 해석할 수 있다. 
# 11

그리고 함수의 본체 내에서 …을 쓰면 암묵적으로 모든 인자의 리스트로 인식한다. 이는 특히 super를 호출할 때 유용하다.

파라미터

파라미터는 표현식을 쓰는 표기로도 확장된다. 즉 객체나 다른 파라미터에 관한식으로 파라미터를 받으면, 해당 값으로 맵핑된다는 이야기이다. 이를 통해서 함수 본체에서 각 파라미터간의 관계를 재설정해야 하는 부담을 줄 일 수 있다. 아래의 예제를 보자.

set-person-params = (
  person
  person.age        # 두 번째 인자는 첫 번째 인자의 .age 키에 배당된다.
  person.height ) -> person

p = set-person-params {}, 21, 180cm
# {age: 21, height: 180}

# 'this' 확장하기
set-text = (@text) -> this
# var setText = function(text){ this.text = text; return this; }

위의 setPersonParams() 함수는 세 개의 인자를 받는데, 그 중 두 번째, 세 번 째 인자는 첫 번째 인자의 프로퍼티로 명시되어 있다. 따라서 함수 호출 시에 해당 값이 주어지면, 이는 첫번째 인자로 넘겨진 객체의 프로퍼티로 자동으로 세팅된다. 따라서 함수 본체 내에서 person.age = age와 같은 처리를 따로 하지 않고 인자 자체를 확장하여 person.age로 쓰는 것으로 대체가능하다.

특히 이 확장은 객체의 메소드나 함수를 작성할 때, 특히 this를 다룰 때 매우 간결하게 쓰일 수 있다.

디폴트 값

파라미터에는 디폴트 값을 미리 지정해줄 수 있으며, (파이썬 스타일), 좀 헷갈릴 수는 있는데, 논리 연산을 수행하여 디폴트값/오버라이드를 적용할 수 있다.

add = (x && 4, y || 3) -> x + y
# x는 무조건 4로 오버라이드되고
# y는 없으면 3이 된다.

또한 객체를 통으로 받아서 특정 키를 분해해 낼 수 있다.

set-coords = ({x, y}) -> "#x, #y"
set-coords y:2, x:3 #=> "3, 2"

# 그리고 그 와중에 다시 디폴트 값을...
set-coords = ({x = 1, y = 3}) -> "#x, #y"

그리고 ...y 등과 같이 일련의 인자들을 하나의 리스트로 취급하는 것도 가능하다.

f = (x, ...ys) -> x + ys.1
f 1 2 3 4 #=> 4

이외에도 캐스팅 연산자를 파라미터에 붙일 수 있는데, 그러면 자동으로 평가된 후 들어간다.

f = (!!x) -> x
f 'truely' # true

g = (+x) -> x
g ' ' # 0

obj = {prop: 1}
h = (^^x) ->
  x.prop = 99
  x
h obj
obj.prop # 1

파라미터의 생략

JS의 함수는 호출 시 파라미터 개수에 크게 구애받지 않는다. 즉 선언된 파라미터보다 부족한 개수의 인자가 넘겨지면, 빈 인자는 undefined를 갖게되고, 인자가 과하게 많이 넘겨지면 함수의 로컬 스코프에 매핑되지 못한 인자는 모두 무시된다.

LS의 함수에 있어서 정의를 파라미터 없이 함수를 만들었다 하더라도 함수 본체에서는 파라미터를 참조하는 것이 가능하다. 사실 이는 JS의 스펙에 정의된 arguments2 객체에 의한 것이다.

단 인자 함수의 파라미터는 it으로 지칭한다. 그외의 파라미터는 모두 &0, &1 과 같은 식으로 번호 순서대로 참조하여 처리할 수 있다.

바운드 함수

바운드 함수는 특정 객체에 소유권이 묶인 함수를 말한다. 보통 함수 내에서 this를 참조하는 것은 해당 함수를 호출한 문맥이 되는데, 바운드 함수는 처음 바운드한 시점의 문맥이 유지된다. 바운드 함수의 자세한 내용에 대해서는 별도로 찾아보도록 하고, 바운드 함수를 작성하는 것은 ~> 를 써서 웨이브진 화살표를 쓴다는 점만 기억하자.

obj = new
  @x = 10
  @normal = -> @x  
  @bound = ~> @x

obj2 = x: 5
obj2.normal = obj.normal
obj2.bound = obj.bound

obj2.normal! # this.x 이고 이 때 this는 obj2 이므로 5
obj2.bound # 10. bound메소드는 obj에 바인딩되어 있으므로, 내부의 this는 obj를 가리킨다.

흔히 바운드 함수는 특정 객체의 메소드를 이벤트 핸들러를 사용하며, 그 내부에서 this를 참조할 때 유용하다.

- 대신 ~를 쓰는 것이 바운드 함수를 의미한다는 것은 매우 일관적으로 적용되며, !~>, ~~>, ~function 등의 표현이 그대로 사용될 수 있다.

커링

커링은 하스켈 커리의 이름을 따서 명명되었는데, 모든 다 인자 함수는 단인자 함수들이 합성된 상태로 볼 수 있다는 것을 말한다. 예를 들어 두 정수를 더하는 add 라는 함수가 있다면 다음과 같이 정의하고 호출할 수 있다.

add = (x, y) -> x + y
add 3 2

이 때, 이 호출식을 (add 3) 2라고 생각하는 것이다. 그러면 add 3은 정수 하나를 받아서 3을 더한 값을 리턴하는 단인자 함수가 된다.

그렇다면 다시, adda 라는 정수를 받아서 “b라는 정수를 받아 여기에 a를 더해서 리턴하는 함수”를 리턴하는 함수라고 생각할 수 있다.

add = (a) ->
  (b) -> a + b

add-one = add 1
add-one 2
# 3

이렇게 커링을 이용하면 쉽게 부분적용된 함수를 만드는 것이 가능해진다. 그리고 LS에서는 자동으로 커리되는 함수를 만들 수 있는 선언법으로 -->를 쓰는 것을 지원한다. 역시 같은 맥락에서 바운드된 커리드 함수는 ~~> 으로 선언할 숭 있다.

add = (a, b) --> a + b
add-one = add 1
add-one 2 #=> 3

접근자 단축

접근자 메소드/함수의 경우에 맵이나 필터 동작에 적용되는 경우 예를 들어 (x) -> x.prop과 같은 식으로 처리하는 것은 (.prop)으로 줄여 쓸 수 있다. 이는 특정 프로퍼티를 호출하는 것을 괄호로 둘러싼 것이기 때문에 메소드 호출 역시 같은 식으로 처리할 수 있다.

map (.length) <[ hello there you ]> #=> [5, 5, 3]
filter (.length < 4), <[ hello there you ]> #=> ['you']

map (.join \|) [[1 2 3], [7 8 9]]
#=> ['1|2|3', '7|8|9']

반대로 (obj.) 이라고 쓰는 것은 (it) -> (obj.it)의 단축 표현이 된다.

obj = one:1, two:2, three:3
map (obj.) <[one, three]> #=> [1, 3]

부분 적용

표현식 내에 언더스코어(_)를 사용하여 해당 표현식을 1개 인자로 받는 함수로 간주하고, 언더 스코어는 해당 인자의 위치를 표시하는 플레이스 홀더로 생각할 수 있다. 이는 커리드 함수에서 적절한 위치에 있지 않은 파라미터를 가변으로 남기고 싶을 때 유용하다.

# 여기서 쓰인 filter는 별도로 정의된 top-level의 함수라 가정한다.
filter = (fn, arr) --> arr fn
# 이 떄, 특정한 리스트에 대해서 고정하고 필터링 함수만 변경하려할 때,
# 다음과 같이 쓰게 되는데
filter-nums = (fn) -> filter fn, [1 to 5] # 
# 인자로 받게되는 fn의 위치를 `_`를 이용해서 표시해주면 된다.
filter-nums = filter _, [1 to 5]


filter-nums (<3) # [1, 2]

이러한 부분적용된 함수는 특히 고차 함수를 파이핑으로 연결할 때 유용하다. 아래 예제는 underscore.js를 이용해서 특정한 리스트를 조작하는 코드이다. “인자로 받은 객체가 다시 인자로 전해질 때”를 상정하기 때문에, _의 사용이 혼동되지 않고 해석될 수 있다.

[1 2 3]
|> _.map _, (* 2)
|> _.reduce _, (+), 0
# => 12

백 콜

콜백으로 주어지는 표현식들은 결국 블럭으로써, 들여쓰기를 적용해야 한다. 백 콜은 이러한 방식을 거꾸로 표현하여 콜백을 들여쓰지 않고 표현할 수 있게 해준다.

map (-> it * 2), [1 to 3] 

# 백 콜로 전환
x <- map _, [1 to 3]
x * 2  # 여기서부터는 _ 에 들어갈 표현을 쓸 수 있다. 

백 콜은 콜백을 들여쓰지 않게 해주기 때문에, 중첩되는 콜백지옥을 깔끔하게 처리해주는 장점을 가지고 있다. 만약 top레벨의 코드 중간에 백 콜을 쓰게 된다면, 이후 끝라인까지의 모든 내용이 콜백 내의 코드로 간주된다. 백 콜의 코드가 중간에서 끝나야 한다면 미리 do를 사용해서 들여쓰기 블록을 시작해주자.

do
  data <-! $.get 'ajaxtest'
  $ '.result' .html data  # $.get \ajaxtest의 콜백이며, 콜백의 인자는 data
  processed <-! $.get 'ajaxprocess', data  # 콜백 내에서 `ajaxprocess`에 대한 처리를 또 호출
  $ '.result' .append processed # 여기는 콜백 내의 콜백이지만, 들여쓰기는 더 이상 없다.

alert \hi

위 코드는 다음과 같이 컴파일 된다.

$.get('ajaxtest', function(data){
  $('.result').html(data);
  $.get('ajaxprocess', data, function(processed){
    $('.result').append(processed);
  });
});
alert('hi');

LET/NEW

함수와 관련하여 let, new에 대한 표현을 짚고 마무리하도록 하겠다. let은 특정한 익명함수를 생성하는데, 해당 함수 내에서의 특정한 문맥을 생성해준다. 즉 let A = B { 블럭} 의 형태이며, 이 때 블럭 내에서 언급되는 A는 모두 B로 치환된다.

let $ = jQuery
  $.isArray []

위의 이 표현은 $jQuery가 되는 블럭 스코프를 생성한 후에 $.isArray []를 평가하였으므로 true가 된다. 이 때 외부 스코프에 $이 있더라도 여기서는 자체 스코프만을 참조할 것이다. 비슷하게 아래와 같은 코드도 작성할 수 있다.

x = let @ = a:1, b:2
  @b ^ 3
x #=> 8

new는 새로운 익명 컨스트럭터를 만들어서 즉시 호출하는 개념이다.

doc = new
  @name = \spot
  @mutt = true

# {name: 'spot', mutt: true}

보너스

다음은 nodejs를 통한 간단한 HTTP 서버의 기본 구현을 LS로 작성한 것이다. 콜백속에서 또 콜백을 전달하는 형태의 함수호출 패턴이 존재하고, 체이닝이 쓰인다. 이를 do와 백콜을 이용하여 깔끔하게 작성할 수 있다.

require! <[ fs http url ]>
const ROOTDIR = 'html/'

do
  (req, res) <-! htttp.create-server
  url-obj = url.parse req.url, yes no
  do 
    err, data <-! fs.read-file ROOTDIR + url-obj.pathname
    if err? 
      res.write-head 404
      res.end <| JSON.stringify err
    else
      res.write-head 200
      res.end data
.listen 8080

참고자료

  • LiveScript.net
  • HTTP 서버 구현: https://mylko72.gitbooks.io/node-js/content/chapter7/chapter7_4.html

  1. named function. 여기서는 function 키워드를 써서 정의한 함수를 말한다. 
  2. arguements에 대한 MDN 설명 참조.