Swift는 escaping 클로저와 non-escaping 클로저에 차이를 두고 있다. escaping 클로저는 한번 호출되고나면 리턴값으로 클로저를 반환하는 함수이다. 클로저는 인자로 받은 함수 스코프를 escape한다.

클로저를 escape 하는 것은 종종 아래 예시처럼 비동기 컨트롤 플로우와 연관되있다. 
  • 함수가 백그라운드 작업을 시작하고 즉시 리턴하면, 완료 핸들러를 통해 백그라운드 작업의 결과를 알린다.
  • 뷰 클래스가 버튼 탭 이벤트를 다르기위해 프로퍼티에 클로저를 저장해둔다. 이 클래스는 사용자가 버튼을 탭 할때마다 클로저를 호출한다. 클로저 프로퍼티는 세터를 escape한다.
  • 여러분은 DispatchQueue.async를 사용하여 디스패치 큐에서 비동기 실행을 위한 작업을 스케줄링한다. 이 테스크 클로저는 비동기에 호출된 이후에도 소멸되지 않은 채 살아있다.
DispatchQueue.sync와 대조되는데, 이것은 리턴되기 전에 테스크 클로저의 실행이 끝나기 전까지 기다린다. 이 클로저는 절때 escape하지 않는다. 표준 라이브러리에 map, 다린 일반적인 sequence, 그리고 collection 알고리즘에도 동일하다.

escaping 클로저와 non-escaping 클로저의 차이가 왜 중요할까?
간단히 말해 메모리관리 때문이다. 클로저가 붙잡고있는 모든 오브젝트는 강참조로 들고 있으며, 클로저 안에서 self의 프로퍼티나 self의 메소드에 접근하려 한다면 이 모든것들이 묵시적으로 self 파라미터를 다루기 때문에 self까지 포함하여 들고 있는다.

이러한 방식은 굉장히 참조 사이클(reference cycle)을 마주치기 쉬운데, 이것이 왜 컴파일러가 클로저 안에서 명시적으로 self를 쓰게 만드는지에대한 이유이다. 명시적으로 쓰게 만듦으로서 당신에게 잠재적인 참조 사이클에대해 생각해볼 수 있게 해주고, 붙잡고 있는 항목들을 이용해 손수 해결할 수 있게 해준다.

그러나 non-escaping 클로저로는 참조 사이클을 만드는게 불가능하다. 클로저는 함수 리턴시점까지 붙잡아둔 모든 오브젝트를 릴리즈(release) 시킬 것이라는 것을 컴파일러가 보장한다. 이러한 이유로 escaping 클로저에서만 명시적으로 self를 사용하여 참조할 것을 요구한다. 이 점이 non-escaping 클로저 사용을 더 확실히 즐겁게 해준다.

non-escaping 클로저의 또다른 장점은 컴파일러가 더 적극적으로 퍼포먼스 최적화를 수행할 수 있다는 점이다. 예를들어 클로저의 라이프타임을 알고 있을때 몇몇 리테인(retain)과 릴리즈(release) 호출은 생략할 수 있다. 또, non-escaping 클로저라면 클로저의 컨텍스트를 위한 메모리가 힙이 아닌 스택에 담아둘 수 있다.(현재 Swift 컴파일러가 이러한 최적화를 시키는지는 잘 모르지만 2016년3월 버그리포팅에서 그렇게 하지말자고 제안이 들어왔었다)

디폴트로 클로저는 non-escaping이다.
Swift3부터 non-escaping 클로저가 디폴트이다. 만약 클로저 파라미터에 escape 시키고 싶으면 그 타입에다가 @escaping 지시자를 써야한다. 예를들어 DispatchQueue.async (escaping)와 DispatchQueue.sync (non-escaping) 선언이 있다.

Swift3 전까지는 좀 다른 방식으로 동작했었는데, escaping이 디폴트이고 오버라이드에 @nonescape를 추가할 수 있었다. 이러한 새로운 동작은 디폴트에의해 더 안전해진다는 면에서 더 좋은데, 이제 함수 인자는 반드시 참조 사이클의 잠재성이 있다는 것을 명시적으로 표시해주어야한다. 따라서 @escaping 지시자는 이 기능을 사용하는 개발자에게 경고를 해주는 역할을 한다.

...그러나 오직 즉석 함수 파라미터(immediate function parameters)로서
디폴트에의한 non-escaping 규칙에 대해 주목할 것이 있다. 이는 직접 함수 매개 변수 위치의 클로저에만 적용된다. 즉, 함수 타입을 가지는 모든 함수 인자에 적용된다. 다른 모든 클로저는 escaping하고 있다.

직접 파라미터 위치가 무슨 뜻일까?
예제를 한번 보자. 가장 간단한 예제로는 map이 있다. 이 함수는 직접 클로저 파라미터를 받는다. 우리가 보았듯 클로저는 non-escaping이다. (여기서 실제 map의 표시를 말하려는게 아니므로, 그것들을 조금 생략하겠다)

함수 타입의 변수들은 항상 escaping이다.
반대로, 변수나 프로퍼티가 함수 타입을 가질 수 있다. 이때는 명시적인 지시 없이 자동으로 escaping이 된다.(사실은 @escaping을 넣으면 에러가 뜬다) 이렇게 이해할 수 있는데, 변수에 값을 할당하면 묵시적으로 값을 변수의 범위로 escape할 수 있기 때문이다. 이것은 non-escaping 클로저로 허가될 수 없다. 그러나 드물게 지시되지 않는 함수는 파라미터에서의 의미가 아닌 다른 곳에서는 다른 의미를 가지기 때문에 혼란스러울 수 있다.

옵셔널 클로저는 항상 escaping이다.
더욱 놀라운 점은, 파라미터로 쓰이지만 다른 타입(튜플이나 enum case, 옵셔널 같은)으로 감쌓여진 클로저들 또한 escaping이라는 것이다. 이 경우에 클로저는 더이상 직접 파라미터가 아니므로 자동으로 escaping된다. 그 결과 Swift3에서는 파라미터가 옵셔널이면서 동시에 non-escaping한 곳에 함수인자로 받는 함수를 만들지 못한다. 아래의 다소 인위적인 예제를 생각해보자. transform 함수는 정수 n과 옵셔널 변환 함수인 f를 받아 f(n)를 반환하거나 f가 nil이면 n을 반환한다.

여기서 ( (Int) -> Int )?가 Optional<(Int) -> Int>의 축약이기 때문에 함수 f는 escaping이며, 따라서 함수 타입은 직접 파라미터 위치에 있지 않다. 이 결과는 우리가 바라는 것이 아닌데, 여기서 f가 non-escaping 될 수 없는 이유가 없기 때문이다.

옵셔널 파라미터를 디폴트 구현으로 대체하자.
Swift 팀은 이 한계를 알고있고, 미래의 배포에서 고치기로 계획했다. 그전까지 우리가 이것을 알고 있어야한다. 현재 강제로 옵셔널 클로저를 non-escaping할 방법은 없지만, 많은 경우에 클로저에다 디폴트 값을 제공하여 옵셔널 인자를 피할 수 있을 것이다. 우리 예제에서는 디폴트 값이 항등 함수(identity function)이고, 이 함수는 간단하게 인자를 바꾸지않고 그대로 반환한다.

옵셔널과 non-escaping 변형을 제공하기 위해 오버로드를 사용하자
디폴트 값을 제공하기 힘든 경우라면, Michael Ilseman이 오버로드를 사용할 것을 제안했다. 여러분은 함수의 두기자 변형을 만드는데, 하나는 옵셔널(escaping) 함수 파라미터를 받고, 하나는 non-옵셔널, non-escaping 파라미터를 받는다.

어떤 함수가 호출되었는지 설명하기 위해 print 상태를 추가했다. 여러 인자로 이 함수를 테스트해보자. 당연하게도 nil을 보내면, 그 인풋과 일치하는 것이 하나밖에 없으므로 타입 체커가 첫번째 오버로드를 선택한다.
동일하게, 옵셔널 함수 타입을 가지고 있는 변수를 보낸다.
그 변수가 non-옵셔널 타입일지라도, Swift는 여전히 첫번째 오버로드를 선택할 것이다. 그 이유는 변수에 저장된 함수는 자동으로 escaping되고, 따라서 non-escaping 인자를 예상한 두번째 오버로드와는 일치하지 않는다.

그러나 클로저 표현식을 보낼때 이것은 바뀐다. 즉 함수 리터럴이 이 자리에 있을때 말이다. 이제 두번째 오버로드 non-escaping이 선택된다.

리터럴 클로저 표현식으로 higher-order 함수를 호출하는 것은 굉장히 일반적이므로, 대부분의 경우 선택적으로 여전히 nil을 보낼 수 있게 해줌으로서 여러분을 즐거운 길(참조 사이클을 생각할 필요 없는 non-escaping)로 안내해줄것이다. 이런 방식으로 한다면 왜 두가지 오버로드가 필요한지 증명할 수 있을 것이다.

타입에일리어스(typealiases)는 항상 escaping이다.

마지막으로 한가지 알고 있어야 하는 것은 Swift3에서는 타입에일리어스에 escaping 혹은 non-escaping 지시자를 넣을 수 없다는 것이다. 함수 선언에서 함수 타입을 위해 타입에일리어스를 사용한다면 그 파라미터는 항상 escaping으로 생각될 것이다. 이 버그 수정은 이미 마스터 브런치에서 이루어졌고, 다음 배포에 적용될 수 있을 것이다. 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

프로그래밍 패러다임 관점에서나 탄생 이유의 관점에서나 리액티브(Reactive)를 이해하기 위해서는 현재 개발자와 회사가 직면한 문제들과 10년전의 문제를 비교하여 생각해보아야한다.

개발자와 회사에 바뀐 두가지는 아래와 같다.
  • 하드웨어의 발전
  • 인터넷

"모닥불 주위에 모여 옛날일을 이야기하는 것"은 일부에의해 대화의 수준이 낮아질 수 있음으로 고려해야하는 반면, 우리는 모든 개발자가 직면한 문제를 고심하기 위해 직종의 역사를 탐구해볼 필요가 있다.

왜 이것들이 이제 달라졌을까
10년간 컴퓨팅 연구 끝에 발견한 의미있는 지식이 하나 있다. 리액티브 프로그래밍은 소프트웨어의 새 세대를 만들기 위한 시도이다.

1999년
1999년, 내가 캐나다 임패리얼 상업은행에 있고 처음 자바를 배우기 시작할 무렵이다.
  • 인터넷은 2.8억 유저에 달성했다.
  • JSEe는 여전히 썬(Sun MicroSystems)사의 꿈으로서 가슴속에 있는 상태였다.
  • 온라인 뱅킹이 5년정도 된 유아기 시점이었다.
1999년으로 돌아가면 나는 동시성에 대한 문제에 직면하고 있었다. 그 해결책은 스레드와 락(lock)에 관련이 있었으며, 유경험자의 개발자까지도 그 문제를 해결하기 힘들어하는 상황이었다. 자바의 특징은 "한번 작성하면, 어디서든 실행할 수 있다"인데, 여기서 "어디서든"은 JVM이 설치되어있는 OS에 한에서 "어디에든"이지, 클라우드나 IoT 세대를 위해 설계한 다른 동시접속의 개념이 아니다.

2005년
2005년이 오래전은 아니지만 컴퓨팅쪽과 인터넷쪽이 크게 바뀌었다. J2EE, SOA, XML이 인기를 끌었고 루비온레일즈가 J2EE의 고통받는 컨테이너 기반 개발 문제를 해결하기 위해 탄생하였다.

이때의 인테넷에는
  • 1억유저가 있었다.
  • 페이스북이 550만 유저를 가지고 있었다.
  • 유튜브가 탄생했다.(2005년 2월)
  • 트위터가 아직 없었다.(2006년)
  • Netflix가 비디오 스트리밍을 소개했다.(2007년)

2014년
이 글을 쓰고 있는 시점인 2014년은 Internet Live Stats에 의하면 약 2,950,000,000(29억 5천만)정도의 인터넷 유저가 있다고 한다. 중국이 혼자서 6억 4천만 인터넷 유저를 보유하고 미국이 2억 8천만을 보유하고 있었다.

오늘날 가장 인기있는 웹사이트이다.
  • 페이스북—13억 유저
  • 트위터—2억 7천 유저

시간이 흐르면서 한 웹사이트의 트래픽이 지난 20년전의 인터넷 전체 트래픽보다 많다.

1999년부터 2015년까지 인터넷, 페이스북, 트위터 유저 수1999년부터 2015년까지 인터넷, 페이스북, 트위터 유저 수



우리는 점점 확장과 예측에 관한 이슈가 중요해지고, 우리의 삶에서의 소프트웨어가 중요해짐을 쉽게 확인할 수 있다. 또한 과거의 패러다임이 현재까지 이어질 수는 없을 것이고, 분명 미래까지도 이어지기 힘들 것이다.

4가지 리액티브 요소
리액티브 앱은 4가지 요소로 구성되있다.

  • 반응성(responsive)있는 앱이 그 목표이다.
  • 반응성있는 앱은 확장가능(scalable)하고 탄력(resilient)있다. 반응성은 확장성과 탄력성 없이 불가능하다.
  • 메시지-주도(message-driven) 구조는 확장가능함과 탄력있음을 근간으로 하고 궁극적으로 반응성있는 시스템을 기반으로 한다.

반응성(Responsive)
"앱이 반응성 있다"는 것이 어떤 의미일까?

반응성있는 시스템은 시종일관으로 분명한 유저 경험을 보장하기위해 좋은 상황이나 나쁜  상황이나 상관없이 모든 유저에게 즉각 반응하도록 하는 것이다.

외부 시스템의 실패나 트래픽 급증과 같은 다양한 상황에서의 재빠른 처리분명한 사용자 경험탄력성확장성이라는 두 성질에 의존한다. 메시지-주도 구조는 반응성 있는 시스템을 위한 전반적인 근간을 제공해준다.

왜 반응성 있는 것이 메시지-주도 구조에 중요할까?

세상은 비동기적이다. 당신이 한 포트의 커피를 끓이는데 크림과 설탕이 없음을 깨닭았을때의 그 예시가 있다.

아래는 그 한가지 방법이다.
  • 한 포트의 커피를 끓이기 시작한다.
  • 커피가 끓는동안 가게에 간다.
  • 크림과 설탕을 산다.
  • 집으로 돌아온다.
  • 즉시 커피를 마신다.
  • 여유를 즐기면 된다.

또 다른 방법이다.
  • 가게에 간다.
  • 크림과 설탕을 산다.
  • 집으로 돌아온다.
  • 커피를 끓을 때까지 시계를 보면서 기다린다.
  • 카페인 금단현상을 겪는다.
  • 젠장

여러분이 볼 수 있듯, 메시지-주도 구조는 당신을 시공간으로부터 분리된 비동기 바운더리를 가능하게한다. 우리는 남은 이 포스트에서 비동기적 바운더리 개념에 대해 이야기해볼 것이다.

왈마트 캐나다(Wlamart Canada)에서의 일관성
Typesafe에 들어가기 전에, 나는 왈바트 캐나다의 플랫폼을 만든 Play and Scala 팀의 기술 리더였다.

우리의 목표는 분명한 사용자 경험의 일관성을 만드는 것이었다. 다음 것들에 관계없이 말이다.
  • 데스크탑, 테블릿, 모바일 기기등 어떤 기기에서도 walmart.ca를 서핑할 수 있다.
  • 현재 피크 트래픽이 튀어오르든 유지되든 관계없어야 한다.
  • 전체 데이터 센터의 손실과 같이 주요 기능이 실패해도 관계없어야 한다.

응답 시관과 전반적인 사용자 경험은 위 시나리오와 관계없이 일관성있다. 일관성은 당신의 웹사이트를 전달하는데 근본적으로 중요하며 오늘날 웹사이트는 당신의 브렌드임을 생각해야한다. 좋지않은 사용자 경험은 실제 상점이 아니라 온라인에서 일어나기 때문에 쉽게 잊어지거나 무시되지 않는다.

Glit에서의 반응성있는 소매(retail)
전자상거래 도매인에서의 일관성은 우연에의해 일어나지 않는다. Glit의 경우, 매일 저녁에 하루 세일을 공지하는데, 그 때 트래픽이 급증하는 플래시 스케일 사이트이다. 플래시 스케일 사이트의 사용자 경험에대해 이야기해보자. 만약 당신이 오전 11:58에 한번 Glit를 접속하고 오후 12:01에 한번 더 접속한다면 Glit는 일관성있게 정해진 응답 경험을 제공하고 이것을 리액티브를 사용하여 시행하였다. 스칼라 기반의 마이크로 서비스 구조로 마이그래이션한 Glit를 더 배우고 싶다면 interview with Eric Bowman of Gilt 여기를 보아라.

탄력있는(Resilient)
많은 앱들이 이상적 환경만을 고려하면서 설계, 개발하지만 사실 이상적이지 않을때도 많다. 이것은 매일 주요앱 기능이 실패하는 다른 보고서를 받거나, 해커에의해 서비스 멈춤, 데이터 손실, 지속적인 손상을 초례하는 다른 co-ordinated breach 시스템이 있을 수 있다.

탄력있는 시스템은 이상적이지 않는 상황에서도 반응성을 보장하기 때문에 바람직한 설계 요소와 구조 요소를 사용한다.

자바와  JVM은 다중 OS에서 한 앱을 문제없이 실행시키는 것에 대한 것이었다면, 201x년대의 상호연결된 앱은 앱단의 구성, 연결성, 보안성에 대한 것이다.

이제 한 앱은 웹사이트나 다른 네트워크 규약을 통해 통홥되어 여러 앱으로 구성되어있다. 오늘날 하나의 앱은 신뢰된 방화벽을 가진 바깥은 외부 서비스들에(10개, 20개, 혹은 더 많이) 의존하며 만든다.

이 복잡한 통합을 고려하면 얼마나 많은 개발자가 필요할까?

  • 모든 외부 의존성을 분석하고 모델링하는 사람
  • 통합된 각 서비스의 이상적인 응답 시간을 문서로 만들고 피크일때나 아닐때 모두 퍼포먼스 테스트가 초창기 기대와 일치하는지 확인하기위해 관리하는 사람
  • 모든 퍼포먼스, 실패, 핵심앱 로직으로 포함되는 다른 비기능적인 요구사항을 문서화하는 사람
  • 각 서비스의 모든 실패 시나리오를 다시 분석하고 테스트하는 사람
  • 외부 의존성의 보안성을 분석하고, 외부 시스템과 통합했을때 새로운 취약점이 있는지 알아내는 사람

탄력성은 가장 정교한 앱의 가장 약한 연결 중 하나이지만, 추가적으로 탄력있어야하는 것은 곧 끝난다.(원문: Resiliency is one of the weakest links of even the most sophisticated application, but resiliency as an afterthought will soon end.) 현대의 앱들은 이상적인 상황 보다는 다양한 현실세계에서 반응성을 유지해야하기 때문에, 앱의 핵심(core)이 반드시 탄력적이어야한다. 퍼포먼스, 내구성, 보안성은 모든 면에서 말이다. 여러분의 앱은 단지 몇 부분이 아닌 모든 부분에 걸쳐 탄력적이어야한다.

메시지-주도 탄력성
메시지-주도 핵심에서 가장 아름다운 제작방법은 여러분이 자연스럽게 작업에 필요한 것들을 한조각 한조각 얻어내는 것이다.

고립(Isolation)은 시스템의 자체 회복을 위해 필요하다. 잘 고립되있을때, 우리는 실패의 위험이나 퍼포먼스 특징, CPU와 메모리 사용 등과 같은 요인에 기반하여 여러 타입으로 나눌 수 있다.

정확한 위치는 마치 같은 VM에서 동작하게 한 것 처럼 서로다른 노드위에서 서로다른 프로세스들이 상호소통할 수 있게 해준다.

특정 목적용 에러 채널은 단지 에러 신호를 호출자에게 던저버리는게 아니라 다른 우리가 원하는 곳으로 보낼 수 있다. 

