ReScript in Korean

모듈

원문

개요

모듈은 작은 파일들 같은 것입니다! 타입 정의와 let 바인딩, 자식 모듈 포함 등을 할 수 있습니다.

모듈 만들기

모듈을 만들려면 module 키워드를 사용합니다. 모듈 이름은 반드시 대문자로 시작해야 합니다. .res 파일 안에 넣을 수 있는 모든 것을 모듈을 정의하는 {} 블록에 넣을 수 있습니다.

module School = {
type profession = Teacher | Director
let person1 = Teacher
let getProfession = (person) =>
switch person {
| Teacher => "A teacher"
| Director => "A director"
}
}
function getProfession(person) {
if (person) {
return 'A director';
} else {
return 'A teacher';
}
}
var School = {
person1: /* Teacher */ 0,
getProfession: getProfession,
};

모듈의 내용은 타입을 포함해 레코드와 동일한 표현인 . 표현을 사용해 접근할 수 있습니다. 다음은 모듈에 접근하는 예제입니다. 네임스페이스처럼 사용할 수 있습니다.

let anotherPerson: School.profession = School.Teacher
Js.log(School.getProfession(anotherPerson)) /* "A teacher" */
var anotherPerson = /* Teacher */ 0;
console.log('A teacher');

모듈 안에 모듈을 포함해도 마찬가지로 작동합니다.

module MyModule = {
module NestedModule = {
let message = "hello"
}
}
let message = MyModule.NestedModule.message
var NestedModule = {
message: message,
};
var MyModule = {
NestedModule: NestedModule,
};
var message = MyModule.NestedModule.message;

모듈 열기

모듈 안에 있는 타입 또는 값을 사용할 때마다 모듈 이름을 명시하는 것은 매우 귀찮습니다. 그래서 모듈을 열면("open") 포함하는 모듈 이름을 쓰지 않고 내용만 쓸 수 있습니다. 아래 처럼 사용하기 보다는,

let p = School.getProfession(School.person1)
var p = School.getProfession(School.person1);

이렇게 쓸 수 있습니다.

open School
let p = getProfession(person1)
var p = School.getProfession(School.person1);

School 모듈의 내용은 스코프 내에서 접근(파일로부터 복사하는 것이 아니고 단순히 보이게 합니다!)할 수 있습니다. 그러므로 profession, getProfession 그리고 person1은 올바르게 접근 가능합니다.

open을 쓰면 편리하지만, 값들이 어디서 왔는지 알기 힘들 수 있습니다. 따라서 open은 주로 지역 범위에서만 써야합니다.

let p = {
open School
getProfession(person1)
}
/* School 모듈의 내용은 더 이상 여기에 보이지 않습니다.*/
var p = School.getProfession(School.person1);

모듈 확장하기

모듈에서 include 키워드를 사용하면 모듈의 모든 내용을 새 모듈에 정적으로 "확산"합니다. 상속이나 믹스인에 가깝습니다. 주의: include는 컴파일러 레벨에서 복사-붙여넣기와 같습니다. 되도록 사용하지 마시고 최후의 수단으로만 써야 합니다!

module BaseComponent = {
let defaultGreeting = "Hello"
let getAudience = (~excited) => excited ? "world!" : "world"
}
module ActualComponent = {
/* 내용을 복사합니다. */
include BaseComponent
/* BaseComponent.defaultGreeting을 덮어씌웁니다. */
let defaultGreeting = "Hey"
let render = () => defaultGreeting ++ " " ++ getAudience(~excited=true)
}
function getAudience(excited) {
if (excited) {
return 'world!';
} else {
return 'world';
}
}
var BaseComponent = {
defaultGreeting: 'Hello',
getAudience: getAudience,
};
var defaultGreeting = 'Hey';
function render(param) {
return 'Hey world!';
}
var ActualComponent = {
getAudience: getAudience,
defaultGreeting: defaultGreeting,
render: render,
};

주의: openinclude는 매우 다릅니다! 전자는 매번 모듈 이름을 함께 언급할 필요가 없도록 모듈 내용을 현재 스코프로 가져옵니다. 반면 후자는 모듈을 정적으로 복사해 현재 모듈에 포함합니다.

모든 .res 파일은 모듈

모든 리스크립트 파일은 파일 이름이 모듈 이름으로, 파일 내용은 모듈 내용으로 컴파일 됩니다. 파일/모듈명은 대문자로 시작합니다. React.res는 암시적으로 모듈 React를 구성하며 이 이름으로 다른 소스 파일에서 사용할 수 있습니다.