이러한 사실은 우리 앱에서 확고하게 에러 핸들링을 구체화하고 결함에 내성을 만들어준다. 이것은 아카의 감독 계층(Akka's supervisor hierarchies)처럼 구현함으로서 입증되었다.

조각(block)을 만드는 핵심은 이상적인 환경 뿐만 아니라 좋지않은 환경에서도 메시지-주도 구조가 탄력성에 기여하는 것에의해 제공하고 다음으로 반응성에 기여한다.

44억 달러의 탄력성 실수
2012년에 를 생각해보자. 소프트웨어가 향상되는동안 통합된 앱 방식은 점점 인기있고(fired up) 거래 규모가 점점 커지기 시작했다.

다음은 45분동안 일어나는 악몽같은 시나리오였다.

Knight의 자동교환 시스템이 잘못 거래하여 나스닥(NASDAQ)을 침수시켰고, 10억달러가치를 의도치않은 회사에 놓았다. 이러한 사고는 다시 반환(reverse)하는데 회사에 44억달러의 비용이 들게 되었다. 나스닥이 침수되고 거래의 범람을 고치는 동안 나스닥은 Knight 돕는 것을 중단했다. Knight의 주식은 하루만에 63%나 떨어졌으며 그들은 가까스로 살아남았고, 일부를 회복한 후에 투자자에의해 다음 인계를하고 살아갈 수 있었다. 

그때 Knight의 시스템은 동작했었지만 탄력성이 없었다. 탄력성이 없는 Knight 시스템은 문제를 확장시키는데 한 몫을 하였다. Knight는 최악의 버그가 발생했을때 그들의 시스템을 끌 수 있는 스위치(kill-switch) 매커니즘도 없는 상태였으므로, 나쁜 환경에서 그들의 자동 거래시스템이 45분만에 회사의 모든 주요 자산을 고갈시켜버린 것이다.

이것은 이상적인 환경을 위한 설계 정의이자 개발 정의였다. 이제 소프트웨어는 우리 개인 삶이나 회사에서 핵심 요소이다. 예측되지 못한 나쁜 상황의 시나리오 된다면 굉장히 많은 비용을 감당해야할 수 있다.

확장가능한(Scalable)
일관성있는 반응성 앱을 만들때 탄력성과 확장성을 잘 이용해야한다.

확장 가능한 시스템은 다양한 요구량의 상황(various load conditions)에서도 반응성을 보장하기 때문에 그에 맞춰 쉽게 업그레이드 시킬 수 있다.

온라인에서 물건을 팔아본 사람이라면 물건을 최대로 많이 팔때 가장 큰 트래픽이 생긴다는 사실을 알 것이다. 대부분의 경우(사이버상 공격을 제외하고) 트래픽이 폭발하는 것은 당신이 뭔가 잘하고 있을 때이다. 트래픽이 치솟았을대 사람들은 당신에게 돈을 주고 싶어하고 있는 것이다.

그럼 어떻게 치솟는(혹은 꾸준히 증가하는) 트래픽을 다룰까?

첫째로 당신의 패러다임을 먼저 고른다. 둘째로 그 패러다임을 구현할 수 있는 언어와 툴킷을 정한다. 많은 개발자가 종종 너무 가볍게 언어와 프레임워크를 선택한다. 한번 툴을 선택하면 그것을 다시 바꾸기 쉽지 않으므로 당신은 주요 투자가와 함께 그 결정을 내려야한다. 만약 여러분이 기술적인 선택 결정을 원칙과 분석에 기반하여 하고 있었다면 굉장히 잘하고 있는 것이다.

동시성을 위한 스레드-기반 제한
기술적인 선택에서 가장 중요한 것중 하나는 동시성 모델 프레임워크이다. 고수준에서 두개의 서로다른 동시성 모델이 있을 수 있다.
  • 전통적인 스레드-기반 동시성으로 콜스택과 공유 메모리를 기반으로 한다.
  • 메시지-주도 동시성

레일즈와같은 몇 인기있는 MVC 프레임워크는 스레드 기반이다. 이 프레임워크의 전형적인 특징은 아래와 같다.
  • 공유 가변 상태(Shared mutable state)
  • 요청당 한 스래드(A thread per request)
  • 가변 상태에 동시 접근(Concurrent access to mutable state)—이것(변수나 객체 인스턴스)은 또다른 복잡한 동기화 방법이나 락(lock)으로 관리한다.

루비와 같은 인터프리트 언어로 다이나믹 타입의 특징을 합치면 여러분은 쉽게 퍼포먼스와 확장성의 상한선에 도달할 수 있을 것이다( Combine those traits with a dynamically typed, interpreted language like Ruby and you can quickly reach the upper bounds of performance and scalability). 이것이 어떠한 스크립트 언어라도 그 본질은 같다고 말할 수 있을 것이다.

Out 혹은 Up?
앱 확장을 좀 다른 방법으로 생각해보자.

스케일업(Scale up)은 단일 CPU/서버의 리소스를 최대화 하는 것인데, 파워풀하고 희귀하고 값비싼 그런 컴퓨터를 종종 사야한다.

스케일아웃(Scale out)은 여러 저렴한 하드웨어를 연결하여 컴퓨테이션을 제공하는 것인데, 비용면에서 효과적이다. 그러나 당신의 시스템이 시공간 개념에 기반하였다면 매우 어려울지도 모르겠다. 위에서도 이야기했듯 메시지-주도 구조는 시공간으로부터 분리하기위해 필요한 비동기 바운더리를 제공하며, 필요에따라 스케일아웃 할 수 있는 유연성(elasticity)을 제공한다. 반면 스케일업은 이미 가지고있는 자원의 효율을 높히는 것이고, 유연성은 당신의 시스템이 바뀌기 원하는대로 새 자원을 추가할 수 있음에 관한 것이다. 필요에따라 스케일아웃 할 수 있는 능력은 리액티브 앱의 궁극적인 확장성의 목표이다.

공유 가변 상태, 스레드, 락 기반의 앱을 스케일아웃하는게 어렵기 때문에 리액티브 앱들을 스레드 기반으로 만드는것은 어려운 일이다. 개발자들은 한 머신에서 멀티 코어의 이점을 활용해야 할 뿐만 아니라, 특정 시점의 개발자들은 머신의 클러스터를 활용해야 한다. 그게 불가능할지라도, 공유 가변 상태 또한 스케일업하기 어렵게 만든다. 한번이라도 두개의 스레드에서 공유 가변 상태를 다뤄본 사람이라면, 스레드 세이프를 보장하는 과정이 얼마나 어려운지, 스레드 세이프를 위해 과한 작업을 하게되는 실적 패널티가 얼마나 큰지 이해할 수 있을 것이다.

메시지-주도(Message-driven)
메시지-주도 구조는 리액티브 앱의 근간이다. 메시지 주도 앱은 이벤트 주도 이거나 행위자 기반일 것이고, 혹은 이 둘 모두를 합친 것일 것이다.

이벤트 주도 시스템은 0개 혹은 그 이상의 Observer에의해 관찰된 이벤트 기반이다. 이것은 명령형 프로그래밍과는 좀 다른데, 호출자가 부른 루틴으로부터 응답을 블락된 상태로 기다릴 필요가 없기 때문이다. 이벤트는 바로 특정 장소를 지정하는게 아니라 나중에 일어날 어떤 결과를 지켜보고 있는 것이다.

행위자-기반 동시성은 메시지-전달 아키텍처의 확장이며 메시지는 수신자에게 전달된다. 메시지는 스레드 경계를 넘거나 실제 다른 서버의 다른 행위자의 메일함으로 전달 될 수 있다. 행위자가 네트워크를 통해 배포 될 수 있지만 여전히 동일한 JVM을 공유하는 것처럼 서로 통신 할 수 있으므로 요구에 맞게 스케일-아웃 할 수 있다.

메시지와 이벤트의 차이점은 메시지는 전송되는 것이고 이벤트는 일어나는 것이다. 메시지는 명확한 도착지가 있지만 이벤트는 0혹은 그 이상(0-N)의 Observer에의해 관찰되고 있을 것이다.

이벤트-주도와 행위자-기반 동시성에 대해 좀 더 세부적으로 들어가보자.

이벤트-주도 동시성
일반적인 앱들은 명령형 스타일(오퍼레이션 순서)로 개발되고 콜스택을 기반으로 개발한다. 콜스택의 주 기능은 프로세스에서 호출자가 블럭되고 리턴값과 한께 호출자에게 컨트롤을 돌려주는 동안, 주어진 루틴에서 호출자를 계속 쫓고, 호출된 루틴을 실행하는 것이다.

겉으로 보았을 땐 이벤트 주도 앱이 콜스택에 맞추는게 아니라 이벤트 트리거에 맞춘다. 이벤트는 0개 혹은 그 이상의 Observer에의해 지켜보고 있는 큐에 메시지로 인코딩 되어 있을 것이다. 명령형 스타일과 비교하여 이벤트-주도의 큰 차이점은 응답을 기다리는 동안 호출자가 한 스레드 위에서 블락되거나 멈추지 않는다. 이벤트 루프 자체는 단일 스레드 일 수 있지만, (단일 스레드 이기도 한)스레드 된 이벤트 루프가 들어오는 요청을 처리 할 수 있도록 허용하면서 호출 된 루틴이 업무를 수행하는 동안 (잠재적으로 IO 자체를 차단하면서) 동시성은 여전히 달성된다. 프로세스가 완전히 끝나지 않는한 요청을 블락시키는 대신에 호출자의 id가 요청 메시지의 바디와 함께 전달되므로 깨어있는 루틴이 이것을 선택하면 호출자는 응답과 함께 콜백될 수 있다. 

이벤트 주도 구조를 선택하는 이유는 콜백지옥(링크)이라는 것에 괴로워할 수 있기 때문이다. 콜백지옥은 메시지를 받는 놈이 정해져 있는 것이 아니라 익명의 콜백이기 때문에 발생한다. 콜백지옥을 해결하는 일반적인 방법은 이러한 문제가 생기는 이유를 잊고 코드에 표현된 이벤트 순서대로 디버깅하기 어려운것도 생각하지 않으면서 온전히 구문(aka, the Pyramid of Doom) 형태에만 초점을 맞춘다.

행위자-기반 동시성
행위자-기반 앱은 여러 행위자 사이에서 비동기 메시지를 보낸다.

행위자는 아래 속성들을 갖는다.
  • 메시지를 받기 위한 메일 박스
  • 각 타입별로 메시지를 어떻게 받는지 결정하기 위해 패턴매칭의 행위자 로직
  • 요청 사이에 컨텍스트를 저장하기 위한 고립된 상태
이벤트-주도 동시성처럼 행위자 기반 동시성에서는 콜스택은 피하고 가벼운 메시지 전달을 지향한다. 행위자는 메시지를 밖으로든 자기자신에게든 보낼 수 있다. 한 행위자는 그 큐의 첫번째 요청을 먼저 제공한 뒤에 처리가 긴 요청을 처리하기 위해 메시지를 자기자신에게 보낼 수도 있다. 행위자-기반 동시성의 큰 장점은 이벤트-주도 구조에서 얻은 장점을 얻을 수 있다는 점이다. 네트워크 경계를 통해 컴퓨테이션을 스케일-아웃하기도 쉽고, 행위자에게 직접 메시지를 전해주기 때문에 콜백 지옥을 피할 수도 있다. 이러한 강력한 컨셉은 설계, 구현, 유지보수하기 쉬우면서 확장성이 있는 앱을 만들 수 있게 해준다. 시공간에대해 생각하거나 깊게 감쌓인 콜백들에대해 생각하는것보다, 행위자 사이에 메시지 흐름이 어떻게 되는지만 고민하는게 더 낫다.

또 다른 주요 장점은 요소들끼리 느슨하게 연결된다는 점이다. 호출자는 응답을 기다리기 위해 스레드를 멈추지 않으므로 빨리 다른 일을 할 수 있다. 호출자에의해 켭슐화되있는 현재 루틴은 필요에따라 호출자를 다시 호출하면 된다. 이것은 다양한 가능성을 열어준다. 콜백 스택이 한 메모리 공간에 있기 위해 앱을 묶어버리지 않으며 행위자 모델은 프로그래밍적인 일보다 구성에 관련된 일을 추상화하여 만듦으로 여러 머신을 통해 루틴을 분배할 수 있다.

Akka는 행위자-주도 툴킷이고 타입 세이프 리액티브 플랫폼의 일부로서 JVM에서 높은 동시성, 분배, 실패에 대한 내성을 가진 행위자-기반 앱을 만들기 위한 런타임이다. Akka는 탄력을 위한 관리계층이나 확장성을 위한 일 분배와 같은 리액티브 앱 개발에 필요한 멋진 기능들을 탑재하고 있다. Akka에대해 더 깊게 보고싶으면 Let it Crash blog를 확인해보길 바란다.

또한 이 세션 일부의 자료로 사용된 Benjamin Erb’s Diploma Thesis의 글(Concurrent Programming for Scalable Web Architectures)을 읽어보기를 강력 추천한다.

결론
위의 모든 것들이 오늘날 앱 개발에 흠집을 내고, 왜 리액티브 프로그래밍이 단지 또다른 트렌드가 아닌지 이야기하며, 그러나 왜 현대 소프트웨어 개발자들이 배워야하는 패러다임인지도 이야기했다. 여러분이 선택하는 언어나 툴킷에 관계없이 반응성을 얻기위해 확장성과 탄력성 또한 탑재하는 것이 사용자의 기대를 충족시키는 유일한 방법이다. 이것은 몇년간 더욱 중요하게 떠오를 것이다.

저자에대해
Kevin Webber는 Lightbend에서 Enterprise Advocate이다. 그는 heritage 구조에서 리액티브 프로그래밍 원칙을 포괄하는 실시간 분배 시스템까지 큰 구조의 트랜지션을 돕는 것에 열정적이다. 남는 시간에 ReactiveTOProgramming Book Club Toronto를 운영한다. 그는 가끔 제 3자에서 자기 자신에 대해 글을 쓰기도 하는데, 이 단락이 그러한 순간이다. 


신고

'개발' 카테고리의 다른 글

[번역] 리액티브 프로그래밍이란?  (0) 2017.03.03
(번역)Android Architecture  (0) 2016.10.01

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

NSCodingNSObjectProtocol이라는 클래스 프로토콜이 필요하다. 그리고 그 프로토콜은 구제체가 따를 순 없다. NSCoding을 사용해서 인코딩하고 싶다면 가장 쉬운 방법이 클래스로 만들어 NSObject를 상속받는 것이다.

나는 구조체를 NSCoding으로 감쌀 수 있는 말끔한 방법을 찾았고 거추장스러운 작업 없이 저장할 수 있다. 예제로서 Coordinate를 사용할 것이다.

두개의 스칼라 프로퍼티를 가지는 간단한 한 타입이다. 이제 NSCoding을 따르고 Coordinate를 감싸는 클래스를 만들어보자.

이 로직을 다른 유형으로 사용하는 것이 좋으며 단일 책임 원칙에 더 잘 지킨다. 눈치 빠른 독자는 위 클래스에 Coordinate 프로퍼티의 EncodableCorrdinate가 옵셔널이라는 것을 눈치 챘을 것이지만, 꼭 그럴 필요는 없음을 알 수 있을 것이다. 우리는 옵셔널이 아닌 Coordinate를 받는(혹은 실패하게 만드는) 생성자를 만들 수 있는데, 그렇다면 이미 init(coder:) 메소드는 불가능하다. 그리하여 EnabledCoordinate 클래스 인스턴스를 들고 있을때 항상 coordinate를 가지고 있음을 보장해주어야한다.

그러나 더블(Double)타입(혹은 어떤 원시 타입)을 인코딩 할 때 NSCoder 동작 방법의 특징으로 인해 Any?를 반환하는 decodeObejct(forKey:)로 추출해낼 수 없게 되었다. DoubledecodeDouble(forKey:)로 하여 특정 윈시타입에 특정 메소드가 있다. 불행히도 이 특정 메소드는 옵셔널을 반환하지 않으며, 키가 맞지 않거나 오류가 나면 0.0을 반환해 버린다. 이러한 이유 때문에 coordinate 프로퍼티를 옵셔널로 두기로 했고 옵셔널로 인코딩한다. 그리하여 나는 decodeObject(forKey:)를 사용해 Double?을 얻어냄으로써 추가적인 안정성을 보장했다.

이제 Coordinate 오브젝트를 인코딩/디코딩 하기 위해 EncodableCoordinate를 하나 만들고 NSKeyedArchiver로 디스크에 저장할 수 있다.

이러한 추가적인 오브젝트를 만드는게 이상적이진 않으며, 나는 "가능하면 나를 캐싱해줘" 글에서 사용한 SKCache와같은 오브젝트로 작업하기를 좋아한다. 그리하여 내가 인코더와 인코딩 된 사이의 관계를 형식화 할 수 있다면, 아마 매번 NSCoding 컨테이너를 만들지 않아도 될 것이다.

그 끝으로 두가지 프로토콜을 추가하자.
그리고 우리의 두가지 타입에 대해 적용시킨다.

이렇게하여 이제 타입 시스템은 오브젝트 쌍에서 타입과 값 사이에서 어떻게 변환하여 넣고 빼는지 안다.

블로그 포스팅에서 나온 SKCache 오브젝트는 Encoded 타입을 넘어 제네릭으로 업그레이드 되어왔다. 인코더 값 타입이 그 자신이라는 조건으로, 이것은 이 두 타입 사이에 쌍방향으로 변환할 수 있게 해준다.

이 타입에서 마지막 퍼즐조각인 savefetch 메소드가 남아있다. saveencoder를 잡아두고있고(사실 이것은 NSCoding을 따르는 오브젝트이다) path로 저장해둔다.

패칭은 약간의 컴파일러 댄스(compiler dance)를 포함한다. 우리는 언아카이브(unarchive)된 오브젝트를 T.Encoable로 캐스팅 해야하는데, 이것은 encoder 타입이다. 그리고 그 값을 잡아두고, 동적으로 그것을 캐스팅하여 T로 돌려준다.

이제 캐시를 사용하기위해 한 인스턴스를 만들고 Coordinate 제네릭으로 만든다.

이렇게 하면 좌표 구조체를 쉽게 저장하고 검색할 수 있다.
이것을 가지면 NSCoding을 이용해 구조체를 인코딩할 수 있고, 단일 책임 원칙에 따렴, 타입 세이프티하게 만들어준다. 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

작년에 나는 Swift String Cheat Sheet라는 글을 썼는데, 이 글은 Swift 표준 라이브러리에서 더 복잡한 API중 하나를 어떻게 사용하는지 기억나게 해주었다. 이번 Swift3에서는 중요한 변화를 겪으면서 코드 마이그레이션을 힘들게 만들었따. 이것은 부분적으로 API 네이밍 가이드라인이 새로 바뀌면서, 컬렉션, 인덱스, 범위(Range)의 새로운 모델이 적용되었기 때문이기도 했다.

이 글은 Swift3을 위해 업데이트한 Swift Playground에 필요한 것들을 메모한 것이다.

좋은 리네이밍
표준 라이브러이에서 새로운 API 가이드라인을 적용시키면 사용하고 있던 String에서 많은 프로퍼티와 메소드를 바꿔야한다. Xcode에서 이 작업을 어느정도 대신 해주기 때문에 그 모든 바뀐점에대해 언급하진 않겠다. 아래에는 일반적으로 바뀐것에 대한 방법을 알려준다.

문자열 초기화
표준 라이브러리는 String 생성자를 init(count: repeatedValue)에서 init(count: repeatedValue)로 바뀌었다. repeatedValue는 Charater 대신에 String으로 바뀌었는데, 더 유연하게 되었다.

upper/lower case로 변환하기
uppercaseString과 lowercaseString 프로퍼티가 이제 uppercased()와 lowercased() 함수로 바뀌었다.

조금 더 있다가 다른 바뀐 이름에대해 다룰것이다.

인덱스를 사용하여 컬렉션을 탐색하기
Swift3에 들어오면서 String에 가장 큰 영향을 준 변화 중 하나는 컬렉션과 인덱스의 새로운 모델이다. 고쳐 쓰기위해 String의 요소에 직접 접근할 수 없고 대신에 컬렉션에 있는 인덱스를 사용해야한다.

Swift3에서 각 컬렉션의 startIndex와 endIndex 프로퍼티는 바뀌지 않았다.

character에 있는 것을 원할때 character 프로퍼티를 생략할 수도 있따.

인덱스로 문자열을 탐색할 수 있도록 바뀌었다. 이제 successor(), predecessor(), advancedBy(n) 함수들은 없어졌다.

Swift3에서는 이제 같은 결과를 얻기 위해 index(after:), index(before:), index(_: offsetBy:)를 사용한다.

또한 end 인덱스를 넘어가버릴때 에러를 피하기위해 offset 한계를 정할 수도 있다. index(_: offsetBy: limitedBy:) 함수는 너무 멀리까지 가버리면 nil을 반환하는 옵셔널 반환 함수이다.

첫번째로 일치하는 요소(아래는 character이다)의 인덱스를 찾는다.

마지막으로는, 두 인덱스 사이의 거리를 계산해주는 메소드 이름이 바뀌었다.

범위(Range) 사용하기
범위는 Swift3에서 바뀌었는데, character에 시작 인덱스(lower bound)와 끝 인덱스(upper bound)를 가지고 있다고 가정하자.

upper와 lower 바운드로부터 범위를 생성하기 위한 생성자 전체

..<와 ... 연산자를 사용하면 더 쉽게 생성할 수 있다.

하위 문자열에 일치하는 문자가 있는지 확인하고 범위를 반환한다.

Playground
여러분은 전체적으로 업데이트된 playground 내 예제 코드를 저장소에서 확인할 수 있다. 또한 이전에 작성한 포스팅(영문)도 업데이트 되었다.

더 읽을거리


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

Swift로 깨끗한 코드를 짜려 할 때, Swift API Design Guidelines은 매우 중요한 것 중 하나이다. 이것을 본 적이 있는가? 사실 나는 애플의 WWCD 비디오를 보기 전까지 이것을 본 적이 없었다. WWDC 영상을 기다리면서 내 직장 동료와 이것저것 이야기 했는데, 영상이 눈에 잘 안들어올 여지가 있었다. 내가 직접 작업하는 것 말고는 다른사람이 사용하기 위해 "프레임워크" 형태의 코드를 작성하진 않는 사람 중 한명으로서, 나에게 이 영상의 설명과 직접 연관되있지 않을것이라 생각했다.

Swift3은 명확하고 정교한 Swift 코드를 위해 고유의 특징을 만들어 담은 새 API 설계 가이드라인을 소개한다. 이 이야기는 Swift API 가이드라인 너머에 있는 철학을 탐구하고 Swift 표준 라이브러리, Cocoa, Cocoa Touch API를 통한 그 응용프로그림을 탐험할 것이다. 어떻게 이 API 변형이 여러분 Swift 코드에 영향을 줄 수 있는지 확인해보고, 어떻게 Swift3으로 부드럽게 변형할 수 있는지 배워보자. Swift3이 어떻게 Objective-C API를 불러오는지, 어떻게 기존의 Objective-C 라이브러리를 Swift 인터페이스로 만드는지 배워보자.

비디오를 보고 나서 Swift.org에서 실제 설계 가이드라인을 읽어 보았는데, 의미있었고 여러분도 한번 읽어보기를 추천한다.

Swift API 설계 가이드라인은 여러분의 코드에서 변수, 파라미터, 메소드의 네이밍의 "단어"를 어떻게 만들어낼 것인지에대한 설명의 메뉴얼이다. 이것은 여러분의 코드에서 단어들을 어떻게 잘 조합할 수 있는지 연관된 문서를 제공하면서 이야기한다. Swift API 설계 가이드라인과 친해질수록 커뮤니티의 코드와 일관성을 가지게 될 것이며, 애플이나 Swift팀의 프레임워크에서 만들어진 API이다(원문: the APIs that are being created within the frameworks right out of Apple and the Swift team). 이것은 윈/윈의 방법이다.

아래는 내가 뽑은 Swift API 설계 가이드라인의 중요 포인트이다.

"ed/ing" 규칙
WWDC 영상의 발표자는 이 규칙은 "ed/ing" 규칙이라고 말했다. Swift.org 문서에서는 String for Fluent Usage 섹션에서 그 사이드 이펙트에의한 함수와 메소드 이름으로서 소개되어있었다. 가이드라인에서는 "가변의 메소드가 종종 유사한 의미에서 불변 변형을 가지는데 이때는 그 자리에서 인스턴스를 갱신하는 것 보다는 새로운 값을 반환하는게 낫다"고 한다. ed/ing 규칙에 따라 불변 버전 뒤에 붙인인다. 아래 예시이다.

가변(Mutating)
불변(Nonmutating)
<코드>

필요없는 단어는 생략하기
Objective-C로부터 남아있는 나쁜 습관이 하나 있다면, 장황한 메소드 이름인데, 특히 반복적으로 사용되는 단어들이다. 예를들어 Objective-C의 NSMutableArray 클래스 문서를 열어 "Object"가 몇번 나오는지 한번 세어보자. 이런 메소드는 Objective-C Foundation API 전반에 걸쳐 나타난다.
<코드>
NSMutableArray의 클래스 문서에 "Object"라는 단어가 몇번이나 들어갔는지 세어보아라. 결과적으로 내가 쓴 코드에도 그런식으로 하게 만든다.

반면 Swift에는 이런것이 없다. Swift 창시자들은 간결함과 명확성의 조화를 위해 힘쓰고 있는 중이다.

Swift 코드가 너무 생략되버릴 수 있는데, 작은 단어 갯수로 코드를 최소화시키는 것이 그 목표는 아니다. 너무 짧은 Swift 코드가 있는 곳에는 강타입 시스템의 부작용이다...

이 섹션의 마지막 부분과 연관이 있다. 강타입 시스템으로서 NSMutableArray API를 아래처럼 다시 설계해볼 수 있다.

메소드 이름에서 얼마나 "Object"라는 단어가 없어졌는지 보라. 이것은 가이드라인에서 말하는 "불필요한 단어 생략"의 결과물이다. 이렇게 하는 이유는 이 API 사용자들이 파라미터에 정의된 타입으로만 오브젝트를 메소드로 보내기 위해 Swift의 강타입 시스템에 의존할 수 있기 때문이다. 이것은 Swift API 디자인 가이드라인 안에서 얘기하는, 불필요한 단어 생략을 통해 더 명료한 코드를 만드는 하나의 예시일 뿐이다. 이제 이것을 알았다면 가이드라인을 읽으면서 그것들을 여러분의 API 설계에까지 적용시켜볼 수 있다.

또한 Swift API 설계 가이드라인에서 변수, 파라미터, 연관타입, 네이밍에 해당하는 규칙들과 약타입 정보에 대한 보장 부분을 확인해보아라. 여기에 자세한 예제가 있지는 않지만, 이 가이드라인에서는 어떻게 필요없는 단어를 생략하여 명료함을 달성할 수 있는지에대한 좋은 안내서를 제공한다. 단 명료함을 넘어 축약이 되지 않도록 조심하자. WWDC 발표자가 언급한 것처럼 API는 꽤 간결하기 때문에 API 문서가 자꾸 바뀌길 원하지는 않을 것이다.

문법적인 영어 구(Grammatical English Phrases)
메소드와 함수의 이름은 문법적인 영어의 구처럼 읽을 수 있어야 한다. 나는 이 부분을 굉장히 좋아하는데, 자바 프로그래머로서 암흑기로부터 나를 끄집어내준 기분이 든다. 이것은 Swift API 설계 가이드라인의 String for Fluent Usage 섹션에서 이야기하고있다. 메소드와 파라미터 이름이 옳바른지 확인해보려면 코드를 소리내서 읽어보면 된다. "이것이 영문법적으로 옳바른가? 회화식으로 말할 수 있는가?" 이 질문에 대답이 그렇다면 아마 좋은 API 일것이다. 그렇지 않으면 다시 한번 생각해보아라. 여기 문법적으로 이해하기 쉬운 Swift.org에서 말하는 좋은 API 설계의 예시이다.
<코드>
나쁜 설계는 소리내서 읽기 힘들다.
<코드>

다른 멋진 것들
문서
내가 전문적으로 사용해본 프로그래밍 언어 중에 가이드라인이 이렇게나 잘 정의된 코드 문서는 본적이 없다. Swift API  설계 가이드라인의 Fundamentals 섹션 상단에 있다. Swift 코드를 위한 문서를 작성할 때 고려해야하는 짧고 친절한 요점을 제공한다. 또한 만들어 놓은 문서를 Xcode가 어떻게 보여줄지와 함께 어떻게 그것과 일관된 주석을 달 수 있는지도 알려준다.

컨벤션(약속)
컨벤션 섹션은 매우 마음에 든다. 빅오(Big-Oh) 문서를 만들때나, 변수, 메소드 이름을 정하는 것들로부터 모든 것을 분명하게 설명해주는 1회성 코드들이 여기에 있다.

요약
나는 여러분에게 Swift.org에있는 Swift API 설계 가이드라인을 확인하라고 강요할 순 없다. 그래도 오늘 한 것이 여러분이 Swift 코드를 더 좋게 만드는데에 직접 행동으로 옮겨볼 수 있는 방법중 하나라는 생각이 든다. 가이드라인은 간단하고 깔끔하며 이해하기 쉽도록 배려해 놓았다. 여러분의 코드에 일관성이 생기는 것 뿐만 아니라 커뮤니티 어디에서나 쓰일 수 있는 일관성도 갖추게 될것이다. 이것을 누가 바라지 않겠는가?

즐거운 클리닝하기 바란다. 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret





애플은 이번 2016년 WWDC에서 Xcode Source Editor Extensions를 소개했다. 내가 좋아하는 툴을 쉽고 간편한 방법으로 확장할 수 있게 해준다. 이것이 왜 생산성에 영향을 미칠지 의아해 한다면 아래 이야기를 계속 읽어보아라.

소프트웨어 개발 업계의 수많은 거장들은 이렇게 생각할 것이다. 전문성은 장인정신 자체에서 나오는 것이다. 이 비유는 특히 그렇다. — 도구. 좋은 장인은 좋은 도구가 필요다. 그러나 "못난 일꾼이 늘 연장 탓한다"이라는 속담이 있다.

크리스토프 고켈은 우리에게 이렇게 상기시켜주었다. "일을 할때 옳바른 연장을 손에 쥐어라" 그러나 적당한 연장이 없다면 우리 스스로 연장을 만들 수 있으며 이 이야기가 바로 우리가 해야할 일이다.

Xcode는 수많은 써드파티 플러그인을 사용했었다. "사용했었다"라고 과거시제를 쓴 이유는 Xcode8부터 더이상 플러그인을 지원하지 않기 때문이다. 이것은 굉장히 슬픈 소식이지만, 좋은 이유(보안상의 이유나 신뢰성과 관련되있는)에서 이렇게 하게 되었다.

애플은 무책임하게 IDE를 플러그인으로부터 벗어내어 개발자를 힘들게 하려는게 아니라, 새로운 방식으로 고유의 툴을 만들 수 있게 제공한다. WWDC에서 새 기능에 대해 멋지게 설명해주었지만 우리 App'n'roll'에서는 우리 손으로 직접 코드를 짜보는게 최고의 방법이라 생각하여 그렇게 해보았다.

JSON Models
많은 개발자들이 앱에서 네트워킹 작업을 할 것이다. 그리고 JSON을 파싱하여 모델로 만드는 작업을 하는데 시간을 소비한다. 이러한 일은 IDE에서 대신 해줄 수 있는 일이므로 우리는 extension으로 이것을 만들어 보기로 했다. 몇 가정을 하고 작업에 갔는데, 첫째로 현재 수정되는 파일은 JSON 타입이여야한다. 둘째로 감쌓여진(nested) 오브젝트에 연관된 가장자리 케이스는 무시한다.

중요 note : 우리 예제는 Xcode8.0 베타2에서 만들어졌고, 아마 항상 모든 버전에서 잘 동작하지는 않을지도 모른다. 만약 여전히 엘케피탄에서 작업하고 있다면 Xcode8.0 beta Release Notes에 들어가서 IDE와 Source Editor Extension에 관한 Xcode8.0 beta 이슈를 보아라. 또한 당신의 extension을 실행시킬때 약간의 딜레이가 있다. 만약 프로젝트가 너무 빨리 열린다면 extension이 불러와지기 전에 켜진것이며, 테스트 Xcode의 인스턴스 메뉴에서 사용할 수 없을 것이다.

이제 새 macOS 프로젝트를 생성하고(UnitTests 박스에 체크했는지 확인해보라) 기본 앱에서 Xcode Source Editor Extension이라는 새 타깃을 추가하여 시작해보자.



이렇게하면 하나의 Info.plist와 두개의 클래스를 자동으로 만들어 줄 것이다. 코드를 자세히 살펴보기 전에 먼저 plist를 보자. Xcode의 메뉴에 보이는 이름을 바꾸기 위해 Bundle Name과 XCSourceEditorCommandName을 고친다.

자동으로 만들어진 첫번째 클래스는 XCSourceEditorExtension이고 이것은 extension이 불러와질때 우리에게 알려주는 역할을 한다. 이번 프로젝트에서는 굳이 손 델 필요가 없다. 두번째 클래스는 XCSourceEditorCommand이다. 명령을 내릴때 실행되는 perform(with invocation:, completionHandler:)  메소드가 하나 프로토콜로서 정의되있을 것이다. 이 extension은 현재 파일의 내용물과 그것을 수정하는 방법을 제공한다. 우리는 간단하게 추상화된 층을 사용하여 유닛테스트하기 쉬운 방향으로 만들어갈 것이다. 파일과 함께 인터렉션 하는 것은 SourceFile 프로토콜을 따른다.

다음 순서는 테스트하기 쉽게 도와주는 오브젝트이다. 이것을 JSONConverter라 부르자.

SourceFile을 받고 뭔가 문제가 생기면 예외로 넘겨주는 메소드 하나만 가지고 있다. XCSourceEditorCommand와 합치려면 아래처럼 간단한 연결점이 필요하다.

아직까지는 extension을 실행하고 명령을 해도 아무일도 일어나지 않을 것이다. 이제 TDD 방식을 조금 사용해보자. 먼저 테스트에 기반한 시스템을 만들고 소스파일을 위한 테스트 쌍을 작성한다.

첫번째 테스트는 유효한 JSON으로 파싱되었는지 체크한다.

다음으로는 한 JSON 양식으로된 문자열을 JSON으로 파싱한다. 여기 테스트가 있다.

...그리고 여기 그 구현이 있다

이제 약간 꼼수를 써서 NSNumber의 서로다른 타입들을 위해 3개의 테스트를 만든다.
...그리고 이것은 한번에 테스트를 통과할 것이다.
다음 이 코드는 런타임동안 오브젝트의 타입을 체크하고 Swift 타입과 일치하게 만들어준다.

다음 우리의 리스트가 한 배열로 만들어지는지의 테스트이다.

The next step is parsing a simple JSON with one String property. Here is the test:

...그리고 구현이다.

마지막으로 감싸진 타입의 파싱 기능은 특별히 우리에게 필요한 것이었다. 이것이 가능한지 알아보는 테스트는 아래 테스트로 충분히 확인할 수 있다.

감싸진 타입은 다른 타입보다 더 많은 구현이 필요하다. 여기 JSONConverter의 완성된 구현이다.

끝이다. 우리는 앞으로 모델 네이밍 시스템을 약간 바꿀 예정이다. JSONConverter는 이미 XCSourceEditorCommand와 합쳐졌고 이제 어떻게 Xcode에서 동작하는지 체크하는 것만 남았다.

첫 Xcode Source Editor Extension을 완성한 것을 축하한다.

요약
툴을 사용하는 것은 모든 소프트웨어 개발자에게 매우 중요한 영역이다. 우리 IDE 프로바이더는 항상 우리가 원하는 것을 제공해주는 것이 아니므로 때론 우리 손으로 직접 그 문제를 해결해야한다. 이런 맥락에서 Xcode Extension은 활용하기 좋은 기능이다.

애플이 이번에 처음으로 발표한 시점이긴 하지만, 우리 개발자에게 AST와 파일 시스템에 접근할 수 있게 해주는 순간 어마어마한 가능성을 가지게 될 것이다. 여기 Github에서 예제코드를 확인해 볼 수 있다.

사용된 모든 이미지는 CC0 1.0 Universal (CC0 1.0)로 쓰였다. 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

요즘 많은 사람들이 그들의 다음 그들의 다음 모바일 앱 개발에 사용할 플랫폼으로 리액트 네이티브에 다가가고 있다. 이것은 사소한 일이 아니다. 당신의 소프트웨어 개발 플랫폼을 바꾸는 것은 높은 초기 설정 비용을 수반하여 매일 프로그래밍 작업 플로우에 깊숙히 영향을 줄 것이다. 또한 지금까지 만들어놨던 모든 요소들을 전환하는 것도 가장 비싼 비용의 결정 중 하나이다.

아마 더 중요한 것은, 당신의 소프트웨어 개발 플랫폼 또한 소프트웨어 엔지니어로서 당신을 이루는 것 중 하나이다. 소프트웨어 개발 플랫폼은 한 언어만 사용하게하고, 어느 특정 아키텍처를 우선시하며, 특정 툴을 요구하고, 당신을 그 모든 생태계와 플랫폼의 개발자 커뮤니티와 결혼시켜버리도록 지향(혹은 강요)한다.

페이스북은 당신이 전환해오기를 바란다.

그리고 리액트 네이티브 팀의 꾸준하며, 공들인 노력은 그 야망과 같다. 그들은 우리의 전통적인 Xcode/Swift/Objective-C 스택을 대체할 수 있는지 고려해야하는, 너무 근본의 소프트웨어 개발 플랫폼을 만들어왔다.

이것이 실용적인 변화일 수 있을까? 내가 읽어온 리액트 네이티브에 관련된 블로그 포스팅들은 오히려 피상적인 접근을 알려주었다. 그것의 장점과 단점에대한 깊이있는 접근은 없고, 여러분이 누군가로부터 소프트웨어 개발 플랫폼을 바꿔라고 설득당한 사람이라고 가정하고 이야기를 시작한다.

지난 몇달동안 리액트 네이티브를 사용하면서, 나는 이것으로 개발할 수도 없고 이것을 사용하기를 추천하지도 않을 플랫폼인 것으로 결론이 나왔다. 이 글의 목표는 Swift 개발에서 리액트 네이티브로 전환하는 것에 대한 빈틈없는 평가의 찬반을 알려주는 것이다. 그리고 그 전환에대해 반대하는 이야기도 할 것이다.

찬성
선언 스타일(Declarative style)
리액트 네이티브와함께 작업할때 기뻤던 한가지는 UI 프로그래밍의 선언 스타일이었다. 리액트 방법으로 하면 UI는 함수의 상태와 프로퍼티들이다. 반면 Cocoa Touch에서는 UI를 명령적으로 짜야한다.

아래 예제가 내가 말하는 바를 설명해 줄 것이다. 화면 왼쪽 상단에 작은 사각형을 넣고 싶다고 가정하자. 이 작은 사각형은 사용자가 연결되있을 때는 빨강, 연결되지 않을때는 초록을 표시한다.

아래는 우리가 일반적으로 iOS에서 어떻게 했는지 보여준다.

명령형 스타일에서는 UI를 갱신하기 위해 이 모든 단계를 명시해주어야한다. isConnected가 바뀌었는지 듣고 있어야 하고, 뷰에 일치하게 갱신해야한다. 우리는 iOS에게 어떻게 상태를 컴퓨트할지 말해줘야한다.

이제 리액트의 선언 방법과 비교해보아라.

간단하게 어떻게 여러분의 앱이 주어진 점을 보여주는지 표현했고, 리액트는 데이터 갱신때마다 자동으로 모든 UI 갱신을 관리해줄 것이다.

리액트의 선언 스타일은 뷰의 render() 메소드로 작용하여 여러분의 UI를 묘사하개 해준다. 리액트 프레임워크는 상태의 모든 갱신을 다시 랜더링하도록 해준다. 여러분의 데이터 갱신(backgroundColor의 갱신이 일어날때)은 자동으로 UI 갱신으로 이어진다.

이렇게하면 모델의 갱신에대한 응답에서 손수 뷰를 갱신하지 않앋 되게 해준다. 여러분이 이 책임으로부터 벗어나고 싶든 아니든, 리액트는 당신의 설명에따라 갱신 관리를 보장해주는 점은 매우 좋은 점이며, 여러분은 더이상 isConnected 변수에대한 프로퍼티를 관리할 필요가 없다. 당신이 갱신한 모든 것은 상태이다.

또한 UI 요소들이 결국 클래스의 객체가 아니라 함수인것처럼 생각하게 될 것이다. 그것들은 상태를 받아서 UIKit 오브젝트로 랜더링한다. Component의 역할은, 요청이 들어왔을때 예상되는 상태에서의 그 자신을 반환하는 것이다.

UI에 대해서는 유용한 방법이라 생각한다. 그리고 MVC로부터 좋은 발전이다. 뷰는 단지 그 자체를 보여주는 역할만 하고 데이터를 관리하지는 않으며, UIViewController에서 시작하기를 좋아하고, 뷰와 컨트롤러 사이 관계에 밀접하게 붙어있는 장님으로 바뀐다.

빠른 반복
리액트 네이티브의 매력은 단지 지적임이 아니다. 프레임워크는 실용적이기까지 할 것이다.

여러분이 리액트 네이티브에서 프로그래밍을 하면, 프레임워크는 당신이 작업하고 있는 자바스크립트 코드를 서브하는 로컬 서버를 만들어 줄 것이다. 앱을 만들면 iOS 시뮬레이터나 기기에서 돌려보고, 리택트 네이티브는 자바스크립트로 만든 모든 변화를 앱에 반영하게 해준다.

여러분에게 두가지 옵션이 있다.
  • 실시간 리로딩(Live reloading)은 당신이 그 파일에 수정하고 저장할 때마다 앱을 리로드할 것이다. 기본적으로 시뮬레이터로 전환하는 것을 막고 ⌘ + R을 누르지 않아도 된다.
  • 핫 리로딩(Hot reloading)은 앱 전체를 리로드하지 않고 당신이 수정한 부분만 리로드한다. 당신이 네비게이션 스택 깊숙히 테이블 뷰 셀의 UI를 작업하고 있었다면, 그 바뀐점을 보기위해 시작화면에서 셀까지 찾아가지 않아도 된다. 그리고 컴포넌트는 이미 그 안에 있던 상태로 그대로 있을 것이다. 이것은 WYSIWYG 프로그래밍 경험(WYSIWYG programming experience)이다. 앱을 위해 Xcode가 결코 제공하지 않았던 호화로운 기능이다.

나는 앱을 시작에서부터 보여주고 싶은 ViewController마다마다 AppDelegate에 디버그 메소드를 넣었던 것을 기억한다. 그리하여 앱을 켤때마다 손수 그 페이지를 찾아가고 싶지 않아서 코드로 그 페이지를 찾아가게 했었다. 핫 리로딩은 나를 동굴에서 나온 사람의 기분으로 만들어 버렸다.

리액트 네이티브의 피드백 루프 주기는 넋이 나갈 정도로 낮다. 앱에서 파일을 저장하고 그것을 눈으로 보기까지 1~2초정도 걸린다. 이것은 Xcode에서 일반적으로 우리가 사용한 빌드&런보다 10배 작게 든다.

이것은 방송처럼 코드 갱신을 가능하게 해준다. 자바스크립트 코드에서의 어떤 변화라도 앱이 제품으로 출시되어 있는동안 사용자에게 즉시 적용될 수 있다.

크로스 플랫폼
품위없고 서튼 이야기를 기억한다.

"훌륭한 앱이군요! 안드로이드에서도 동작하나요? [...]오, 안드로이드 버전은 언제 배포할 계획이시죠?[...], 오."

이제 코드베이스가 수백만 개의 추가 장치에서 실행될 수있는 앱을 만들 수 있으며 아웃 리치를 몇 배 증가시킬 수 있다.

그리고 여러 플랫폼에서 동일한 코드가 더욱 확실하게 같은 모습과 같은 동작의 앱을 만들 것이라 확신한다.

같은 코드베이스로 여러분 앱의 안드로이드 것을 만들 수 있고, 그래픽적으로든 기능적으로든 iOS쪽과 동등해지게 한다.

크로스 플랫폼 프레임워크의 명백한 장점은 언제나 시장 개발, 통합 코드베이스 및 앱의 코드베이스를 유지하는데 필요한 통합 기술 세트이다.

반대
불확실한 로드맵
리액트 네이티브를 사용할 때 생기는 중요한 걱정은 그 프로젝트에서의 장기적인 보장이 없다는 것이다. 

만약 이 프로젝트가 네트워크 라이브러리나 CGPath를 그리는 SVG같은 부가적으로 합쳐서 쓸 수 있는 요소라면, 장기적인 지원이 두번째 걱정으로 밀려날것이다. 리액트 네이티브 개발자들이 그만둬버리거나 리액트 네이티브 개발 속도가 마음에 들지 않으면, 대충 비슷한 라이브러리로 대체하거나 아니면 내 스스로 고쳐 쓸수도 있었을 것이다. 이것은 큰 작업이 될지도 모르겠지만, 이러나저러나 거대한 일은 아니다. 한 라이브러리를 쓸 수 없게 되었다해서 프로젝트 전체가 붕괴되지 않음을 보장하기 위해선 모든 나의 써드파티 라이브러리/CocoaPods을 내 프로젝트에서 충분히 떼어놓으면 된다.

그러나 리액트 네이티브는 CocoaPod처럼 부가적으로 쓰는 요소가 아니며, 단지 SDK도 아니고, 단지 라이브러리도 아니다. 이것은 완전히 소프트웨어 개발 플랫폼이다. 내 앱이 이것에 "아주 붙어있다"고 말하는 것은 너무 조심스러운 표현이고, 애 앱이 그것에 완전히 의존적이라고 말해야한다. 만약 페이스북이 리액트 네이티브 유지보수를 중단하면, 그것을 쓰고 있던 내 앱은 썩어갈 것이고, 내가 할 수 있는 "리액트 네이티브 대체물"은 없을 것이다. 만약 그것을 내 스스로 개발하려 한다면 리액트 네이티브 소스에도 친숙해야할 뿐 아니라, React.js 코드베이스, 네이티브 CLI 툴, JavaScriptCore에도 익숙해져야 할것이다. 커뮤니티가 이 프로젝트의 생존을 보장해줄까? 아마-만약 그렇게 되버리면 우리가 사용할 수 있을 정도의 개발 속도가 나지 않을 것이다.

깃헙 저장소에서는 2주에 한번 정도 건강한 속도로 리액트 네이티브 릴리즈가 되고있다. 두개의 분리된 것과 복잡한 소프트웨어 개발 플랫폼을 타겟으로 하는 소프트웨어 개발 플랫폼에는 나쁘지 않다. 페이스북이 오늘 약속할지도 모르지만, 지속적인 네이티브를 유지하기위한 장기간의 약속을하지 않았다. 회사에서는 이 프로젝트에서 손을 떼지 않을 거라는 어떠한 보장도 하지 않고 있다. 가까운 미래뿐만 아니라 여러분의 앱이 살아있는 동안에 말이다. 다른 말로는, 현재 시점에서 리액트 네이티브가 iOS11이나 iOS12에 호환될거라는 보장은 어디에도 없다는 것이다.
여기서 다음년도로!
2016년 4월 이후 공식적인 리액트 네이티브 블로그 포스팅은 종료되었다. 그 이후엔 어떻게 되었을까? 그것에대해 페이스북은 걱정없이 침묵만 지키고 있다.
우리는 곧 계획을 발표할 것이다-Konstantin
2015년 12월에 리액티브 팀이 말한 것이다. 이 계획이란것은 아직도 발표되지 않고 있다. 확실히 리액트 네이티브의 페이스북 장기간 비용 분석 결과는 당신이 생각한 만큼 멋지게 보이지 않는다.

다시 말하면, 우리는 개별의 CocoaPod에대해 이야기하는게 아니라, 여러분의 코드가 실행될 수 있는 유일한 플랫폼에대해 이야기하고 있는 중이다. 굉장히 장기적인 시각으로 다뤄야할 중요한 문제이다.

명백히 위압하는(Patently daunting)
리액트 네이티브는 복제를 허용하는 BSD-스타일 라이센스와함께 페이스북의 추가적인 특허권 문서(Additional Grant of Potent Right)를 만들었다.(버전2) 이 파일을 넣은 페이스북의 입장은 분명하지 않고, 이 파일 자체도 불분명하다.

양극의 문서이다. 문서가 끝나자마자 리액트 네이티브를 사용하기 위해서는 "perpetual(영속의), worldwide(널리), royalty-free(무료저작권), non-exclusive(독점불가능한), irrevocable(변경불가능한)" 라이센스를 따라야한다고 한다. 아래 조항들을 제공받으면서 말이다.

The license granted hereunder will terminate, automatically and without notice, if you (or any of your subsidiaries, corporate affiliates or agents) initiate directly or indirectly, or take a direct financial interest in, any Patent Assertion: (i) against Facebook or any of its subsidiaries or corporate affiliates, (ii) against any party if such Patent Assertion arises in whole or in part from any software, technology, product or service of Facebook or any of its subsidiaries or corporate affiliates, or (iii) against any party relating to the Software.

나는 변호사에게 이것을 명확하게 해달라고 직접 물어보았다(여러분이라도 그렇게 할 수 있었을 것이다). 그 변호사 말에 따르면 이 조항은 다음으로 요약된다. 내가 만약 페이스북에게 특허권 위배를 주장하며 소송을 걸면 리액트 네이티브를 사용하는 라이센스가 즉시 사라질 수 있다.(아래 Appendix에서 그의 더 세부적인 설명을 인용했다)

실제로는 특허권으로 페이스북으로 고소하지 못하게 하는 것처럼 보였다. 이런 의미에서, 이것은 페이스북이 내 특허권을 침해할 수 있는 기회를 주는 것처럼 보였다.("당신이 여기에있는 훌륭한 양자 물리학 기술은 당신의 앱에 나쁜 일이 생길 경우 부끄러운 일이 될 것입니다.")

위 설명이 비판적이라고 생각할지도 모르겠다. 그러나 소프트웨어 라이센스와 특허권을 평가할 때는 비판적으로 해야한다. 알고리즘이 최악의 시나리오에서 얼마나 잘 동작하는지 그 효율을 평가한다. 똑같은 이유로서 소프트웨어 라이센스와 특허권도 그렇게 평가해야한다고 생각한다.

리니어 서치(linear search) 알고리즘은 타겟값이 리스트의 첫번째로 일어나는 시나리오에서는 최적화된 동작을 할 것이다. 그 이유 하나로, 모든 시나리오에 리니어 서치 알고리즘을 사용하라고 나를 설득하기엔 만족스러운 이유가 아니다. 만약 타겟값이 리스트 마지막에 있을 수 있다면, 내 리니어 서치 알고리즘의 평균 퍼포먼스는 굉장히 나빠질 것이다.

비슷한 맥락에서, 페이스 북이 내일 일련의 IP를 침해하는 것이 가능하고 React Native를 사용하기위한 라이센스를 취소함으로써 내측으로부터의 보복을 처벌 할 수 있다면, 페이스북이 오늘 일련의 IP 침해를 하지 않았다는 사실은 나를 조금 안심시킨다.(원문: Likewise, the fact that Facebook happens not to be a serial IP infringer today reassures me little if it is possible for Facebook to become a serial IP infringer tomorrow and punish any retaliation from my side by revoking my license to use React Native.) 만약 내일 페이스북이 내 IP를 침해하고(그러나 그것이 내 소프트웨어와 관련은 없다) 그들에게 소송을 걸면,  페이스북에게 내 앱에서 리액트 네이티브를 빼내게 만들 기회를 주는 셈이 되버린다.

내 앱이 그 플랫폼에 의존적인 것 뿐만 아니라 영리한 내 프로퍼티들까지도 위험하게된다.

iOS 앱들은 완전히 애플의 재량으로 앱스토어에 들어간다. 나는 쉽지 않은 느낌을 두배로 느끼고 싶지 않다.

명백히 침묵하는(Patently silent)
위의 해석이 정확한가? 최악의 시나리오 가능성인가? 이런것들을 걱정할 만큼 좋은 이유가 있는가? 걱정하지 않아도 될 좋은 이유가 있는가?

수많은 깃헙 이슈와 포럼 포스팅에서, 그들의 법률 부서가 이런 조항 때문에 리액트나 리액트 네이티브를 쓰지 마라고 조언한다고 이야기해왔다. 볍률 부서가 없는 개발자들에게는 유감스럽게도 이 문제들이 모호하게 남아있다.

좀 더 걱정스럽게도, 페이스북은 이 문제를 더 명확하게 하기 위한 근본적인 노력을 해오지 않고 있다. 2015년 페이스북 오픈소스 블로그 포스팅에서는 이러한 "혼란"에대해 인정했고, 기꺼히 추가적인 특허권 문제에대해 명확하게 할것이라 알렸었다. 깃헙에서 그것과 연관된 5개의 이슈 이후에도 그 불분명함은 사라지지 않고있따.

여러 페이스북 개발자들이 이 이슈에대해 응답해오고 있는데, 그들이 적어놓은 것에는 안심될만한게 하나도 없으면서 그것을 명확하게 하는 동안에 개발자들을 안심시키려고 노력하고 있다. 그들 중 한명은 Reddit과 HackerNews에있는 이 이슈에대한 토론 링크를 주기도 했었다(도움이 되지 않을 뿐더러 오해시키기까지 했다).

이것은 해석학과 추측의 작업으로 바뀌어버렸다. 페이스북은 리액트 네이티브 라이센스를 취소 할 수 있을까? 그렇다면 어떤 조건에서?

자바스크립트
Swift에서 리액트 네이티브로 전환할때 생기는 심각한 부정적인 면은 기술적 역행이다. 여러분은 자바스크립트에 적응하고 사용해야하는데 이 언어는 다음과 같은 특징을 담고있다.
  • 기술적으로 불완전함
  • 언세이프
  • 늦은 발전
왜 그렇게 말하는지 보자.

여기 이후에 예제에 나오는 모든 자바스크립트 코드는 ES2016에 유효하다.

자바스크립트의 결점
내가 좋아하는 법퍼 스티커에서 하는 말이다.
안전은 사고가 없는 것이다(Safety is no accident)
처음 봤을땐, 이 말은 중의적이었다. 우리는 안전의 정의를 사고가 나지 않는 것과 여러 안전장치 제품의 제안으로 알고 있다.

자동차에 안전벨트나 에어백이 탑재된 여러 안전장치의 제품은 안전하다고 할 수 있는가?

물론 대답은 같은 조건에서 둘 다이다.

운전자는 수많은 안정장치가 탑재된 차를 더 선호할 수 있다. 운전하기 어렵게 만들지라도 막을 수 있는 사고는 최대한 줄일 것이다.

비슷한 의미에서, 프로그래밍 언어는 프로그래머 에러에 대비한 안전장치를 제공할 수 있다.

수많은 운전자가 생산성을 위해 안전벨트를 착용하지 않고 운전한다는 사실은 옳바른 주장이 아니다. 비슷하게, 수많은 자바스크립트 개발자들이 생산성을 위해 태생의 언세이프 언어를 쓴다는 것도 옳바른 주장이 아니다.

프로그래밍 언어에서 안전의 중요성은 iOS 개발 툴이 진화하고 있는것의 진가를 인정하는 것이기도 하다.

오토 레퍼런스 카운팅이 Objective-C에 처음 나타났을때, 여러분의 iOS 프로젝트에서 그 옵션을 끄고 작업했을 수도 있다. 왜 ARC를 끄는게 나쁜 생각이었을까? 그 이유는, 이제 컴파일러가 당신의 오브젝트 라이프타임 계산을 할 수 있게 되었는데, 당신이 계산하는 것보다 더 빠르게 해준다. "컴파일러가 당신보다 더 똑똑해졌다"는 진언이고, 레퍼런스 카운팅에 관해서는 확실해졌다. 결과적으로 EXC_BAD_ACCESS 런타임 크레쉬를 줄였다는것을 알게되었을때 얼마나 만족했었는지 기억한다.

Obejctive-C는 한 변수 타입을 id로 설정할 수 있는데, 이것은 "어떤 것이든 모든 타입"의 의미이다. 그러나 컴파일러가 막을 수 있는 크레쉬임에도, 이러한 습관때문에 런타임 크레쉬가 일어날 수 있다. 컴파일러가 해결할 수 있는 문제라면, 컴파일러가 해결하게 놔두고 당신은 다른 문제를 해결하러 가면 된다.

여러분은 unrecognized selector sent to instance 크레쉬를 기억할 것이다. 응답하지 않는 오브젝트에 메소드를 호출할 때 생기는 크레쉬이다. 타입에러. 내 버그에서 3번째이다.

당연하게도 Swift를 사용하고나서 나의 첫 반응은 "Objective-C에서 런타임 크레쉬를 막고 싶었던 누군가가 만들었구만"이었다.

Swift는 안전하다. String을 받길 예상한 함수에 Int값을 넣도록 컴파일러가 허용하지 않는다. 사실 컴파일러가 타입 추론을 할 수 없었다면, 명시적으로 그렇게 할 수 있었을 것이다. 

그러나 자바스크립트는 프로그래머 에러에 대비한 안전장치가 부족하며, 여러분의 루틴에서 런타임 크레쉬를 막아야하고, 프로그래머 에러를 막아야한다.

타입 에러
자바스크립트는 변수나 함수에서 파라미터의 타입을 정해주지 않는다.

어떤 변수라도 언제든 무엇이든 될 수 있다.

자바스크립트는 class, typeof, instanceof와같은 키워드로 타입과 클래스 개념이 있다고 믿게 만든다.

여기서 우리는 다음을 보자.
  • 자바스크립트에서의 "class", "type", "instance" 개념은 주요 프로그래밍 세계에서의 개념과 완전히 다르다.
  • 자바스크립트에서는 타입을 너무 신뢰할 수 없게 정의해서, 이것들이 유용한 용도를 제공하지 못한다.

이  unrecognized selector sent to instance 크레쉬가 기억나는가? 이것이 이제 여러분 곁을 떠날것이라 생각하는가? 아래에는 리액트 네이티브의 것들이 있다.


옵셔널 결핍
Objective-C 코드(혹은 다른 수많은 오래된 언어)에서 엄청나게 많은 양의 버그들은 프로그래머가 부주의하게 nil에다가 메소드를 호출해서 생긴다.

리액트 네이티브와 자바스크립트 세계에서는 아래 에러를 종종 볼 수 있다.

그리고 이론적으로나 실무적으로나 막을 수 있다.

Swift는 옵셔널을 만들어냄으로서 이 문제를 해결했는데(옵셔널이란 nil일 수도 있고 값이 들어있을 수도 있는 타입이다), 이것을 사용할 때는 여러분에게 강제로 nil 체크를 거쳐서 사용하게 만든다.

함수 기호(function signature)의 결핍
자바스크립트에서는 함수가 리턴타입을 가지고 있지 않으며, 이 함수가 어떤 타입을 반환할지 모르거나 함수가 어떤것이든 반환할 수 있다.

좀 더 재미있게 만들어보자. 자바스크립트에서는 어떤 식이든 어떤 함수에의해 어떤 시간에든 계산될 수 있다. 자바스크립트 표준 라이브러리에 있는 map과 parseInt 함수를 사용한 예제를 생각해보자. map은 아마 Swift의 map과 동일한 것이고, parseInt는 문자열을 숫자로 파싱해주는 함수이다.
parseInt 함수는 2개의 파라미터(val, radix)를 받는데 반해, map은 3개의 파라미터(currentValue, index, array)를 보내기 때문에 이런 엉망의 결과가 생긴다. 이런것들이 여전히 합법적인 자바스크립트이다(일부 사람들이 함수형 프로그래밍 언어에 유용할것이라고 생각하는 언어).

불변성
자바스크립트가 지원하는 불변성은 매우 안 좋다.

const 연산자가 있는데, 이것은 기본타입(primitive)이 바뀌지 않는 것을 보장하는데 도움을 준다. 그러나 기본타입이 아닌 나머지 모든 것은 젤리처럼 유연하다.

유일한 복사? 아니다. 어떤 유일한 객체라도 언제든지 여러분의 앱에의해 수정될 수 있다. 멀티스레딩에 행운을 빈다. 정말로.

앱 개발을 힘들게하는 많은 것들은 가변을 쫓고 상태를 유지하는 것이다.

페이스북의 Immutable.js 문서에서 말한 내용이다(자바스크립트에서 불변 데이터 구조를 만들기위해 설계된 프레임워크이다).

그러나 아래는 리액트에서 어떻게 불변성을 만드는지이다.

이 컴포넌트에 넣는 인풋을 props라 부르는데, "properties"의 준말이다. 그들은 JSX 문법으로 속성들을 보낸다. 여러분은 이것을 컴포넌트에 불변할 것이라 생각할 수 있는데, 이것은 절때 this.props에 덮어 씌울 수 없다.

여러분은 상태를 바꾸지 말자고 공손히 물어본다. 그렇다 위 글은 리액트 네이티브 문서에서 가져온 것이다.

배열을 믿을 수 없다.
여러분은 배열이 "행과 열로 된, 비슷한 객체들의 질서정연한 배치"라 생각하는가? 아래를 보고 다시한번 생각해보아라.
자바스크립트에서 배열은 우리가 원래 배열이라 부르는 그것보다는, 보통의 자바스크립트 객체에 더 가깝다. 정확한 수서성의 결여와 가변성은 잘 동작하기 힘들게 만든다.

에러 핸들링하기 힘들다
자바스크립트에서는 경고 없이 런타임이나 예외를 던지는 함수를 만들 수 있다.
예외로 당신이 원하는 무엇(문자열, Date, 함수 등)이든 던질 수 있다. 팀원의 코드를 잠제적으로 크레쉬할 수 있다는 표시를 함수에 한다던가, 예외의 과정이 어떻게 되는지 명시할 메커니즘이 따로 없다. 문서에서는 대신에 if문을 사용하라고 권장한다.

당신의 예상하지 못한 예외 처리를 다루기 위해서는 마지막 라인의 방어로 예외를 남겨놓는게 최고이다. 그리고 예상되는 에러를 컨트롤 플로우 문으로 관리하기에도 최고이다.

디시멀(Decimal)을 지원하지 않는다.
하드웨어에서 대부분의 소수 자릿수들은 이진수로 정확하게 표현되지 않으며, 많은 프로그래밍 언어(자바스크립트와 Swift를 포함한)는 종종 수학적으로 잘못된 소수 계산을 내놓을 것이다.
이것이 왜 다른 언어의 표준 라이브러리가 소수 자릿수를 지원하는지 이유이다(예를들어 Swift에서는 Decimal을 사용할 수 있다). 자바스크립트에서는 서드파티 라이브러리나 여러분이 직접 만든 코드에 의지해야한다.

믿지 못하는 수학
초등학교 산수에도 주의를 기울여야함을 기억해라. 자바스크립트는 0과의 관계가 복잡하며 이것은 숫자가 아니다.

언세이프한 초기화
자바스크립트는 속성을 초기화 할 필요가 없으므로 객체를 만든 후 모순된 상태로 둘 수 있다.

if 다음에 선택적으로 가능한 커리 중괄호(curly brace)
if문 뒤에 커리 중괄호는 선택적이다.
여러분의 컨트롤 플로우 문에 모험의 취향을 추가한다.

모호한 커리 중괄호
프로그래머의 의도가 정확하게 추론된 것이 아니라면, 이 언어는 커리 중괄호를 선택적으로 할 수 있게 놔두지 않는다.

fallthrough 전환
switch  절에서 break를 깜빡했다면, 아래로 쭉쭉 떨어질 것이다. 또한 Swift에서는 케이스 철저성을 확인하지 않아도 된다.

어떤것이 '무(없음)'일까? (What's nothing)
 모든 의도나 목적에서 null이 아닌 것이 무가 될 수 있다. 도움이 되지 않는 구별이다.

그리고 변수에 어떤 데이터가 담겨있는지 아닌지 알고 싶다면 어떨까? 음, 확인이 너무 거추장스럽게 null과 undefined 둘 다 체크해야한다.


빈약한 표현력
  • 열거형이 없다. 연관타입을 가진 열거형은 말할것도 없다. 신뢰성 높은 상태 표현에 행운을 빌 수 있을까.
  • guard문이 없다.
  • 제네릭이 없다.
  • 컨트롤 플로우문의 표현력을 늘리기위한 where이 없다.

지극히 느린 진화
ES2016 때 자바스크립트에 새로 추가된 기능을 보라.
1. 배열을 위한 includes 메소드
배열이 특정 값을 가지고 있는지 확인한다. 아래에 어떻게 사용하는지 나와있다.
이건 사용하지 말자.
2. **연산자
지수 연산에 사용하기위한, a**b는 Math.pow(a, b)의 축약이다.

한번 생각해보면, 파이썬의 **연산자는 파이선1에 있었고, 루비의 **연산자는 20년도 전에 있었던 연산자이다.

따라서 자바스크립트는 이 기본적인 산수 연산자와 배열을 위한 꽤 제한된 맴버쉽 체크 메소드를 추가하는데 20년이나 걸렸고, 이 특징이 한 해중에 가장 의미있는 것이었다.

Flow로 구조받자!(Flow to the rescue!)
Flow는 위의 수많은 불평으로부터 내놓은 페이스북의 대답이다. 이것은 자바스크립트를 위한 정적 타입 체커이고, 여러분 코드에서 변수의 타입을 추론하고 추적할 수 있는 능력이 있으며, 여러분에게 일어날 운명(옮긴이: 실행되었을 때 일어날 오류)을 경고한다.

위에서 보았던 함수 기호의 결핍으로부터 나온 문제의 예제를 다시 보자.(number가 들어오길 예상한 divideByFour 함수는 문자열을 받는다) 여기엔 Flow가 이것을 어떻게 다루는지 보여준다.

함수 기호 부재 때문에 생기는 많은 문제를 고쳐준다.

제네릭 배열에도 동일하게 할 수 있다.

Flow는 nullability를 잘 다룰 수 있게해주고, 값이 null이되면 안되는데 null일 수 있을때 여러분에게 경고를 띄워 줄 것이다.

만약 j가 string으로 가정하지만, 언제든 null이 될 수 있으면, Flow는 그 사실을 우리에게 적절하게 알려주려 할 것이다. 따라서 인자로 string을 받되 null이면 안되는 곳에서 인자를 넘겨주기 전에 굳이 null 체크를 할 필요가 없다면 불만을 가질 수 있을 것이다.

Flow의 기능들은 타입체킹과 주석을 달아주는 것을 넘어, 새로운 구성체(new construct)를 지원한다. 그 중 하나는 리터럴 타입인데, 우리가 만드는데 사용할 수 있으며, 그 예로 열거형이 있다.

그리고 Flow는 direction이 "North"도 아니고 "South"도 아닌 것을 반환하려하면 뭔가 불평하고 있을 것이다.

내가 찾은 또다른 유용한 구성체는 유니온(Union) 타입이다. 이것은 딱 하나의 값으로 제한하고, 미리 정의해놓은 타입들 중에서 하나로 제한한다. 아래 예제는 Flow 문서에서 온 것이다.

Flow은 도움이되는한 꽤 도움이 되는 리액트 네이티브와 훌륭한 동반자이다. Flow 문서에서는 구성 요소의 유형에 올바르게 주석을 추가 할 때 어떤식으로 동작하는지 보여주는 좋은 예시를 제공한다.

저 주석은 Flow가 어디서 경고를 내는지 알려주며, 여러분 계약에 만족스럽지 않다고 충분히 꾸짖는다.

Flow는 전적으로 강력한 도구이다. 여기에는 여러분이 해볼 수 있는 유용한 커멘드라인 인터페이스 예제가 있다.
  • suggest는 주어진 파일에대해 타입 주석 제안을 보여준다(suggest Shows type annotation suggestions for given files)
  • type-at-pos는 주어진 파일과 위치에 타입을 보여준다(type-at-pos Shows the type at a given file and position)
  • get-def는 변수나 프로퍼티의 선언 위치를 받는다(get-def Gets the definition location of a variable or property)

Flow는 flossing와 비슷하다
이제 자바스크립트가 고쳐졌을까? 아니다.

Flow가 했을 공학의 노력에 감명받았고, 이것은 자바스크립트의 부모집합(superset)으로 남았으며, 따라서 당신을 태생적으로 약한 파운데이션에서 만들게 한다. 자바스크립트의 결점을 치료하는 어느 종류로서 Flow가 빗발치는 것은 (Monty Python and the Holy Grail으로부터) Swamp Castle and its King을 떠올리게 했다.

내가 여기 처음 왔을때, 여기는 전부 늪이였어. 여기 늪에 성을 세울거라는 말에 모두가 나를 얼간이라 했지만, 똑같이 만들어서 그냥 보여주었지. 그것이 늪으로 가라앉았고, 나는 두번째 것을 지었어. 그것도 늪으로 가라앉았고 세번째것을 또 지었어. 그것은 불에타고 쓰러져서 늪에 가라앉았어. 그러나 네번째는 버티고 있어. 그리고 이게 너가 얻어야 할 것이고, 영국에서 가장 강한 성을 얻어냈지.

안전하지 않은 파운데이션을 만들수 있을거라는 말이 파운데이션을 더 안전하게 만들진 않고, 더 효율적인 과정이지도 않을 것이다. 그리고 이 행동을 저지하려는 주장은 그 불합리함을 놓치게 만든다.

아마 더 의미있게, 자바스크립트 부모집합, 린터(linters), 정적 분석기는 당신이 더 안전한 언어를 고를 수 없는 플랫폼을 다룰때 완화시키는 방법으로서 일시적으로 억제해줄 것이다. 그게 된다면, 그것들을 사용하면서 도움을 구하지 않아도 될 것이다.

이런 일시적인 조치에는 또다른 근본적인 문제가 있다. 이것이 실제로 그 권위자가 쓰지도 않고 커뮤니티에서도 존경받지 못하면 법같은 이 안전장치는 아주 조금만 의미있게 된다.

Flow는 당신이 코드바운드로 리액트 네이티브 앱을 만들고 실행시키는 것부터 런타임 크레쉬를 생성하는 것까지 당신의 작업을 멈추게 하지는 않는다. 그리고 그것은 프로그래밍 언어에대한 기본적인 안전 요구사항이다. 만약 에러를 막을 수 있으면, 그 언어는 능동적으로 막으려고 할 수 있고, 안전하지 않은 코드를 짜고 실행시키는 것을 디폴트에의해 방해할 것이다(그 후에 하는게 아닌).

리턴값이 모호한 함수를 짜라고 팀에게 말할 수는 없을 것이고, 그것에 응답하지 않는 객체에 메소드를 호출하라고 나에게 말할 수도 없으며, 컴포넌트에 proptypes을 정의하지 않고 proptypes이 정의되있다는 코드 리뷰를 하는 동안 나에게 손수 끄집어내라고 할 수 없을 것이다.

유닛 테스트와 flossing과같은 Flow는 유익하고 선택적이고 지루한것의 저주를 낳는다. It’s in your next year’s resolutions.

그리고 여기에 현실이 있다. 깃헙에 공개된 .js 파일에 Flow를 사용하는 파일(즉, Flow가 체크할 수 있는 @flow가 포함되있는 파일)이 몇개나 될까? 어림잡아 80,000,000개의 .js파일에서 1,400,000개정도이다. 안전한 자바스크립트를 짜기 위해 2%보다 작은 곳에서 이 툴을 사용하고 있었다.

여기에 관련된 노트인, awesome-react-native에서 100개가 넘는 리액트 네이티브 저장소들이 있는데, 여기서 Flow를 제대로 사용하고 타입 주석을 사용한 저장소는 한개도 발견할 수 없었다. 오직 리액트 네이티브 튜토리얼에만 Flow를 사용하고 있었다.

자바스크립트 생태계: 속박과 굴레
자바스크립트 개발자를 제외한 나머지 모두는 자바스크립트의 결핍에대한 깊은 인상을 받은것 같다. 위에서 내가 설명한 자바스크립트는 흉측한 사마귀가 아니라는 사람들에게는, they’re “quirks” or “gotchas” that you, not your language, have to be on the lookout for.

왜냐하면 자바스크립트 개발자들은 자바스크립트가 불충분하다고 믿지 않기 때문이다.

언어에서 불변성을 지원하지 않는다? 그럼 만들면 된다. 언어에서 typing을 지원하지 않는다? 그럼 만들면 된다. 언어에서 decimal을 지원하지 않는다? 그럼 만들면 된다. 언어에서 안전한 함수형 프로그래밍 언어를 허용하지 않는다? 그럼 만들면 된다. 언어에서 nullability를 지원하지 않는다? 그럼 만들면 된다.

혹은... 당신이 이 기능들이 근본적으로 중요하다고 인정하면, 여러분은.. 바깥세상에서의 그것을 지원하는 언어로 전환해버리면 될까?(잘 모르겠다. 그냥 생각난것을 말한거다)

내 생각엔, 자바스크립트가 태생적으로 결핍하다고하여 그것을 계속 갈고 닦는게 아니라 다른것으로 대체하려 한다면 그것은 일반화된 고집의 거부(generalized obstinate refusal)이다. 갑자기 증가한 개선이나 버팀목의 결과는 살아있는 생태계의 신호로 보이나, 실제로 이것이 의미하는 것은 이 언어가 근본적으로 중요한 기능의 결핍이 있다는 것이다.

아래 그림은 이러한 상황을 만화로 잘 표현했다.

삽질으로부터의 자유는 당신이 필요한 것이 내장된 언어를 선택하는 것에 달렸다. 이러한 삽질은 에너지 낭비이다. 자바스크립트는 다음의 이유로 좋은 소프트웨어를 제작하는데 도움이 되지 않게 하고 있다: 자바스크립트는 우선 당신이 개발하게 만들고, 다른 언어가 제공하는 것에 의존적이게 만든다.

사슬(Chains)
자바스크립트는 튀긴 커다란 생선을 가지고 있다. 이 언어는 다양한 버전의 인터넷 브라우저의 몇십억 사용자 입맛에 맞춰주어야한다. 이것이 언어의 발전을 더디게 만든다.

typeof(null)==='object'를 기억하는가? 음, 예전에 type of null을 null로 바꾸자고 제안했었다. 그러나

이렇게하면 현존하는 수많은 사이트가 망가지게 될 것이다. 자바스크립트의 스피릿에 적합하지 않다.

그리고는 이 제안이 거절되었다. ES06에의해 null은 여전히 object이다.

자바스크립트의 발전 과정은 필요에의해 입맛에만 맞추고 있다.
  • 수많은 구식 버전의 브라우저 사용자들
  • 각색의 브라우저 벤더 집합
  • 수십억의 사이트와 그 사이트의 각 개발자들
이것들은 훌륭한 민주주의이긴하나 한편으로는 다른 언어에비해 굉장히 더디게 발전한다. 그리고 이것은 이식성이 좋은 만큼 개발자의 인간환경공학은 좋지 않을 것이다.

넓은 시각(Wider angles)
역사적인 관점에서, 한 언어가 인기를 얻고나서, 그것을 넘겨받고싶은 점유자들이 거부하게되는 패턴은 겉으로보기에 불충분한 언어에게 나타나는 익숙한 패턴이다.

여기 따뜻한 모닥불 옆에 앉아서 내 얘기를 좀 들어봐라.

1994년으로 돌아가보자. Richard Stallman은 "왜 Tcl을 쓰면 안되는가"라는 다소 모호한 타이틀로 comp.lang.tcl 뉴스그룹(news group)에 글을 하나 썼다. Stallman은 프로그래밍 언어로서 Tcl의 단점을 심술궂게 꾸짖었고, 목적에 맞지 않는 Tcl을 고발했다. 자바스크립트에대해 내 불평과 유사하게 매우 장관이었다.

Emacs의 주요 수업에선 확장을 위한 언어는 단지 "확장 언어"가 될 수 없다고 한다. 실제 프로그래밍언어가 되야하고, 상당한 양의 프로그램을 짤 수 있어야하며, 그것을 유지보수할 수 있게 설계되야한다. 왜냐하면 사람들이 그것을 바라니까! [...] Tcl은 진지한 프로그래밍 언어로 설계되지 않았다. 이것은 "스크립트 언어"가 되기위해 설계되었으며, "스크립트 언어"라 가정하는 것은 실제 프로그래밍 언어가 되려고 할 필요가 없었음을 의미한다. 따라서 Tcl은 하나의 능력을 빼먹었는데, 배열이 부족하다; 링크드리스트를 만들 수 있는 구조체를 빼먹었다. 그것은 가짜로 넘버를 가지고 있는데, 매우 느리지만 동작은 한다. Tcl은 작은 프로그램을 만드는데 적합하나, 그 넘어를 하기엔 적합하지 않을 것이다.

이 글은 1994년에 Tcl 전쟁을 촉발시킨 글이다. 그가 들었던 대답들중에 가장 인상적이었던 John Ousterhout(Tcl을 만든 사람)의 대답이다.

언어 설계자로서 이 언어가 왜 선천적으로 더 낫고 더 나쁜지 우열을 가리는 토론을 사랑한다. 그러나 이런 건방진 주장에는 많은 문제가 있다. 궁극적으로 모든 언어의 이슈는 그 사용자가 원하는 것을 투표할 때 합의가 생긴다. 만약 Tcl이 그들이 사용하는 것보다 더 생산적이게 만들었다면(혹은 이미 그렇게 되었다), 다른 언어 사용자들이 이것이 더 낫다고 하면서 온다면, 그때 사람들은 언어를 바꾼다. 이것이 법칙이고 이게 맞다.

Outsterhout의 반격은 내가 활동하고 있던 많은 사람들 앞에서 이루어졌고, 그의 일격은 더 가까이서 볼 수 있었다.

(몇몇 독자들은 Ousterhout가 순회적으로 말했다는 것을 알아차릴 것이다. "왜 사람들이 언어를 바꿀까? 그게 더 낫기 때문이다. 왜 그것이 더 나을까? 사람들이 많이 쓰기 때문이다." 그의 주장은 더 많은 설명이 필요함에도 불구하고 조금도 하지 않았는데, 우리는 종종 특정 기술 스택(예를들어 여러분이 생각하고 있는 리액트 네이티브)에 여러 주장에서 이런 추런 라인을 보았었기 때문이다)

Ousterhout에 따르면, 프로그래밍 언어는 그 언어가 더 낫다고 말할 수 있는 두가지 특징을 가지고 있는데, 바로 선천적인 특징들(프로그래밍 패러다임, 문법, 표준 라이브러리. 이식성과같은 그 고유의 특성)과 후천적인 특징들(수많은 개발자들이 폭넓게 채택한것)이다. 만약 언어A가 언어B보다 후천적으로 낫다면, 선천적으로 언어B가 언어A보다 더 낫다고해도 별로 소용없어질것이다.

다른말로 하자면, 채택되는 것이야말로 기술 우선순위에서 최고봉이다.(언어 설계자가 어떤것을 우위로 정의했는지 상관없이 말이다)

Ousterhout 주장에 딱 맞아보이는 앨범 커버이다.

더 많은 팬을 가진 가수가 그를 더 낫게 만든다. 요점은 팬이나 히트 기록의 양에의한 인기이며, 이것이 가수를 평가하는 좋은 기준이다.

그러나 언어가 널리 채택되고 그것으로 만들어진 멋진 앱들을 자랑할 수 있다는 사실로는 그 메리트로 당신의 판단에 설득시킬 수 없을지도 모른다.

복잡해보이거나 보기에 멋진 앱이 기술X로만 만들었다는 의미는 기술X가 복잡해보이거나 보기에 멋진 앱을 만들기에만 최적화되있고, 보통 일에는 최적화되있지 않다는 뜻일 수도 있다.

전세계에서 열리는 모래성 대회는 당신도 만드는데 쓸 수 있는 모래와 물로 아름다운 구조물을 만들어서 보여준다. 이것은 모래가 좋은 장난감임을 주장할 수 있는 근거가 될 수는 있으나, 모래성 대회를 근거로하여 모래로 여러분의 집을 지을 수 있다고 납득하기는 어려울 것이다. 그러나 많은 이들이 납득되어 버렸다.

The King of Swamp Castle은 수많은 해커의 왕이다.

비슷하게, 인기는 프로그래밍 언어를 평가하기엔 신뢰할 수 없는 기준이다. 아주 기본적인 이유로, 인기는 벤더를 속박시키거나(브라우저에서만 돌아가는 프로그래밍 언어가 된다던지), 벤드웨이건 효과나, 레거시(legacy) 코드베이스의 결과를 매우 잘 만들지도 모른다.

프로그래밍 언어의 고유 특징들(프로그래머 에러에 대비한 안전장치, 표준 라이브러리, 이식성 등)은 여러분의 기준에 기반해야한다. 이것은 생산성 향상의 기본요소이다.

사실은 많은 것들이 부정적인 생산성을 이끌기도 한다. 그 예이다.
  • 피할 수 있는 에러를 만드는 당신을 막는 안전장치의 부족
  • 많은 목적에 맞지 않는 작은 표준 라이브러리
  • 당신의 의도를 명확하게 표현하지 않는 문법과 의미론(syntax and sementics)
  • 더딘 발전
  • 위의 모든 것들(자바스크립트)

의존성
리액트 네이티브는 총 648개의 의존성을 가지고 있다.

특별히 놀랄 것 없게도, 의존성 체인은 세상의 npm 중에 길수 있으므로, 리액트 네이티브의 패키지 매니저이다.(원문: Not particularly surprising, as dependency chains can be long in the world of npm, React Native’s package manager.)

이 광경은 오픈소스의 동료관계이다. 여러분의 앱은 600명 이상의 사람들의 지속된 노력으로 만들어졌다.

이게 함정이기도한데, 여러분은 648명의 자원 봉사자가 그 라이브러리를 유지해주어야 앱이 잘 돌아가며, 그 자원봉사자들로부터 어떤 약속 따윈 없다.

그들의 라이센스가 당신의 소프트웨어에 맞게 유지될까? 희망적으로?

그리고 그 구현이 모두 보안면에서 최고의 실천을 하고 있을까? 혹은 648개의 별개의 잠재적인 보안 리스크를 묵인할 수 있겠는가?

더 나은 제안들
크로스-플랫폼 개발 때문에 리액트 네이티브를 선택하게 되었다면, 다른 옵션도 생각해볼 필요가 있다.

리액트 네이티브는 지금 Xamarin과 Appcelerator라는 두개의 크로스 플랫폼 개발과 경쟁하고 있다.

Xamarin과 Appcelerator 둘 다 iOS, 안드로이드, 윈도우 폰을 지원한다. 그리고 둘 다 아래의 것들도 지원한다.
  • 더 종합적인 API
  • 더 성숙한 IDE
  • 더 나은 문서
  • 명확하고 친숙한 라이센싱
  • 동등한 퍼포먼스(더 낮진 않더라도)

Xamarin 개발은 C#으로 하는데, 이것은 자바스크립트에 비해서 버그를 줄여주는 경향이 있고, 더 표현력이 좋다(more expressive). 만약 자바스크립트를 쓰는 개발자라면 Appcelerator 개발이 자바스크립트로 할 수 있다.

리액트 네이티브와 비교하여 Xamarin과 Appcelerator 둘 다 멀리 보았을때 더 나은 가능성을 가지고 있다. Appcelerator(Titanium의 아버지)(3억  5천만 기기에 앱으로 실행 되었었다)는 2016년 1월에 인수되었고 Xamarin(포춘지 선정 500 대 기업에서 100 기업 이상 사용했다)는 마이크로소프트에의해 2016년 2월에 인수되었다. 둘 다 이전보다 8천만달러 이상의 주식형펀드가 올랐다.

나에게 지원해주기 위한 비즈니스의 회사가 빽으로 있다면, 프로젝트들은 이 크로스 플랫폼이라는 레드오션에서 신뢰할 수 있는 소프트웨어 개발 플랫폼으로 살아남고 성장해야한다(최근에 세어보니 10개 이상의 활발한 개발 프로젝트들이 있었다).

(의미있는 언급으로 Flutter(이하 플루터)에 대해 이야기 하겠다. 이것은 구글이 만든 크로스-플랫폼 개발 플랫폼이다(iOS, 안드로이드 둘다 지원한다). Dart라는 언어를 사용하는데, 이 언어는 자바스크립트보다 안전하고 표현력도 더 좋다. 또한 메트리얼 디자인 원칙을 따르게 도와주고, 네이티브하게 컴파일 된 코드를 만들어준다. Xamarin과 Appcelerator와는 다르게 오픈소스이며, 아직 제품으로 준비되지는 않았지만 그럼에도 불구하고 약속해놓았다.)

결론
좋은 소프트웨어 개발 플랫폼은 4가지 필수 특징을 가진다.
  • 이식성 - 한가지 이상의 플랫폼을 타겟으로 한다.
  • 생산성 - 성숙한 IDE와 다른 개발 툴, 문서, 그리고 표현력 있는 그 개발 언어
  • 안전 - 당신이 만들 수 있는 실수를 그 플랫폼이 얼마나 막을 수 있는지의 정도
  • 지속성(longevity) - 당신의 앱이 살아있는 동안 플랫폼이 얼마나 오래 가는지
비록 우리 산업이 아직까지는 이것을 가늠하기위한 확립된 오픈 표준이 없지만, 나는 내 경험과 조사를 바탕으로 내 평가를 공유하고 싶다.


리액트 네이티브의 강점을 이식성과 생산정의 면에서 보자면 너무 과하고, 안전, 장기간 프로젝트에는 불확실성, 위압적인 특허 라이센스에는 부족함이 있다.


Xamarin과 Appcelerator와같은 성숙한 플랫폼은 최고의 이식성(윈도우 폰을 지원한다), 성숙된 개발 툴, 프로그래밍 언어를 선택할 수 있게 해주는 것을 제공하는데, 언어 선택으로 생산성과 안전성을 얻어낼 수도 있다. 그리고 이것들이 잘 투자받은 회사의 핵심 제품이라는 사실이 장기간의 면에서 안심시켜준다.


Swift 개발은 안전성, 지속성, 생산성 면에서는 좋다. 이식성의 면은 상대적으로 낮으나 무시해도 되는 수준이다. 안드로이드 버전을 개발 할 수 있게 해주는건 아니지만 macOS 버전이나 백엔드 서버 앱을 만들 수 있게 한다.





여기까지가 내가 왜 리액트 네이티브 개발자가 되지 않는지의 그 이유였다.

감사
이 글의 초안에 도움을 준 Rami Chowdhury와 Alkis Papadakis에게 감사를 표한다. 그리고 난해한 IP 텍스트들을 해독하는데 도움을 준 Greg McMullen에게도 감사하다.

바뀐 이력
이 글이 바뀐 이력은 여기에서 확인할 수 있다.

커멘트
이 글에 대한 토론 에 팔로우하여 참여하고 싶으면 Hacker News, Hacker News(Again)/r/programming로 와라.

참조
 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글  2개가 달렸습니다.
  1. 웹쪽에 관심이 많아서 자연스럽게 리액트js에 관심이 있었습니다. 그런데 네이티브로도 사용할 수 있다고 해서 과연 어떨지 궁금하던 차에 좋은 비판글을 읽어서 생각 정리하는데 도움이 많이 됐습니다! 그리고 넓은 시각 문단에서 'ㅇ낳는' 오타 발견했어요!
secret

기억할것: Swift는 여전히 ABI-안정성이 지원되지 않음!
이 글을 읽기 전에 모두가 알아야 할 사실은 Swift가 여전히 ABI에 안정적이지 않다는 사실이다. 이 말은 이전 버전의 Swift 컴파일러로 만든 바이너리가 현재 버전과 호환이 되지 않는다는 의미이다. 좀 더 알아듣기 쉽게 설명하자면 여러분의 모든 파일과 의존성들은 같은 버전의 Swift로 컴파일해야한다는 의미이다: 다른 버전의 Swift로 작성된 코드들은 합칠 수 없다.

애플은 현재 최신버전의 Swift3.0을 깨부수면서 (약속한것은 아니지만) 이것을 해결하려고 노력중에 있으며 Swift4에서 ABI 안정성을 지원할 계획이다.

2.3으로 가야할까 3.0으로 가야할까?
만약 여러분이 우리 VTS처럼 중요한 Swift 코드베이스를 가지고 있다면 아래에 해답이 있다.

그렇다. 두가지 다이다.

여러분의 Swift 코드베이스가 지저분하지 않다면 2.3을 생략하고 3.0으로 넘어가면 된다.

아래에 염두할 것들이다.
  1. 의존성은 중요하다. 여러분의 의존성이 Swift2.3이나 Swift3.0을 지원하는지 확인해야한다. 대부분 주요 라이브러리/프레임워크들은 둘 다 지원하고 있다(Alamofire, Charts). 그러나 기억해야할 것은 Swift3.0을 업그레이드 하는 방법은 하나밖에 없다. Swift3.0으로 새 개발을 시작하고 나면 2.3버전의 모든 의존성이 업데이트 없이는 작동하지 않을 것이다.
  2. 8.x 버전의 Xcode는 Swift2.3 지원을 멈출 것이다. Swift2.3은 Swift3.0으로 업그레이드 하기 힘든 프로젝트를 위한 매개 단계의 경향이 있다.
  3. 만약 여러 프로젝트에 걸쳐진 개인의 CocoaPods이 있다면 먼저 이것부터 업그레이드 해야한다.

2.3으로 업그레이드 할 수 있으나 어느정도 지연이 될지도 모른다. Xcode9가 배포되기 전에 Swift3으로 갈아타야하기 때문이다.

Swift2.2에서 2.3으로 마이그레이션 이슈
마이그레이터는 아마 Range<T>를 CountableRange<T>로 변환할 것이다. Swift3에는 오직 CountableRange<T> 밖에 없기 때문이다.

마이그레이터는 dispatch_queue_set_specific() 사용을 위해 /*Migrator FIXME: Use a variable of type DispatchSpecificKey*/ 주석을 달아 줄 것이다. 이것은 미래의 Swift3으로 마이그레이션 하는 것과 관련이 있다.

Swift2.x에서 Swift3.0으로의 마이그레이션 이슈
Swift의 3.0버전은 "모든걸 깨부순(break everything)" 배포이다. Swift3은 모든 것을 깨부수고 언어적 긴 용어가 필요없는 모든 것을 없앨 것이다. 따라서 많은 변화가 생기고, 프로젝트를 안정화시키기위해 그 많은 것들을 고쳐야할 것이다.

Swift2.3
Swift2.3은 굉장히 마이너한 업데이트이다. 어떤 식으로 마이너할까? 사실 딱 한가지가 바뀌었다. 컴파일타임 nullability가 바뀐 애플 SDK의 Objective-C 코드를 체크하는 것이다.

몇몇 마이너한 이름 재정의과 수많은 옵셔널 생성자와 같은 다른 변화들은 이제 옵셔널 대신에 필요한 오브젝트를 반환한다. 이것은 번들로부터 nib을 인스턴스화하는 것과 같은 것들에 적용됐다.

Swift3.0

(인지할만한)주요 변화들
private의 정의는 fileprivate로 바뀌었다.

공식적으로 private라 불리던 것이 이제 fileprivate로 바뀌었다. fileprivate 변수는 extension으로 접근할 수 있다. private 변수는 클래스에서 extension으로 접근할 수 없다.


public의 정의는 open으로 바뀌었다.
현재 클래스나 파라미터 public 정의는 두가지 기능을 제공한다.
  1. 외부모듈이 클래스나 맴버를 사용할 수 있다.
  2. 외부모듈이 클래스나 맴버를 오버라이드 할 수 있다.

Swift3에서의 public은 외부적으로 사용가능함이지, 오버라이드 가능함은 아니다.
이전의 이 기능은 open으로 되었다.


동사와 명사
함수 이름이 -d(과거형)으로 끝나는 것들은 그 오브젝트의 새 인스턴스를 반환한다.
이러한 변화는 reverse와 reversed, enumerate와 enumerated 등에 적용한다.

Objective-C의 불리언은 이제 is라는 단어로 시작되고 Foundation 타입은 더이상 접두에 NS를 사용하지 않는다(NSString처럼 String과 충돌이 일어나는 경우들은 빼고 말이다.).

Foundation 타입은 Swift의 let과 var 선언에서 아지 잘 동작할 것이다.



맴버로 불러오기
설명이 달린 C 함수들을 메소드로 부를 수 있다. 임포터(importer)는 보통 이러한 맵핑을 자동으로 추론할 수 있아며 Swift에 예전의 C-API를 자연스럽고 네이티브하게 사용할 수 있게 해준다. Case and point, CoreGraphics API. 참고로 CoreGraphics는 이번 배포때 공식적으로 업데이트 되지 않았다.


케멀케이스(CamelCase) 변화들
열거형이나 프로퍼티에서 머리문자(CG, NS등)로 시작하는 것들이 UpperCamelCase에서 LowerCamelCase로 대체되었다. 
// Before
let red = UIColor.redColor().CGColor
// After
let red = UIColor.red.cgColor

(개인적으로 가장 작은 변화라 생각되는)상태절 변화

이제부터는 guard, if, while절에서 where 키워드를 사용할 수 없다. where 키워드는 for-in-where 형태의 for문에서는 사용할 수있다.
case 절에서도 마찬가지로 바뀌었다.


첫번째 인자의 문자 일관성
이렇게 이해하면 쉽다: 첫번째 파라미터의 이름은 디폴트로 필요하다.


(내가 좋아하는 변화인)묵시적으로 언랩핑된 옵셔널 다루기
여러분이 여러 언어(shared-language)로 프로젝트를 할 때 이 점 덕분에 마이그레이션 작업이 의미있어진다고 생각이 든다. 그것이 무엇일까?

이전에 ImplicitlyUnwrappedType! 프로퍼티를 가진 Objective-C 타입이 이제 WrappedType?로 되었다. 아래의 경우를 제외하고 모두 적용된다.

더 나은 Objective-C API 변환
네이밍이 더 명확해졌다.

최신식 디스패치

컬랙션 타입이 새로운 모델을 가지게 됨
이전에 컬랙션 타입에서 한 인덱스에서 시작하여 탐색할 때는 index의 successor 메소드를 사용해야 했다. 이제는 이 책임이 컬랙션으로 넘어가게 되었다. c.index(after:index) 이런식으로 작성한다. 컬랙션은 이제 어떤 comparable 타입의 인덱스를 가진다.
(아래 부분은 Range 오브젝트를 손수 만들때 적용된다. 보통 이렇게 할 일은 드물거라 생각된다.)

이런 변화에서 사이드 이팩트로서 Range가 여러 타입으로 쪼개어졌다(Range, ClosedRange, CountableRange, CountableClosedRange) ClosedRanged는 이제 그 타입(0...Int8.max)의 최대값 범위를 포함한다. Range와 ClosedRange는 더이상 반복(iterate)을 할 수 없다. 그 의무로서 오직 Comparable 오브젝트만 필요하다. 따라서 Range<String>을 만들 수 있다.

Objective-C의 id는 Swift의 Any 타입으로 불러와진다.
애플의 말을 인용하자면
이제부터 id는 'AnyObject'가 아닌 'Any'로 불러오기 때문에, 이전에 여러분이 'AnyObject'로 동적인 검색을 수행하는 곳에서 애러가 뜰 수도 있다.

(인식하지 못할지도 모르는)작은 변화들
옵셔널 비교연산자가 제거됨
현재 nil 타입은 비교가능하다.

이 점은 아래와같이 버그를 만들기 쉽다.

이제는 이것들을 비교하기 전에 반드시 언랩핑을 해주어야한다.
이것이 굉장히 좋은 점 중 하나이기도 하지만 유닛테스트를 망가뜨릴지도 모른다.

클러저 파라미터 이름과 레이블
수많은 클로저 파라미터 이름이 재정의되고 선택적으로 바뀌었다.

flatten이 join으로 명칭이 바뀌었다.

UnsafePointer<T> 다루기
오브젝트가 아닌 것들의 포인터 타입의 nullability는 옵셔널을 사용해 표현할 수 있다.

부동 소숫점 값을 반올림하는 기능은 이제 그 값이 가지고 있다.
이전에는 부동 소숫점을 반올림하기 위해 전역의 C함수(float나 ceil)을 사용할 수 있었다. 아직 이 함수들도 사용가능하나 디프리케이트 될지 고려되고 있다.

대신 아래와 같이 사용할 수 있다.
추가적인 반올림 기준이다.
  1. toNearestOrAwayFromZero
  2. toNearestOrEven
  3. towardZero
  4. awayFromZero

제네릭 타입 에일리어스

연산자 정의 문법의 변화

Objective-C의 상수는 이제 Swift 타입이다.
이제 Objective-C inter-op 식의 문자열을 사용할 수 없다.

굳이 걱정하지 않을 정도로 작은 것
NSError의 Bridging이 강화되었다.

nulTerminatedUTF8CString이 utf8CString으로 이름이 바뀜

문자열의 UnicodeScalar 생성자 중복을 제거

실패할 수 있는 UnicodeScalar 생성자는 이제 옵셔널을 반환한다.

더이상 튜플 splatting이 안된다.

더이상 curry한 func 선언 문법이 안된다.

이 모든 변화가 Swift3에서 일나날까?
아니다.

아직 이번 배포를 위해 검토하는 계획 안이다.(옮긴이: Swift3은 16.9.13에 정식 배포가 되었는데, 이 글은 16.8.31에 쓰여졌다)  몇 계획은 Swift3.x에서 달라질 것이고, 우리가 일반적으로 사용하지 않는 API 호출의 제거/병합과 같은 아주 마이너한 변화들은 포함시키지 않았다. 

추가로 나는 이 개별적인 변화를 깊게 볼 필요가 없었으며, 대신 각 변화에대한 높은 수준에서 개괄적인 설명을 하였다. 몇 변화는 그 영역에서 파급표과를 가지고 있으며, 여러분이 이러한 변화에 관심이 있으면 Swift EVO project를 한번 방문해 보아라.

이번 배포에서 검토 진행/대기중인 변화들
  1. Sequence-기반 생성자를 추가하고 Dictionary에 메소드를 합친다.
  2. 약 참조(weak reference)에서 강 참조(strong reference)로 self를 업그레이드하여 옵셔널 바인딩을 사용할 수 있게 해준다.
  3. 표준 라이브러리에 AnyHashable을 추가한다.

3.x 배포에서 바뀐것들
  1. 순환 알고리즘
  2. .self 제거
  3. 커스텀 Objective-C 표현을 제공하는 것을 Swift 타입에서 허용
  4. 동적 케스터로부터 연결 변환 동작을 제거
  5. Sequence end-operation 이름을 합리화

보너스
제거되어서 우리 모두가 기쁜 것들

  1. Swift 언어에서 where문 제거
  2. 인스턴스 맴버를 접근하기 위해 self가 필요함
  3. extension에서 접근 변경자 제거


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret


이 튜토리얼은 비도오 시리즈로 나와있다.
라이브 코딩과 함께 비디오 튜토리얼을 보고 싶다면 이 글의 내용으로 녹화한 비디오 시리즈를 보아라: Egghead - Introduction to Reactive Programming

여러분은 Rx, Bacon.js, RAC등의 다양한 것들을 포함해서 리액티브 프로그래밍이라 불리는 새 것을 배우는데 관심이 있을 것이다.

이것을 배우기는 쉽지 않은데, 좋은 도구가 없다는 점이 한 몫을 더한다. 내가 처음 배우려할 때도 튜토리얼을 찾아보고자 했다. 나는 소량의 가이드를 찾아냈지만 그것도 겉핥기 식이었지 전체 구조를 리액티브로 만드는 가이드는 없었다. 또한 라이브러리는 당신이 몇몇의 기능만 이해하려 할 때 크게 도움이 되지 않을 때가 종종 있다. 아래를 보면 무슨 의미인지 공감할 수 있을 것이다.

Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
Projects each element of an observable sequence into a new sequence of observable sequences by incorporating the element's index and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.

아이고..

나는 두가지 책을 읽었는데, 하나는 그냥 큰 그림을 설명하는 것이었고, 다른 하나는 리액티브 라이브러리를 어떻게 사용하는지 깊이 이야기한 책이었다. 나는 마침내 실제 만들어 보면서 리액티브 프로그래밍 배우는 것을 힘겹게 끝냈다. 나의 Futurice라는 일에서 리액티브 프로그래밍을 실제 프로젝트에 사용하였고 내가 문제가 생겼을 때 몇몇 동료로부터 도움을 얻을 수 있었다.

이것을 배우면서 가장 어려웠던 점은 리액티브하게 생각해야한다는 것이었다. 자꾸 이전의 명령형이나 상태를 가지는 전형적인 프로그래밍으로 가려는 것을 강제로 새로운 패러다임으로 작업하게 만들었다. 이 부분에 대해서는 인터넷에서 어떤 가이드도 찾지 못했다. 나는 사람들이 새로 이것을 시작할 수 있게 어떻게 리액티브하게 생각하는지에 대한 실질적인 튜토리얼이 필요하다고 생각했다. 이 생각이 조금 바뀌고 나면 라이브러리의 문서가 좀 이해가 될 것이며, 이 글이 당신에게 도움이 되길 바란다.

"리액티브 프로그래밍이 뭔가?"
인터넷 상에는 좋지 않은 설명과 정의가 있었다. 위키피티아는 너무 일반적이고 이론적이게 설명해 놓았다. 스택오버플로우의 인기있는 답변은 새내기들에게 적합하지 않아 보인다. 리액티브 매니페스트는 당신 회사에 프로젝트 매니저나 영업자에게나 보여줄 법 하다. 마이크로소프트의 Rx용어 "Rx = observables + LINQ + Schedulers"는 마이크로소프트한것이 우리에게 혼동만 남겨준다. "리액티브"나 "변화의 전파(propagation of change)"와 같은 용어는 일반적인 MV * 나 즐겨쓰는 언어가 이미 한 것과 다른 내용이 아니다. 물론 내 프레임워크의 뷰는 모델에게 리액트한다. 물론 변화는 전파된다. 그렇게 하지 않으면 아무것도 렌더링 되지 않을 것이다.

그럼 이제 잡소리는 집어치우자.

리액티브 프로그래밍은 비동기 데이터 스트림으로 프로그래밍 하는 것이다.

뭔가 새로운 것이 있는게 아니다. 이벤트 버스나 여러분의 일반적인 클릭  이벤트는 굉장히 비동기적인 이벤트 스트림인데, 이것을 옵져브 할수도 있고, 다른 추가적인 효과를 줄 수도 있다. 리액티브 스테로이드(옮긴이: 화학에 관련된 용어)로부터 떠오른 아이디어이다. 여러분은 클릭이나 마우스 호버(hover) 이벤트 뿐만 아니라 어떠한 데이터 스트림도 만들 수 있다. 변수, 유저입력, 프로퍼티, 캐시, 데이터 구조체등 어떤것도 스트림이 될 수 있으며, 스트림은 싸고 범용적이다. 예를들어 당신의 트윗 피드가 클릭  이벤트와 같은 방식으로 데이터 스트림이라고 상상해보아라. 당신은 그것을 듣고(listen)있다가 적절하게 반응을 하면 된다.

최상부에는 어떤 스트림에서도 합치고, 생성하며, 필터링할 수 있는 툴박스를 제공한다. 이것이 "함수형"의 마법 효과이다. 한 스트림은 다른 것의 입력으로 사용될 수 있다. 여러개의 스트림까지도 다른 스트림의 입력으로 사용될 수 있다. 두 스트림을 합칠 수 있다(merge). 원하는 이벤트만 골라내어 새 스트림으로 필터링할 수 있다(filter). 한 스트림을 새 스트림으로 각 값들을 매핑할 수 있다(map).

리액티브에서 스트림이 중심이 되면, 스트림을 잘 살펴보자. 우리는 친숙한 "버튼 클릭" 이벤트로 이 이야기를 시작하려한다.


한 스트림은 시간 순서로 정렬된 이벤트 순서이다. 이 스트림 값(어떤 타입), 에러, '완료'신호 3개의 상태를 내뱉을 수 있다. "완료" 지점이 되었다고 하자. 예를들어 버튼이 있는 현재 윈도우나 뷰가 닫혔을 때 이다.


우리는 발생된 이벤트를 비동기적으로 잡아내야하는데, 값을 내뱉을 때 실행되는 함수, 에러를 내뱉을 때 실행되는 함수, '완료'를 내뱉을 때 실해오디는 함수를 선언해놓고 잡아낼 수 있다. 때론 뒤에 두가지 함수는 생략하고 값을 위한 함수만 사용할 수도 있다. 스트림을 "듣고(listen)"있는 것은 '구독하고 있다'고 부른다. 우리가 정의한 함수는 옵저버이다. 스트림은 옵저베이블(observable)이다. 이것이 바로 옵저버 디자인 패턴이다.

이제는 위 그림 대신에 아스키(ASCII)로 설명을 대신하겠다.
--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
여기까지는 꽤 친숙할 것이고 여러분을 지루하게 하고 싶지 않다. 이번에는 원래의 클릭 이벤트 스트림에서 변형하여 만든 새 클릭 이벤트 스트림을 만들어보자.

먼저 숫자를 세는 스트림을 만들자. 이 스트림은 버튼이 얼마나 클릭됐는지 나타낸다. 일반적인 리액티브 라이브러리에서는 map, filter, scan과 같이 제공되는 많은 함수를 가지고 있다. 만약 당신이 clickStream.map(f)처럼 함수를 호출하면 clickStream으로부터 새 스트림을 반환받는다. 원래의 clickStream은 손대지 않고 말이다. 이러한 특징을 불변성이라 부르며, 마치 팬케잌에 시럽이 좋듯 리액티브에 좋게 해준다. 이것은 clickStream.map(f).scan(g)와 같이 함수 체이닝을 가능하게도 해준다.
  clickStream: ---c----c--c----c------c-->
               vvvvv map(c becomes 1) vvvv
               ---1----1--1----1------1-->
               vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f) 함수는 당신이 만든 f함수에 따라 만들어진 각 값으로 대체한다. 우리의 경우 각 클릭마다 숫자 1로 매핑시켰고 scan(g) 함수가 값 x=9(acculated, current)를 실행하여 스트림의 모든 이전 값을 합한다. 여기서 g함수는 단지 더하는 함수이다. 그리고 counterStream은 클릭이 일어난 수를 내뱉는다.

리액티브의 실제 힘을 보여주기 위해 여러분이 갑자기 '더블 클릭' 이벤트의 스트림이 필요하다고 해보자. 좀 더 흥미롭게 만들기 위해 더블클릭처럼 트리플클릭도 되는지 혹은 다중클릭(더블클릭 이상)까지도 되는지 보자. 깊게 숨을 들이마쉬고 생각해보자. 전통적인 명령형의 상태와 함께 사용한 방식에서는 어떻게 구현했는지 상상해보라. 아마 꽤 지저분하게 몇몇 변수는 상태를 가지고 있었을 것이고 몇몇 변수는 시간차를 세고 있을 것이다.

흠 리액티브에서는 꽤 간단하게 해결된다. 사실 로직은 4줄의 코드 밖에 안된다. 그러나 지금 당장은 코드를 보지 말자. 당신이 초보자이든 숙련자이든 스트림을 이해하게 만드는 데는 그림으로 설명하는 것이 최고이다.


회색 상자들은 한 스트림에서 다른 스트림으로 변경시키는 것이다. 먼저 250ms동안 이벤트 발생이 없으면 클릭했던 이벤트들을 리스트로 모은다. (이게 buffer(stream.throttle(250ms))가 무슨 일을 하는지의 이야기이다. 이 시점에서 굳이 깊게 이해하려고 하지 말자. 지금은 단지 리액티브로 데모를 만들고 있다.) 그리고 map()을 적용시켜 각 리스트의 크기를 정수로 매칭시키고, 매핑한 리스트의 스트림으로 결과가 나온다. 마지막으로 filter(x>=2)함수를 이용해 정수 1은 무시한다. 우리가 의도한 스트림으로 만들기 위해 사용한 것은 3개의 오퍼레이션이 다다. 이제 우리가 원하는 곳에 반응해주기위해 이것을 구독할 수 있다.

여러분이 이 아름다운 방법을 잘 음미했길 바란다. 이 예제는 사실 빙산의 일각이다. API 응답 스트림처럼 다른 스트림을 이 오퍼레이션으로 적용할 수 있고 또 수많은 다른 함수들도 사용할 수 있다.

"왜 RP 적용시키기를 고려해야할까?"
리액티브 프로그래밍은 당신 코드의 추상화 수준까지 올라왔으므로, 수많은 세부구현을 끊임없이 할게 아니라 비즈니스 로직을 정의하는 이벤트의 상호 의존에 초점을 맞출 수 있었다. RP의 코드는 더 간결해 질것이다.

데이터 이벤트에 관련한 수많은 UI 이벤트와 높게 소통하는 현대 웹앱과 모바일앱에서는 그 이점이 더욱 분명하다. 10년전에는 백엔드에 긴 형식으로 보내면 간단하게 프론트엔드에 렌더링 시키는 것이 웹페이지의 인터렉션이었다. 앱은 더 실시간으로 진화되었다. 몇 컨텐츠는 연결된 다른 사용자에게 실시간으로 반영되는 것처럼 가볍게 한 필드를 수정하는 것이 바로 백엔드에 저장하는 트리거가 될 수 있다.

오늘날의 앱은 사용자에게 높은 소통을 경험하게 해주는 수많은 종류의 실시간 이벤트가 들어가있다. 우리는 이것을 다루기위해 적절한 툴을 골라야하는데, 리액티브 프로그래밍이 그 해답이다.

예제와함께 RP처럼 생각하기
실제 예제로 들어가보자. 어떻게 RP처럼 생각하는지 단계별로 설명하는 실세계의 예제이다. 가짜 예제도 아니고 개념의 일부만 설명하기 위한 예제도 아니다. 이 튜토리얼이 끝날때쯤 우리가 왜 이것들을 했는지 아는체로 실제 함수형 코드를 만들 것이다.

나는 다음의 이유로 자바스크립트RxJS를 사용할 것이다. 자바스크립트가 현재 가장 인기있는 언어이며 Rx* 라이브러리 패밀리가 많은 언어나 플랫폼에서 가장 폭넓게 쓰이고 있다.(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy 등등) 따라서 당신이 어떤 툴을 쓰든, 이 튜토리얼을 따라오면서 구체적인 이점을 얻을 수 있을 것이다.

"Who to follow" 제안 박스 구현하기
트위터에서는 당신이 팔로우할 수 있는 다른 계정들을 제안하는 UI 요소가 있다.

우리는 이 핵심 기능을 모방해 볼것이다.
  • 시작하면 API로부터 계정 데이터를 불러오고 3가지 제안을 띄운다.
  • "Refresh"를 클릭하면 3개의 또다른 계정을 띄운다.
  • 계정중에 'x' 버튼을 누르면 그 계정은 사라지고 다른 계정을 띄운다.
  • 각 줄은 계정의 아바타를 띄우고, 누르면 그들 페이지로 이동한다.
다른 기능이나 버튼은 부수적이기 때문에 남겨 둘 것이다. 그리고 트위터 API는 최근들어 승인이 필요하게 되었으므로 대신에 깃헙의 팔로잉 사람들을 위한 UI를 만들어보자. 여기에 사용자를 얻어내기 위한 Github API(링크)가 있다.

완성된 코드는 http://jsfiddle.net/staltz/8jFJH/48/ 여기에 준비되있다.

요청과 응답
어덯게 Rx로 이 문제를 접근할까? 흠, 시작하기 앞서, (대부분) 모든 것은 스트림이다. 이것이 Rx의 주문이다. 가장 쉬운 "시작하면 API로부터 3가지 제안을 띄운다" 기능부터 시작해보자. 특별한 것은 없어 보이며 간단하게 (1)요청을 날리고 (2)응답을 받고 (3)그 응답을 표시하면 된다. 그럼 이제 우리의 요청을 스트림으로 표현해보자. 처음 봤을때는 좀 힘들어 보이지만 우리는 기초부터 시작해야 한다.

시작하면 한 요청만 날리면 되고, 데이터 스트림으로 모델링하면 하나의 값만 스트림이 될 것이다. 그 후에는 우리도 알듯 많은 요청을 보내게 될테지만, 지금은 하나만 해보자.
--a------|->

Where a is the string 'https://api.github.com/users'
이것은 우리가 요청할 URL의 스트림이다. 요청 이벤트가 발생하면 언제, 무엇이 발생했는지 알려준다. 요청이 "언제" 호출될지는 이벤트가 언제 발생되는지와 같은 시점이다. 그리고 "무엇이" 요청됐는지는 발생된 값이다.(URL을 담고 있는 문자열)

Rx* 한 값으로 이런 스트림을 만드는 것은 굉장히 간단하다. 스트림에서 공식적인 용어는 "Observable"이다. 이것을 Observe 할 수 있다고 하지만 이렇게 바보처럼 말하지 말고, 이것을 스트림이라 부르겠다.

var requestStream = Rx.Observable.just('https://api.github.com/users');

이제부터는 이것이 문자열의 스트림이고 다른 동작을 하지 않으며, 값이 나올때 우리가 필요한대로 어떻게 처리할 수 있다. 이것을 subscribing으로 스트림에 할 수 있다.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  jQuery.getJSON(requestUrl, function(responseData) {
    // ...
  });
}

우리는 요청 오퍼레이션의 비동기 처리를 위해 jQuery Ajax 콜백(여러분은 이미 알고 있다고 가정한다)을 사용한다. 그런데 가만보자. Rx는 비동기 데이터 스트림을 다루기위해 있다. 그 요청에 대한 응답이 곧 있다가 받을 데이터를 담은 스트림일 수는 없을까? 음, 개념상으로는 가능해보이니 한번 시도해보자.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  var responseStream = Rx.Observable.create(function (observer) {
    jQuery.getJSON(requestUrl)
    .done(function(response) { observer.onNext(response); })
    .fail(function(jqXHR, status, error) { observer.onError(error); })
    .always(function() { observer.onCompleted(); });
  });

  responseStream.subscribe(function(response) {
    // do something with the response
  });
}

Rx.Observable.create()가 하는 일은 각 Observer(혹은 다른말로 '구독자')에게 데이터 이벤트(onNext())나 에러(onError())를 명시적으로 알리는 커스텀 스트림을 만든다. 우리가 한 일은 그냥 jQuery Ajax Promise을 감싼 것이다. 잠시만요, 이게 Promise이란게 Observable을 의미하는 건가요?




그렇다.

Observable은 보장++이다. Rx에서 당신은 var stream = Rx.Observable.fromPromise(promise)하여 쉽게 Promise를 Observable로 변환할 수 있다. 그러니 이것을 사용해보자. 다지 다른 점은 Observable이 Promise/A++를 따르지 않지만 개념적으로 충돌은 없다. 간단하게 한 보장은 하나의 발생된 값과 함께 한 Observable이다. 꽤 좋아보인다. 적어도 Observable이 Promise만큼 강력한지 보여준다.