주의: 리스크립트 파일은 컨벤션에 의해 모듈 이름과 파일 이름의 대/소문자가 일치해야 합니다. 그래서 소문자로 시작하는 파일 이름은 모듈 이름으로 유효하지 않기 때문에, 암묵적으로 대문자로 시작하는 모듈 이름으로 바뀝니다. 예를 들면, file.resFile 모듈로 변환됩니다. 일관성을 유지하기 위해, 파일 이름의 첫 글자를 대문자로 합니다.

시그니쳐

모듈 타입은 시그니쳐(Signature)로 불립니다, 그리고 명시적으로 쓰입니다. 모듈 구현이 확장자가 .res인 파일에 정의되어 있다면, 모듈 타입은 .resi 파일에 정의합니다.

시그니쳐 만들기

시그니쳐를 만들기 위해서는 module type 키워드를 사용해야 합니다. 시그니쳐 이름은 반드시 대문자로 시작해야 합니다. .resi 파일에 내용을 작성 했다면 파일 내용은 시그니쳐 정의 안 {} 블록에 작성한 것과 동일합니다.

/* 이전 목차에서 가져왔습니다. */
module type EstablishmentType = {
type profession
let getProfession: profession => string
}
// 빈 출력입니다.

시그니쳐에서 정의하는 것들은 모듈이 시그니쳐에 매칭이 되기 위해서 만족해야 할 요구사항들입니다. 각 요구사항들은 다음과 같은 형태를 띄고 있습니다.

  • let x: intxint 타입으로 let 바인딩 되어야 합니다.
  • type t = someType 은 타입 필드 tsomeType이여야 합니다.
  • type t 는 타입 필드 t를 요구합니다만, 타입 t가 특정 타입이라고 명시적으로 요구하지 않습니다. t는 관계를 설명하기 위해 시그니쳐의 다른 항목에서 사용합니다. 예를 들어, let makePair: t => (t, t) 같은 시그니처에서 tint 타입으로 가정하지 않습니다. 이렇게 추상화를 강제할 수 있습니다.

다양한 타입을 고려해 작성한 EstablishmentType 모듈 타입의 요구사항을 확인해보면,

  • profession 타입을 정의합니다.
  • profession 타입인 값을 인자로 받고 string을 반환하는 함수를 포함합니다.

참고:

EstablishmentType 모듈 타입은 이전 섹션에서 다루었던 School 모듈 같이 시그니쳐가 정의한 것보다 더 많은 필드를 포함할 수 있습니다.(리스크립트가 EstablishmentType 타입으로 할당할지 선택합니다. 선택하지 않았다면 School은 모든 필드를 노출합니다.) 모듈 타입이 있어 person1 필드만 효율적으로 구현합니다! 그리고 person1 필드는 시그니쳐에 없기 때문에 외부에서 접근할 수 없습니다. 시그니쳐는 다른데서 접근하는 것을 제한합니다.

EstablishmentType.profession 타입은 구체적인 타입이 아니라 추상화된 타입입니다. 이는 "실제 타입이 무엇이든 신경 쓰지 않겠으나, getProfession에 들어갈 입력의 타입으로 쓰인다." 와 같은 말입니다. 이는 같은 인터페이스를 공유하는 많은 모듈을 맞추는 데 유용합니다.

module Company: EstablishmentType = {
type profession = CEO | Designer | Engineer | ...
let getProfession = (person) => ...
let person1 = ...
let person2 = ...
}
function getProfession(person) {
...
}
var person1 = ...
var person2 = ...
var Company = {
getProfession: getProfession,
person1: person1,
person2: person2
};

다른 사람이 의존하지 못하도록 구현 타입을 숨기는 데 유용합니다. Company.profession의 타입이 무엇이냐고 묻는다면, 이 타입은 배리언트라고 노출하지 않고 Company.profession이라고만 알려줄 것입니다.

모듈 시그니처 확장

모듈과 마찬가지로, 모듈 시그니처는 include를 사용해 다른 모듈 시그니쳐를 확장할 수 있습니다. 다시 한번 이야기하지만, include는 쓰지 않는 것이 좋습니다.

module type BaseComponent = {
let defaultGreeting: string
let getAudience: (~excited: bool) => string
}
module type ActualComponent = {
/* BaseComponent 시그니쳐를 복사합니다. */
include BaseComponent
let render: unit => string
}
// 빈 출력입니다.

주의: BaseComponent는 모듈이 아니라 모듈 타입입니다.

만약 모듈 타입 정의를 모른다면, include(module type of "실제 모듈 이름")을 사용해 모듈로부터 모듈 타입 정의를 추출할 수 있습니다. 예를 들어 다음 코드조각과 같이 모듈 타입 정의를 사용하지 않고 표준 라이브러리에 있는 List 모듈 타입을 확장 할 수 있습니다.

module type MyList = {
include (module type of List)
let myListFun: list<'a> => list<'a>
}
// 빈 출력입니다.

모든 .resi 파일은 시그니쳐