여러분이 Promise의 속임수라 믿는다면 Rx Observable이 어떤 것이 유능한지 눈으로 지켜보자.

이제 예제로 다시 돌아와서, 새 subscribe()를 또 하나더 그 안에서 호출하면 콜백지옥 같은 형태가 되버린다. 또한 responseStream 생성은 requestStream에의해 결정된다. 이전에도 들었듯 Rx에서는 새 스트림을 변형, 생성해내는 간단한 매커니즘이 있으므로 이것을 시행해보자.

이제부터는 알아야할 기본함수인 map(f)가 있다. 이것은 스트림A의 각 값을 받아서 f()를 적용시키고 스트림B로 만든다. 이것을 우리 요청과 응답 스트림에 하려면 요청 URL을 응답 Promise(스트림처럼 만든)에 매핑할 수 있다.
var responseMetastream = requestStream
  .map(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

그다음 우리는 스트림의 스트림인 "metastream"을 하나 만들 것이다. 아직 당황하지 마라. metastream은 각각 발생했던 값이 있는 또다른 스트림이다. 여러분은 이것을 '포인터'라 생각할 수도 있겠다. 각 발생한 값은 다른 스트림을 가리키는 포인터이다. 우리 예제에서는 각 요청 URL이 Promise 스트림을 가리키는 포인터로 매핑되는데, 이 promise 스트림은 해당되는 응답을 가지고 있다.


응답을 위한 한 metastream은 혼란스러워 보이며 우리에게 크게 도움이 되지 않는 것 같다. 우리는 각 발생된 값이 'promise'의 JSON 객체가 아닌 그냥 간단한 응답 스트림이 필요할 뿐이다. Flatmap씨(링크)에게 인사하자. "branch" 스트림에서 나오는 모든 "trunk" 스트림을 내보냄으로서 동작하는, flatmap은 한 metastream을 "flattens"하게 만드는 버전의 map()이다. Flatmap은 "고정"된 것이 아니고 metastream은 일종의 버그가 아니다. 이것들은 Rx에서 비동기로 응답을 다루기위한 실제 툴이다.
var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });


좋다 응답 스트림이 요청 스트림에 맞춰 정의되었으므로, 후에 요청 스트림에서 이벤트가 발생하면 예상한대로 응답 스트림에서 발생한 응답 이벤트들을 가질 수 있을 것이다.

requestStream:  --a-----b--c------------|->
responseStream: -----A--------B-----C---|->

(lowercase is a request, uppercase is its response)

이제 마침내 응답 스트림을 가지게 되었다. 우리가 받은 데이터를 화면에 띄우면 된다.

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});

지금까지의 코드를 모두 합쳐보자.

var requestStream = Rx.Observable.just('https://api.github.com/users');

var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});
새로고침 버튼
나는 아직 응답의 JSON이 100명의 유저라는 것에 대해 말하지 않았다. 이 API는 한 페이지 크기를 설정할 수 있는게 아니라 페이지 번호를 설정할 수 있게 해놓았다. 따라서 우리는 오직 3개의 데이터만 쓰고 나머지 97개의 데이터는 버려진다. 우선은 이 문제를 무시할 것이다. 그리고 나중에 이 응답을 어떻게 캐신할 수 있는지 살펴볼 것이다.

새로고침 버튼을 누를때마다 요청 스트림은 새 URL을 발생시킬 수 있으므로 새 응답을 얻어낼 수 있다. 우리는 다음 두가지가 필요하다. 새로고침 버튼의 클릭 이벤트 스트림(주문: 모든것이 스트림이 될 수 있다). 그리고 새로고침 클릭 스트림에 따라 요청 스트림을 바꿀 수 있는 것도 필요하다. 기꺼히 RxJS는 이벤트 리스너로부터 Observable을 만들 수 있는 툴을 제공한다.
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