React.res 파일이 React 모듈에 암묵적으로 정의되듯이, React.resi 는 암묵적으로 React 시그니쳐를 정의합니다. 구현 파일이 포함되어 있지 않기 때문에, 생태계에서는 .resi 파일을 모듈의 공용 API 문서처럼 사용합니다. React.resi를 제공하지 않으면, React.res의 시그니쳐는 기본적으로 모듈의 모든 필드를 노출합니다.

/* React.res 파일입니다. (아래 구현은 React 모듈로 컴파일합니다.) */
type state = int
let render = (str) => str
function render(str) {
return str;
}
/* React.resi 파일입니다. (React.res의 시그니쳐로 컴파일되는 인터페이스입니다.) */
type state = int
let render: string => string

펑터(모듈 함수)

모듈은 함수로 넘길 수 있습니다. 파일을 일급 객체로 전달하는 것과 동등합니다. 그러나 모듈은 다른 일반적인 컨셉들과는 언어 레이어에서 다르므로 일반 함수로 전달하지는 못합니다. 대신, 펑터라는 특별한 함수들을 전달합니다.

펑터를 사용하고 정의하는 문법은 일반적인 함수들을 사용하고 정의하는 것과 비슷합니다. 주요 차이점은 다음과 같습니다.

  • 펑터는 let 대신 module 키워드를 사용합니다.
  • 펑터는 모듈을 인자값으로 받고 모듈을 반환합니다.
  • 펑터는 반드시 인자를 어노테이션 해야합니다.
  • 펑터는 대문자로 시작해야 합니다.(모듈과 시그니쳐처럼요.)

여기 MakeSet 펑터 예제가 있습니다. 여기서는 Comparable 타입의 모듈을 인자로 받고, 비교 가능한 아이템들을 포함할 수 있는 새로운 집합을 반환합니다.

module type Comparable = {
type t
let equal: (t, t) => bool
}
module MakeSet = (Item: Comparable) => {
/* 리스트를 자료 구조로 사용합니다. */
type backingType = list<Item.t>
let empty = list{}
let add = (currentSet: backingType, newItem: Item.t): backingType =>
// 아이템이 존재한다면,
if List.exists(x => Item.equal(x, newItem), currentSet) {
currentSet /* 동일한 불변 집합(리스트 타입)을 반환합니다. */
} else {
list{
newItem,
...currentSet // 집합 앞에 추가하여 반환합니다.
}
}
}
var List = require('./stdlib/list.js');
function MakeSet(Item) {
var add = function(currentSet, newItem) {
if (
List.exists(function(x) {
return Item.equal(x, newItem);
}, currentSet)
) {
return currentSet;
} else {
return {
hd: newItem,
tl: currentSet,
};
}
};
return {
empty: /* [] */ 0,
add: add,
};
}

펑터는 함수 애플리케이션 문법을 이용하여 적용될 수 있습니다. 이 경우에는 정수값들의 페어로 구성된 집합을 만듭니다.

module IntPair = {
type t = (int, int)
let equal = ((x1: int, y1: int), (x2, y2)) => x1 == x2 && y1 == y2
let create = (x, y) => (x, y)
}
/* IntPair는 MakeSet이 요구하는 Comparable 시그니쳐를 만족합니다.*/
module SetOfIntPairs = MakeSet(IntPair)
function equal(param, param$1) {
if (param[0] === param$1[0]) {
return param[1] === param$1[1];
} else {
return false;
}
}
function create(x, y) {
return [x, y];
}
var IntPair = {
equal: equal,
create: create,
};
var SetOfIntPairs = {
empty: /* [] */ 0,
add: add,
};

모듈 함수의 타입

모듈 타입들과 마찬가지로, 펑터 타입도 펑터에 가정한 것을 제한하고 은닉하는 역할을 합니다. 펑터 타입의 문법은 함수 타입들을 위한 것으로 구성되어있습니다만, 인자 값과 반환 타입이 모두 모듈들로 이루져있기 때문에 대문자로 시작합니다. 이전 예제는 backingType을 노출하고 있지만, MakeSet에 펑터 시그니처를 제공해 자료구조를 숨길 수 있습니다.

module type Comparable = ...
module type MakeSetType = (Item: Comparable) => {
type backingType
let empty: backingType
let add: (backingType, Item.t) => backingType
}
module MakeSet: MakeSetType = (Item: Comparable) => {
...
}
// 빈 출력입니다.

모듈들과 펑터들은 언어 나머지 기능(함수, let 바인딩 데이터 구조 등)과는 다른 레이어에 있습니다. 예를 들면, 함수 펑터에 튜플이나 레코드를 전달 할 수 없습니다. 적절하게 사용하시기 바랍니다. 대부분의 경우 함수와 레코드만으로 충분합니다.