새로고침 클릭 이벤트가 그 자신의 API URL을 관리하지 않으므로 우리는 각 클릭을 실제 URL로 매핑해주어야한다. 이제 요청 스트림을 새로고침 클릭 스트림으로 바꾸었다. 새로고침 클릭 스트림은 매번 랜덤의 페이지 값으로 API endpoint를 바꾸어 매핑한다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

내가 좀 어리석고 자동 테스트를 못해서 이전에 만든 기능 중 하나를 가져왔다. 한 요청은 더이상 시작할 때 일어나지 않고 새로고침을 눌렀을때만 일어난다. 아아. 나는 이 요청이 새로고침을 누를때나 사이트를 켰을때나 둘 다 동작하게 하고 싶다. 


우리는 이 상황별 스트림을 어떻게 분리하는지 알고있다.
var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

그러나 이제 '쪼개진' 두 스트림을 어떻게 하나로 합칠까? merge()가 있다. 아래 다이어그램에서 어떻게 되는건지 설명했다.

stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
          vvvvvvvvv merge vvvvvvvvv
          ---a-B---C--e--D--o----->

이제 쉬워졌다.

var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

var requestStream = Rx.Observable.merge(
  requestOnRefreshStream, startupRequestStream
);

매개 스트림 없이 만들 수 있는 깔끔한 방법의 대안이다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .merge(Rx.Observable.just('https://api.github.com/users'));

더 짧아지고 가독성도 더 좋아졌다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .startWith('https://api.github.com/users');

startWith() 함수는 당신이 생각하는데로 정확히 그렇게 동작할 것이다. 입력 스트림이 어덯게 생겼든 상관없이 startWith(x)로부터 나온 결과 스트림은 시작부분에서 x를 가질 것이다. 그러나 나는 아직 DRY 하지 못하다. 나는 API endpoint 문자열을 반복에서 쓰고 있다. 이것을 고치기 위한 한가지 방법은 startWith()refresgClickStream에 붙이는 것이다. 이것은 시작 시점에 새로고침 클릭을 강제로 시행하기 위함이다.

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

좋다. 만약 내가 자동 테스트를 쪼개었다는 곳으로 돌아가보면 이전 것과 다른 점이 startWith()만 추가한 것 밖에 없다.


스트림으로 3가지 제안을 모델링하기
이제부터 터치된 제안 UI 요소만 가지고 있는데, responseStream의 subscribe에서 일어난 렌더링 단계에서 일어났다. 이제 새로고침 버튼에 문제가 있다. '새로고침'을 누르자마자 현재 3개의 제인이 명확하지 않다. 새로운 제안이 응답이 도착한 후에 화면에 나타나는데, UI적으로 좀더 좋게 만들기 위해 새로고침을 누를 때 현재 있던 제안들을 지울 필요가 있다.
refreshClickStream.subscribe(function() {
  // clear the 3 suggestion DOM elements 
});

아니다.. 빠르지 않다. 우리는 제안의 DOM 요소에 영향을 주는 두 구독자가 있기때문에 그렇다. 그리고 이것은 일을 쪼개는 것(sepration of concerns)처럼 보이지도 않는다. 리액티브의 주문이 기억나는가?


이제 제안을 스트림으로 모델링할것인데, 각각 발생한 값은 제안 데이터를 가지고 있는 JSON 객체이다. 우리는 3가지 각 제안마다 이 일을 할것이다. 이것은 1번 제안을 위한 스트림이 어떻게 생겼는지 보여준다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  });

그리고 suggestion2Streamsuggestion3Stream은 그냥 suggestion1Stream을 복사하여 붙여 넣은 것이다. 이것은 DRY는 아니나 튜토리얼의 예제를 간단하게 만들기위해 이렇게 하였고, 추가로 이 경우에는 어떻게 중복을 피할수 있을지 생각해볼수 있는 좋은 기회라 생각된다.

responseStreamsubscribe()에서 렌더링이 일어나게 하는 것 대신에 이렇게 했다.
suggestion1Stream.subscribe(function(suggestion) {
  // render the 1st suggestion to the DOM
});

"새로고침을 누르면 제안들을 지운다"로 돌아가서, 우리는 간단하게 새로고침 클릭을 제안 데이터에 null로 매핑하여 suggestion1Stream에 넣는다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  );

그리고 화면에 띄울때는 null은 따로 분기처리하여 "데이터가 없음"이라하고 그 UI 요소를 숨긴다.

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

여기 큰 그림이다.

refreshClickStream: ----------o--------o---->
     requestStream: -r--------r--------r---->
    responseStream: ----R---------R------R-->   
 suggestion1Stream: ----s-----N---s----N-s-->
 suggestion2Stream: ----q-----N---q----N-q-->
 suggestion3Stream: ----t-----N---t----N-t-->

Nnull을 의미한다.


보너스로, 시작할때 '빈' 제안들이 화면에 나타날 것이다. 이제 제안 스트림에 startWith(null)을 추가하면 된다.
var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

그 결과이다.

refreshClickStream: ----------o---------o---->
     requestStream: -r--------r---------r---->
    responseStream: ----R----------R------R-->   
 suggestion1Stream: -N--s-----N----s----N-s-->
 suggestion2Stream: -N--q-----N----q----N-q-->
 suggestion3Stream: -N--t-----N----t----N-t-->
제안을 닫고 캐싱된 응답을 사용하기
이 기능이 마지막 남은 구현이다. 각 제안은 그것을 닫을 수 있는 'x'버튼을 하나씩 가지고 있고, 그것을 누르면 그 자리에 다른 제안이 들어온다. 닫기 버튼이 눌러지면 새 요청을 만들어야 한다고 생각해볼 수 있다.
var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button

var requestStream = refreshClickStream.startWith('startup click')
  .merge(close1ClickStream) // we added this
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

위의 것은 제대로 동작하지 않는다. 이것은 하나만 눌렀는데 모두 닫으면서 모든 제안을 다시 갱신할 것이다. 이 문제를 해결하기 위한 방법에는 여러개가 있을 것이다. 우리는 이전 응답을 재사용 함으로서 이 문제를 해결할 것이다. API 응답의 페이지 크기는 100명의 사용하지만 우리는 딱 3명만 사용하고 있으므로 남은 데이터를 사용할 수 있을 것이다. 새로운 요청 없이 말이다.


다시 말하자면, 스트림으로 생각해보자. 'close1' 클릭 이벤트가 발생하면 우리는 가장 최근에 responseStream에서 가져올 랜덤 유저 한명을 사용하고 싶다.
    requestStream: --r--------------->
   responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->

Rx*에서는 우리에게 필요한 것 처럼 보이는 결합함수, combineLatest가 있다. 이 함수는 인풋으로 A, B 스트림을 받고, 어떤 스트림이 값을 발생시키든 그때마다 combineLatest는 가장 최근에 두 스트림으로부터 a, b 값을 합친다. 아웃풋 값은 c=f(x,y)인데, f는 여러분이 정의한 함수이다. 다이어그램으로 이해하는게 더 좋을 것이다.

stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
          vvvvvvvv combineLatest(f) vvvvvvv
          ----AB---AC--EC---ED--ID--IQ---->

where f is the uppercase function

우리는 close1ClickStreamresponseStreamcombineLatest()를 적용시킬 수 있으므로, 닫기버튼1이 클릭될 때 마다 마지막에 발생된 응답을 얻어와서 suggestion1Stream에 새 값으로 만든다. 한편 combineLatest()는 대칭적인데, responseStream에서 새 응답이 나올 때 마다 새 제안을 만들어내기위해 마지막 '닫기1' 클릭을 합친다. 이 부분이 재미있는데, 이전에 suggestion1Stream 코드를 간단하게 만들어준다.

var suggestion1Stream = close1ClickStream
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

아직 한 부분이 빠졌다. combineLatest()는 두 소스 중 가장 최신의 것만 사용한다. 그러나 이 소스 중 하나가 아직 아무것도 발생시키지 않았다면, combineLatest()는 아웃풋 스트림에 데이터 이벤트를 만들 수 없을 것이다. 위의 아스키 다이어그램을 보면 첫번째 스트림이 a를 만들었을때 아웃풋에 아무것도 없음을 확인할 수 있을 것이다. 두번째 스트림에서 b 값을 만들었을때 비로소 아웃풋 값이 만들어질 수 있다.


다른 해결 방법이 있는데, 시작할 때 '닫기버튼1'을 모의로 눌러보는 것이다.

var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
  .combineLatest(responseStream,             
    function(click, listUsers) {l
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
합쳐보기
이제 끝났다. 우리가 만든 코드의 완성본이다.
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// and the same logic for close2 and close3

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var responseStream = requestStream
  .flatMap(function (requestUrl) {
    return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
  });

var suggestion1Stream = close1ClickStream.startWith('startup click')
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
// and the same logic for suggestion2Stream and suggestion3Stream

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

http://jsfiddle.net/staltz/8jFJH/48/ 여기서 예제가 돌아가는 것을 확인할 수 있을 것이다.

이 코드 조각은 작지만 빽빽하다. 여기에는 적당히 일을 분배하여 다중 이벤트를 관리하고 응답을 캐싱하는 것까지 기능이 들어있다. 함수형  스타일은 명령형 스타일보다 더 서술적인 코드를 만든다. 우리는 실행되야할 인스트럭션의 순서를 만든게 아니라, 스트림 사이의 관계를 정의함으로서 그냥 어떤것인지 말했을 뿐이다. 예를 들어 if, for, while과같은 컨트롤 플로우 요소들이 없고 자바스크립트에서 주로 쓰는 일반적인 콜백기반 컨트롤 플로우가 없다는 것을 기억에 남기자. 위의 subscribe()에서 당신이 원하면 filter()를 이용하여 ifelse를 없앨수도 있다. (구체적인 구현은 여러분의 연습 과제로 남기겠다) Rx에서는 map, filter, scan, merge, combineLatest, startWith 등과 같이 이벤트 주도 프로그램의 흐름을 컨트롤하는 많은 스트림 함수를 가지고 있다. 이 함수 툴셋은 작은 코드로 더 강한 영향력을 제공할 것이다.

다음으로 해야할 것은 무엇인가
만약 Rx*가 당신의 리액티브 프로그래밍의 주 라이브러리가 된다면, Observable을 변형, 결합, 생성하기위한 많은 함수들을 익힐 필요가 있다. 이 함수들을 스트림 다이어그램으로 이해하고 싶으면 RxJava's very useful documentation with marble diagrams에 들어가 보아라. 당신이 뭔가 시도하다가 문제가 생기면 다이어그램을 그려 생각해보고 그 많은 함수들을 본 다음 다시 생각해 보아라. 내 경험에는 이 방법이 가장 효율적이었다.

한번 Rx와의 프로그래밍을 파악하기 시작하면 Cold vs Hot Observable 개념을 반드시 이해해야한다. 그렇지 않으면 당신을 계속 괴롭힐 것이다. 여러분은 경고받아왔다. 함수형 프로그래밍을 배우고 Rx에 영향을 미치는 사이드 이팩트같은 이슈를 익히면서 당신의 기술을 더 갈고 닦아라.

하지만 리액티브 프로그래밍이 단지 Rx만 있는게 아니다. Bacon.js라는 것이 있는데, 이것은 가끔 Rx에서 마주칠 수 있는 관습(quirks)없이 직관적으로 작업한다. Elm언어는 그 자신만의 카테고리 안에 있다. 이 언어는 자바스크립트+css+HTML로 컴파일하고 타임 트레블링 디버거(time travelling debugger)기능이 있는 함수형 리액티브 프로그래밍 언어이다. 꽤 멋지다.

Rx는 이벤트가 중심인 프론트엔드와 앱에서 잘 동작한다. 그러나 이게 클라이언트단에서만 그런 것이 아니라 백엔드와 데이터베이스 접근에도 잘 동작한다. 사실 RxJava는 Netflix API에서 서버단 동시성을 가능하게 해주는 핵심 요소이다. Rx는 특정 타입의 앱이나 언어에 종속된 프레임워크가 아니다. Rx는 이벤트 주도 소프트웨어를 사용할 수 있게 해주는 패러다임이다.

이 튜토리얼이 마음에 들었다면 트윗해 달라. 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

내 이름은 Eugene Obrezkov이고 오늘 나는 NodeJS라는 무시무시한 플랫폼에대해 이야기 해보려고 한다. "NodeJS는 어떻게 동작하는가?"라는 아주 복잡한 질문에 대한 답을 해나갈 것이다.

나는 마치 NodeJS가 존재하지 않는다고 생각하고 글을 써내려 갈 것이다. 이렇게하면 그 내부가 어떻게 동작하는지 이해하기 쉬워진다.

여기서 사용한 코드는 실제 NodeJS에서 발췌한 코드이며 이 글을 읽고나면 NodeJS에대해 더 편안합을 느낄 수 있을 것이다.


이렇게 하려면 무엇이 필요한가?
위 이야기를 보면 막상 "이렇게 하려면 무엇이 필요한가?"라는 질문이 먼저 떠오를 것이다.

Vyacheslav Egorov는 이렇게 말했다: "JS VM은 자바스크립트 소스를 0과 1로 만드는 미지의 블랙박스라 생각하고, 더 많은 사람들이 그것을 그만 들여다본다." 이 발상은 NodeJS에도 적용시켜보았다. "NodeJS는 저수준 API를 돌아가게 해주는 미지의 블랙박스라 생각하고, 더 많은 사람들이 그것을 그만 들여다본다."

그냥 해보자!
2009년으로 돌아가면 NodeJS가 만들어지기 시작하는 시점이다.

우리는 백엔드에서 자바스크립트를 실행시키고 싶었고, 자바스크립트가 저수준 API에 접근하고 싶었다. 또한 우리의 자바스크립트를 CLI와 REPL로부터 실행시키고 싶었다. 사실 자바스크립트로 뭐든 다 하고싶었던 것이다!

이게 어떻게 가능할까? 내 머릿속에 처음 드는 생각은 브라우저였다.

브라우저
브라우저는 자바스크립트를 실행시킬 수 있다. 따라서 브라우저를 우리 앱에 합쳐 자바스크립트를 동작시키게 할까?

설마! 아래에 반드시 답해야할 질문들을 보자.

브라우저가 저수준 API를 자바스크립트로 만들까? — 아니다!
다른 곳에서 자바스크립트를 돌릴 수 있을까? — 반반이다. 좀 더 복잡한 이야기이다.
브라우저가 제공하는 모든 DOM 기능이 필요할까? — 아니다!
브라우저가 조금이라도 필요할까? — 아니다!

브라우저는 필요 없다. 자바스크립트는 브라우저 없이 실행된다.

그럼 브라우저가 자바스크립트를 실행시키는게 아니라면 무엇이 자바스크립트를 실행할까?

가상머신(Virtual Machine-VM)
가상머신(VM)이 자바스크립트를 실행한다!

VM은 고수준 프로그래밍 언어라는 추상화를 제공한다.(시스템의 저수분 ISA 추상화와 비교해보아라)

VM은 추상화이고 플랫폼에 독립적인 프로그램 실행환경을 만들기 위해 단일 컴퓨터에서 동작하도록 설계되었다.

가상머신에는 Google의 V8, 마이크로소프트의 Chakra, 모질라의 SpiderMonkey, Apple의 JavaScriptCore 등 수많은 것들이 있다. 여기서 신중하게 고르지 않으면 남은 인생동안 분명 후회하며 살 것이다.

나는 V8을 선택하려 한다. 왜일까? 그 이유는 다른 VM보다 더 빠르기 때문이다. 당신도 아마 백엔드에서 실행속도는 중요하다는 것에 동의할 것이다.

이제 V8을 살펴보면서 이것이 NodeJS 구성에 어떤 도움을 줄 수 있는지 살펴보자.

V8 VM
V8은 어떤 C++ 프로젝트와도 합칠 수 있다. 단지 간단한 라이브러리처럼 V8 소스를 거기에 인클루드(include)시키면 된다. 이렇게만 하면 이제 자바스크립트 코드를 컴파일하고 실행할 수 있다.

V8은 C++을 자바스크립트에서 사용할 수 있게 해준다. 자바스크립트로 저수준 API를 사용할 수 있게 해주는 중요한 역할을 한다.

위 두가지 포인트가 "어떻게 자바스크립트로 저수준 API에 접근할 수 있는지"에대한 대략적인 구현을 생각해볼 수 있을것이다.

다음 챕터부터는 C++ 코드로 설명을 시작할 것이기 때문에 위의 것들을 한데모아 생각해보자. 가상머신을 선택하고(우리의 경우 V8을 선택했다) -> 우리 C++ 프로젝트에 그것을 통합시킨뒤 -> V8이 C++을 자바스크립트로 사용할 수 있게 한다.

그러나 어떻게 C++코드로 작성하고 그것이 자바스크립트에서 가능할까?

V8 템플릿
V8 템플릿들을 통해 가능하다!

한 템플릿은 자바스크립트에서의 함수와 객체를 위한 청사진이다. C++ 함수와 데이터 구조체를 자바스크립트로 감싸기 위해 템플릿을 사용한다.

예를 들어보자. 구글 크롬은 C++ DOM 노드를 자바스크립트 객체로 감싸고 전역에 함수를 만들어 두기 위해 템플릿을 사용한다.

여러분도 템플릿들을 만들어 그것을 사용할 수 있다. 따라서 당신이 원하는 만큼 템플릿을 만들면 된다.

그리고 V8은 두가지 타입의 템플릿을 가지고 있다: 함수 템플릿(Function Templates)객체 템플릿(Object Templates)이다.

함수 템플릿은 한 함수의 청사진이다. 당신이 자바스크립트 함수로 만들고 싶은 컨텍스트에서 템플릿의 GetFunction 메소드를 호출하여 자바스크립트 인스턴스의 템플릿을 만들어 낼 수 있다. 자바스크립트 함수 인스턴스가 호출될 때 함수 템플릿이 호출되는데, 이 함수 템플릿과 C++ 콜백을 연관시킬 수도 있다.

객체 템플릿은 객체 초기화에서 함수 템플릿과 함께 만들어진 객체를 구성하는데 익숙하다. 객체 템플릿은 두가지 C++콜백 타입과 연관이 가능하다: 접근자 콜백(accessor callback)인터셉터 콜백(intercepter callback). 접근자 콜백은 특정 객체 프로퍼티가 스크립트에의해 접근될 때 호출된다. 인터럽트 콜백은 어떤 객체 프로퍼티라도 스크립트가 접근하면 호출된다. 즉 C++ 객체/구조체를 자바스크립트 객체로 감쌀 수 있다.

간단한 예제를 살펴보자. 이것은 C++ 메소드인 LogCallback을 전역 자바스크립트 컨텍스트에서 사용하는 것이다.

두번째 라인에서 새 ObjectTemplate를 생성한다. 그리고 3번째 라인에 FunctionTemplate를 생성하고 그것을 LogCallback이라는 C++ 메소드와 연관시킨다. 그 다음 이 FunctionTemplateObjectTemplate에 세팅한다. 새 컨텍스트에서 자바스크립트를 실행시킬때 전역에서 log 메소드를 사용하도록 이 새로운 컨텍스트에 ObjectTemplate 인스턴스를 보낸다. 결과적으로 FunctionTemplate 인스턴스와 연관된 LogCallback C++ 메소드가 트리거 될 것이다.(As a result, C++ method, associated with our FunctionTemplate instance, LogCallback, will be triggered.)

여러분도 보았듯 C++에서는 자바스크립트에서 객체를 정의하는 것과 비슷하다.

그러나 이제 C++ 메소드를 어떻게 자바스크립트에 드러낼 것인지 배울 것이다. 방금처럼 수정된 컨텍스트에서 어떻게 자바스크립트 코드를 실행시키는지 살펴볼 것이다. 사실 단순하다. 그냥 요소를 컴파일하고 실행시키자.

V8 컴파일 && 자바스크립트 실행
우리가 만든 컨텍스트에서 자바스크립트를 실행하려면 V8에 컴파일(compile)과 실행(run)의 간단한 API만 호출하면 된다.

새 컨텍스트를 만들면서 자바스크립트를 실행시키는 이 예제를 살펴보자.

2번째줄을 보면 새로운 자바스크립트 컨텍스트를 만들었다(위에서 설명한 템플릿들과 함께 수정할 수 있다). 5번째줄은 자바스크립트 코드를 컴파일하고 실행하기 위해 컨텍스트를 활성화 시킨다. 8번째줄은 자바스크립트 소스로부터 새 문자열을 생성한다. 위처럼 하드코딩 할 수도 있고 파일을 읽거나 다른 방법으로도 가능하다. 11번째줄은 우리의 자바스크립트 소스를 컴파일한다. 14번째줄은 실제 실행시켜보고 결과를 기다린다. 이게 다다.

마침내 위에서 말한 테크닉들을 조합하여 우리는 간단한 NodeJS를 만들 수 있다.

C++ -> V8템플릿 -> 자바스크립트를 실행 -> ?
V8 머신을 만든다 -> 원하는 만큼 C++ 콜백과 연관된 FunctionTemplate를 만든다 -> ObjectTemplate 인스턴스를 만들고 그것에 만들어놓은 FunctionTemplate 인스턴스를 할당한다 -> 우리의 ObjectTemplate 인스턴스를 전역 객체로 한 자바스크립트 컨텍스트를 만든다 -> 이 컨텍스트에서 자바스크립트를 실행한다 -> NodeJS. 여기까지이다!

그러나 챕터 타이틀에 물음표부분의 "자바스크립트를 실행" 이후에는 무엇이 올까? 위 구현에는 약간의 문제가 있다. 굉장히 중요한 점을 하나 지나쳤다.

fs, http, crypto등과 함께 동작하는 수많은 C++ 메소드(약 10K SLOC)를 작성했다고 생각해보자. 우리는 [C++ 콜백들]을 ObjectTemplate에 할당하고 [FunctionTemplate]를 ObjectTemplate에 불러온다. 이 ObjectTemplate 자바스크립트 인스턴스를 쓰면 자바스크립트 전역에서 모든 FunctionTemplate 인스턴스에 접근할 수 있게 된다. 그리고 모든 것이 잘 동작하는 것처럼 보일 것이나..

당장에 fs이 필요없다면? crypto의 모든 기능이 필요 없다면? 전역에 모듈을 죄다 불러오지 않고 그들이 요구하는 것만 불러준다면 어떨까? 모든 C++ 콜백이 한 파일에 긴 C++ 코드를 작성하지 않는 것은 어떨까? 그래서 위 타이틀에서 "?"의 의미는..

모듈화이다!

각 C++ 모듈이 각 fs, http 나 다른 기능에 일치하기 때문에 이 모든 C++ 메소드는 모듈로 쪼개지고 각 다른 파일에 위치한다.(이 점이 개발을 쉽게 만들어준다) 자바스크립트 컨텍스트도 같은 로직이다. 전역에서 반드시 모든 자바스크립트 모듈에 접근할 수 있는게 아니라 요구에 따라 접근이 가능하다.

위의 방법을 기반으로 우리만의 모듈로더(module loader)를 구현해야 한다. C++ 코드에서 요구하는 데로 모듈을 가져오고 자바스크립트 컨텍스트도 마찬가지로 그러기 위해 모듈로더는 C++ 모듈과 자바스크립트 모듈을 불러오는 역할을 한다.

먼저 C++ 모듈부터 시작해보자.

C++ 모듈로더
이제 좀 많은 C++코드가 있을텐데, 마음 단단히 먹고 시작해보자 :)

모든 모듈로더의 기초부터 이야기 해보자면, 각 모듈 로더는 모든 모듈(혹은 그것을 어떻게 얻어내는지에대한 정보)을 담고 있는 변수를 반드시 가지고 있어야한다. C++ 모듈에 대한 정보를 가지고 있는 C++ 구조체를 정의하고 이 이름을 node_module이라 하자.

우리는 이 구조체 안에 현재 존재하는 모듈에 대한 정보를 담았다. 결과적으로 사용가능한 모든 C++ 모듈의 딕셔너리를 가진다.

위 구조체의 각 필드를 다 설명하진 않겠지만, 그중 몇개만 이야기 하고 싶다. nm_file에서는 어디서부터 그 모듈을 불러오는지 파일 이름을 저장하고 있다. nm_register_func와 nm_context_register_func에는 모듈이 필요할 때 호출하는 함수들을 담고있다. 이 함수들은 Template 인스턴스로 만들어질 것이다. 그리고 nm_modulename은 모듈 이름을 저장한다 (파일 이름이 아니다) .

다음으로 이 구조체를 도와주는 helper 메소드들이 필요하다. 우리 node_module 구조체에 정보를 저장할 수 있는 간단한 메소드를 만든 뒤 우리의 모듈 정의에서 이 메소드를 사용할 수 있다. 이것을 node_module_register라 부르자.

위에서 보이듯 여기서 하는 일이라곤 모듈 정보를 node_module 구조체에 저장하는 일 뿐이다.

이제 메크로를 이용해 이 과정을 간단하게 만들 수 있다. 당신의 C++ 모듈 안에 메크로를 정의하자. 이 메크로는 단지 node_module_register 메소드를 감싸는 용도이다.

첫번째 메크로는 node_module_register 메소드를 감싸는 메크로이다. 다른 하나는 첫번째 메크로에 미리 정의된 인자를 박아둔 메크로이다. 결론적으로 modnameregfunc라는 두 인자를 받는 메크로가 완성되었다. 이 메크로를 호출하면 새 모듈 정보를 우리의 node_module 구조체에 저장한다. 그럼 modnameregfunc는 무엇일까? 음.. modnamefs처럼 모듈의 이름을 뜻한다. regfunc는 이전에도 우리가 이야기한 모듈 메소드이다. 이 메소드는 V8 Template 초기화와 ObjectTemplate에 이것을 할당하는 역할을 한다.

앞에서 보았듯 각각의 C++ 모듈은 모듈 이름(modname)과 초기화 함수(regfunc)를 인자로 받는 메크로로 정의할 수 있는데, 이 메크로는 모듈이 필요할 때 호출되는 것이다. 우리에게 필요한 것은 단지 node_module  구조체로부터 정보를 읽고 regfunc 메소드를 호출할 수 있는 C++ 메소드를 만드는 것이다.

node_module 구조체에서 이름으로 모듈을 검색할 수 있는 간단한 메소드를 만들어보자. 이 메소드를 get_builtin_module이라 부를 것이다.

이 메소드는 nm_modnamenode_module 구조체의 이름이 일치하면 앞서 정의된 모듈을 반환한다.

node_module 구조체의 정보를 기반으로 C++ 모듈을 불러오고 우리의 ObjectTemplate에 V8 Template 인스턴스를 할당하는 간단한 메소드를 만들 수 있다. 그러면 이 ObjectTemplate는 자바스크립트 인스턴스에 따라 자바스크립트 컨텍스트에 보내질 것이다.

위 코드에 관해 몇가지 짚고 넘어가자면, Binding은 모듈 이름을 인자로 받는다. 이 인자는 당신이 메크로를 통해 주는 모듈 이름이다. 그리고 우리는 get_builtin_module 메소드에서 나온 이 모듈에 관한 정보를 찾아온다. 만약 찾으면 exports와 같은 유용한 인자들을 보내면서 이 모듈의 초기화 함수를 호출한다. exportsObjectTemplate 인스턴스이므로 exports에서 V8 Template API를 사용할 수 있다. 이 모든 동작이 끝나고 Binding ㅁ[소드의 결과물로 나온 exports 객체를 받는다. 여러분이 기억하는데로 ObjectTemplate 인스턴스는 자바스크립트 인스턴스와 Binding이 한 것을 반환할 수 있다.

마지막으로 해야할 것은 이 메소드를 자바스크립트 컨텍스트에서 사용할 수 있게 하는 것이다. 이것은 마지막 라인이 하는 일인데, FunctionTemplate에서 Binding 메소드를 감싸고 전역변수 process에 할당하는 일을 한다.

이 단계에서 process.binding('fs')를 호출하여 네이티브 바인딩을 할 수 있다.

단순함을 위해 로직을 뺀, 내장된 모듈의 예시이다.


위 코드는 process.binding('V8')를 호출하여 자바스크립트 컨텍스트로부터 이 자바스크립트 객체를 얻어내려고 자바스크립트 객체를 내보내는 "V8"이라는 바인딩을 만든다.

고맙게도 여러분들은 아직 잘 따라오고 있다.

이제 우리는 require('fs')와같이 깔끔하게 되도록 도와주는 자바스크립트 모듈 로더를 만들 것이다.

자바스크립트 모듈로더
좋다. 마지막 개선(improvements)에 감사하다. 우리는 process.binding()을 호출하고 자바스크립트 컨텍스트로부터 C++ 바인딩에 접근할 수 있게 되었다. 그러나 아직 자바스크립트 모듈에 관한 이슈는 해결된 바가 없다. 어떻게 자바스크립트 모듈을 작성하고 필요할 때 그것을 require 할 수 있을까?

먼저 모듈에는 두가지 타입이 있음을 이해해야한다. 그 중 하나는 C++ 콜백과 함께 작성된 자바스크립트 모듈이고 NodeJS에 내장된 fs, http 등과 같은 모듈들이다. 이 모듈을 NativeModule이라 부르자. 다른 모듈은 여러분이 작업하는 디렉토리 안에 있는 모듈이다. 이것을 그냥 Module이라 하자.

우리는 두가지 타입 모두 require 할 수 있어야한다. 이 말은 NodeJS로부터 NativeModule을 부르는 법과 작업 디렉토리에서 Module을 부르는 법을 알아야한다는 뜻이다.

NativeModule부터 먼저 이야기해보자.

자바스크립트에 있는 모든 NativeModule은 C++ 프로젝트로 다른 폴더에 위치한다. 이 말은 모든 자바스크립트 소스가 컴파일 시간을 가진다는 뜻이다. 이렇게하여 우리가 나중에 사용할 자바스크립트 소스를 C++ 헤더파일에 감싸 넣을 수 있게 한다.

이것을 위한 js2c.py(tools/js2c.py에 위치한)이라는 파이썬 툴이 있다. 이것은 자바스크립트 코드로 감싸진 node_natives.h 헤더파일을 생성한다. node_natives.h는 C++에서 자바스크립트 소스를 얻어내는 어떠한 C++ 코드에서도 포함될 수 있다.

이제 C++ 컨텍스트에서 자바스크립트 소스를 사용할 수 있다. — 한번 시도해보자. node_natives.h에서 자바스크립트 소스를 가져오고 그것을 ObjectTemplate 인스턴스에 할당하는 DefineJavaScript 메소드를 구현했다.

위 코드에서, 우리는 각 네이티브 자바스크립트 모듈들을 통해 돌면서 ObjectTemplate 인스턴스에 키로서 모듈이름을, 값으로서 모듈 자체를 넣었다. 마지막으로 우리가 할 일은 타겟으로 ObjectTemplate 인스턴스와 함께 DefineJavaScript를 호출하는 것이다.

이때 Binding 메소드가 유용하다. C++의 Binding 구현(C++ 모듈로더 부분)을 보면 하드코딩된 constantsnatives라는 두 바인딩이 있을 것이다. 그러므로 바인딩 이름이 natives이면 environmentexports 객체가 DefineJavaScript 메소드와 함께 호출될 것이다. 결과적으로 자바스크립트 NativeModuleprocess.binding('natives')가 호출될 때 반환될 것이다.

그래 좋다. 그러나 node.gyp 파일에서 GYP 작업을 정의하고 이것으로부터 js2c.py 툴을 호출하여 또다른 개선을 할 수 있다. 이것은 NodeJS가 컴파일 될 때 자바스크립트  소스가 node_natives.h 헤더파일로 감싸지기 때문에 이 개선을 만들 것이다.

이제부터 우리는 process.binding('natives')로 사용할 수 있는 NativeModule의 자바스크립트 소스들을 가지고 있을 수 있다. 이제 NativeModule을 위한 간단한 자바스크립트 껍데기를 만들어보자.

이제 모듈을 불러오기 위해 NativeModule.require() 안에 모듈 이름을 넣어 호출한다. 먼저 모듈이 이미 캐시에 있는지 확인한다. 있으면 캐시에서 꺼네오고 그렇지 않으면 모듈을 컴파일하고 캐시에 넣은 뒤 exports 객체로 반환한다.

이제 좀 더 아까이서 cachecompile 메소드를 살펴보자.

cache가 하는 일은 NativeModule에 위치한 스태틱 객체 _cacheNativeModule 인스턴스를 세팅한다.

compile 메소드가 더 흥미롭다. 먼저 (process.bind('natives')로 세팅해둔 이 스태틱 프로퍼티)_source에서 필요한 모듈의 소스를 꺼낸다. 그리고 wrap 메소드로 그 소스를 감싼다. 위 소스에서 볼 수 있듯 함수의 결과물은 exports, require, module, __filename, __dirname 인자를 받는다. 그 후 필요한 인자와 함께 이것을 호출한다. 결과적으로 NativeModule.exports를 가리키는 exports, NativeModule.require를 가리키는 requireNativeModule 그 자체를 가리키는 module, 현재 파일 이름의 문자열인 __filename을 가지는 영역에서 우리 자바스크립트 모듈이 감싸진다. 이제 여러분은 module이나 require이 자바스크립트 코드 어디에서부터 오는지 안다. 그것들은 단지 NativeModule 인스턴스를 가리키고 있다. 😃

다른 하나는 Module 로더 구현이다.

Module 로더의 구현은 기본적으로 NativeModule과 같다. 그러나 다른 점은 소스가 node_natives.h 헤더파일로부터 오는 것이 아니라 우리가 fs 네이티브 모듈이라 부르는 파일로부터 온다. 따라서 wrap, cache, compile 하는 일은 같지만 파일로부터 읽는 소스만 다르다.

좋다, 이제 우리는 어떻게 네이티브 모듈이나 여러분의 작업 디렉토리로부터 모듈을 요청하는지 안다.

마지막으로, 우리는 위의 일들을 사용하여 만든 NodeJS 환경을 실행(run)하고 준비(prepare)할 때 마다 실행되는 간단한 자바스크립트 모듈을 작성할 수 있다.

NodeJS 런타임 라이브러리?
런타임 라이브러리가 무엇일까? 이 라이브러리는 전역변수인 process, console, Buffer등을 세팅하여 코딩할 수 있는 환경을 갖추고, NodeJS CLI에 인자로 보내는 메인 스크립트를 실행한다. 이 라이브러리는 NodeJS 런타임시 모든 자바스크립트 코드가 실행되기 전에 실행되는 간단한 자바스크립트 파일로 아키브(achieve)될 수 있다.

첫번째 단계로는 전역에 모든 네이티브 모듈을 프록싱(proxying)하고 다른 전역 변수를 설정하는 것이다. 이 일은 단지 global.Buffer = NativeModule.require('buffer')global.process = process와 같은 일들이다.

두번째 단계는 NodeJS CLI에 인자로 보내는 메인 스크립트를 실행한다. 로직은 간단하다. process.argv[1]를 파싱하여 그 값을 객체 초기화때 값으로 Module 인스턴스를 생성한다. 따라서 Module은 파일로부터 소스를 읽을 수 있고 -> NativeModule이 했던 것처럼 미리 컴파일된 자바스크립트 소스로 캐시와 컴파일 해 둘 수 있다.

여기에 내가 더 추가할 수 있는게 없다. 굉장히 매우 간단하며, 만약 그대로 더 세부적인 것을 원한다면 노드 저장소의 src/node.js 파일을 한번 확인해보아라. 이 파일은 NodeJS 런타임시 실행되고 이 글에서 말한 모든 테크닉을 사용한다.

이것이 바로 NodeJS가 어떻게 당신의 자바스크립트 코드에서 저수분 API에 접근하며 실행하는지에관한 이야기였다. 멋지지 않는가?

하지만 아직 비동기 처리에대한 문제가 남았다. 여기서는 fs.readFile()같은 연산은 완전히 순차적으로 실행된다.

비동기 처리를 위해 어떤 것이 필요할까? 이벤트 루프이다.

이벤트 루프
이벤트 루프는 프로그램 내에서 이벤트나 메시지를 기다리고 디스패치(dispatch)하는 메시지 디스패처이다. 이것은 내부/외부 이벤트 프로바이더(보통 이벤트가 도착하기 전까지 요청을 블락시키는 것)에 요청을 만듦으로서 작업한다. 그리고 적절한 이벤트 핸들러를 호출한다(이벤트를 디스패처 한다). 선택되거나 채택될 수 있는 파일 인터페이스를 따르는 이벤트 프로바이더라면, 이벤트 루프는 리엑터(reactor)와 협력하여 사용할 수 있다. 이벤트 루프는 항상 메시지 제공자와 함께 비동기로 처리한다.

V8 환경을 생성하면 V8은 인자로 이벤트 루프를 받을 수 있다. 그러나 V8에 이벤트 루프를 세팅하기 전에 먼저 그것을 구현해놓아야한다.

이제 libuv라 불리는 그 구현을 이미 가지고 있다치자. libuv는 파일 읽기와 같은 모든 비동기 처리의 책임을 가지고 있다. libuv가 없는 NodeJS는 단지 순차적으로 자바스크립트/C++를 실행할 수 있는 도구일 뿐이다.

따라서 기본적으로 NodeJS에 libuv 소스를 인클루드 할 수 있고, 거기에 있는 libuv 기본 이벤트 루프와 함께 V8 환경을 만들 수 있다. 여기에 그 구현이 있다.

CreateEnviroment 메소드는 루프 인자로 libuv 이벤트 루프를 받는다. 우리는 V8 네임스페이스에서 Enviroment::New를 호출할 수 있고 libuv 이벤트 루프를 보낸 다음 V8 환경으로 구성할 수 있다. 여기까지가 NodeJS를 어떻게 비동기 처리 할 수 있는지에 관한 이야기였다.

libuv에대해 더 이야기하고 어떻게 동작하는지 말해주고 싶지만, 이 이야기는 다음으로 미루도록 하자. :)

Thanks!
이 글을 끝까지 읽어준 모든이에게 감사하다. 여러분이 여기까지 읽으면서 뭔가 즐겁게 배웠기를 바란다. 만약 뭔가 문제를 발견한다면 자유롭게 이 글에 커멘트해주면 된다. 그러면 가능한 빨리 내가 답변해주도록 하겠다.(옮긴이: 자유롭게 이 블로그에 댓글 달아주시면 됩니다!)

Eugene Obrezkov aka ghaiklor, Technical Leader at Onix-Systems, Kirovohrad, Ukraine.


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret