2025년 최고의 자바스크립트(JavaScript) 대안, 리스크립트(ReScript) 파헤치기!
안녕하세요! 오늘은 2025년에 주목해야 할 자바스크립트(JavaScript)의 강력한 대안, 바로 리스크립트(ReScript)에 대해 알아보려고 한답니다.
리스크립트(ReScript) 소개
리스크립트(ReScript)는 그 자체로 정말 매력적인 특징들을 많이 가지고 있는데요.
예를 들면, 훨씬 더 탄탄한 타입 시스템, 순수 함수형 프로그래밍 지원 강화, 강력한 언어 기능들, 그리고 네이티브 언어로 작성되어 성능이 어마어마하게 빠른 컴파일러 등이 있습니다.
물론, 그에 상응하는 단점들도 존재하는데요.
이 글에서는 리스크립트(ReScript)의 강력한 기능들, 주변 생태계, 그리고 우리가 매일 사용하는 리액트(React)와의 통합에 초점을 맞춰서 자세히 소개해 드릴게요.
언어 특징들
리스크립트(ReScript)의 문법은 자바스크립트(JavaScript)의 상위 집합인 타입스크립트(TypeScript)와는 사뭇 다릅니다.
자바스크립트(JavaScript)와는 꽤 다른 모습을 하고 있죠.
자잘한 문법 하나하나를 다 설명하진 않을 거고요.
대신, 대표적인 특징들 위주로 소개해 드릴까 합니다.
타입 사운드 (Type Sound)
'타입 사운드(Type Sound)'가 무슨 뜻인지 위키피디아(Wikipedia) 문장을 빌려 설명해 볼까요?
"타입 시스템이 사운드하다면, 해당 타입 시스템에서 통과된 표현식은 반드시 적절한 타입의 값으로 평가되어야 합니다. (엉뚱한 타입의 값을 내놓거나 타입 오류로 프로그램이 멈추는 일이 없어야 한다는 뜻입니다.)"
간단히 말해서, 컴파일을 통과한 타입 시스템은 실행 중에는 타입 오류를 일으키지 않는다는 의미입니다.
타입스크립트(TypeScript)는 타입 사운드하지 않은데요.
아래 예시를 보면 그 이유를 알 수 있습니다.
// typescript
// 이 코드는 유효한 타입스크립트(TypeScript) 코드입니다.
type T = {
x: number;
};
type U = {
x: number | string;
};
const a: T = {
x: 3
};
const b: U = a; // 타입스크립트(TypeScript)에서는 이 부분이 문제 없이 통과됩니다.
b.x = "i am a string now"; // b를 통해 a의 x 타입이 바뀔 수 있습니다.
const x: number = a.x; // x는 number로 예상했지만...
// 오류: x는 이제 문자열입니다!
a.x.toFixed(0); // 실행 시점에서 오류 발생!
리스크립트(ReScript)에서는 타입 검사를 통과했는데 막상 실행해 보니 타입 오류가 펑 터지는 그런 코드는 애초에 작성할 수가 없답니다.
위 예시에서 타입스크립트(TypeScript)가 컴파일을 통과할 수 있었던 이유는 타입스크립트(TypeScript)가 구조적 타이핑(Structural Typing)을 사용하기 때문인데요.
반면 리스크립트(ReScript)는 명목적 타이핑(Nominal Typing)을 사용합니다.
그래서 const b: U = a;
같은 코드는 리스크립트(ReScript)에서는 컴파일조차 되지 않습니다.
물론, 이것만으로 타입 사운드니스를 완벽히 보장한다고 단정할 순 없지만, 구체적인 증명 과정은 꽤 학술적이라 여기서 자세히 다루진 않겠습니다.
타입 사운드니스의 중요성은 프로젝트의 안정성을 더 확실하게 보장해 준다는 데 있습니다.
마치 대규모 프로젝트에서 자바스크립트(JavaScript)보다 타입스크립트(TypeScript)를 선호하는 이유와 비슷한데요.
프로그램 규모가 점점 커질수록, 사용하는 언어가 타입 사운드하다면 리팩토링 후에도 실행 시점 타입 오류 걱정 없이 과감하게 코드를 개선할 수 있습니다.
불변성 (Immutable)
데이터가 여기저기서 바뀌는 가변성은 데이터 변경을 추적하고 예측하기 어렵게 만들어 버그의 원인이 되곤 합니다.
불변성은 코드 품질을 높이고 버그를 줄이는 효과적인 방법 중 하나인데요.
동적 언어인 자바스크립트(JavaScript)는 불변성에 대한 지원이 거의 없는 편입니다.
TC39(자바스크립트 표준을 관리하는 기술 위원회)에서도 레코드 & 튜플(Record & Tuple)에 대한 관련 제안이 있긴 하지만, 현재 2단계에 머물러 있습니다.
리스크립트(ReScript)는 이 두 가지 데이터 타입을 이미 내장하고 있답니다.
레코드 (Record)
리스크립트(ReScript)의 레코드(Record)와 자바스크립트(JavaScript) 객체의 주된 차이점은 다음과 같습니다.
- 기본적으로 불변입니다.
- 레코드(Record)를 정의할 때 반드시 해당 타입을 선언해야 합니다.
// rescript
// person 타입을 정의합니다.
type person = {
age: int, // 나이는 정수형
name: string, // 이름은 문자열
}
// 'me'라는 person 타입의 변수를 만듭니다.
let me: person = {
age: 5,
name: "Big ReScript"
}
// age 필드를 업데이트합니다. 기존 'me'는 바뀌지 않고 새로운 레코드가 생성됩니다.
let meNextYear = {...me, age: me.age + 1}
리스크립트(ReScript)는 특정 레코드(Record) 필드를 가변적으로 업데이트할 수 있는 예외적인 방법도 제공합니다.
// rescript
type person = {
name: string,
mutable age: int // age 필드는 변경 가능하도록 mutable 키워드를 사용합니다.
}
let baby = {name: "Baby ReScript", age: 5}
// age 필드를 직접 변경합니다.
baby.age = baby.age + 1
튜플 (Tuple)
타입스크립트(TypeScript)에도 튜플(Tuple) 데이터 타입이 있는데요.
리스크립트(ReScript) 튜플(Tuple)의 유일한 차이점은 기본적으로 불변이라는 점입니다.
// rescript
// 정수형과 문자열로 이루어진 튜플입니다.
let ageAndName: (int, string) = (24, "Lil' ReScript")
// 튜플 타입 별칭을 만듭니다.
type coord3d = (float, float, float) // 3차원 좌표
// coord3d 타입의 변수를 만듭니다.
let my3dCoordinates: coord3d = (20.0, 30.5, 100.0)
// 튜플 업데이트 (새로운 튜플 생성)
let coordinates1 = (10, 20, 30)
// 첫 번째 요소만 가져오고 나머지는 무시합니다.
let (c1x, _, _) = coordinates1
// c1x 값을 변경하여 새로운 튜플을 만듭니다.
let coordinates2 = (c1x + 50, 20, 30)
배리언트 (Variant)
배리언트(Variant)는 리스크립트(ReScript)에서 꽤 특별한 데이터 구조인데요.
열거형(enumeration)이나 생성자(constructor) 같은 대부분의 데이터 모델링 시나리오를 포괄합니다 (리스크립트(ReScript)에는 클래스(class) 개념이 없습니다).
// rescript
// 열거형을 정의합니다.
type animal = Dog | Cat | Bird // 동물은 개, 고양이, 새 중 하나입니다.
// 생성자를 정의합니다. 어떤 개수의 매개변수든 전달하거나 레코드를 직접 전달할 수 있습니다.
type account =
| Wechat(int, string) // 위챗 계정은 정수와 문자열을 가집니다.
| Twitter({name: string, age: int}) // 트위터 계정은 이름과 나이를 가진 레코드를 가집니다.
배리언트(Variant)는 리스크립트(ReScript)의 다른 기능들과 결합하여, 다음에 이야기할 패턴 매칭(Pattern Matching)처럼 강력하고 우아한 논리 표현 능력을 보여줍니다.
패턴 매칭 (Pattern Matching)
패턴 매칭(Pattern Matching)은 프로그래밍 언어에서 가장 유용한 기능 중 하나입니다.
대수적 데이터 타입(ADT, Algebraic Data Type)과 결합하면, 전통적인 if
나 switch
문보다 훨씬 뛰어난 표현력을 보여주는데요.
값뿐만 아니라 특정 타입 구조까지 판단할 수 있습니다.
자바스크립트(JavaScript)에도 관련 제안이 있지만, 아직 1단계에 머물러 있어 실제로 사용하기까지는 갈 길이 멉니다.
이 강력한 기능을 소개하기 전에, 먼저 타입스크립트(TypeScript)의 구별된 유니온(discriminated union) 예시를 살펴보겠습니다.
// typescript
// 태그된 유니온 (Tagged Union)
type Shape =
| { kind: "circle"; radius: number } // 원
| { kind: "square"; x: number } // 정사각형
| { kind: "triangle"; x: number; y: number }; // 삼각형
function area(s: Shape) {
switch (s.kind) { // 'kind' 태그로 타입을 구분합니다.
case "circle":
return Math.PI * s.radius * s.radius;
case "square":
return s.x * s.x;
default: // 'triangle'의 경우
// 타입스크립트(TypeScript)는 s가 Triangle임을 알지만, 명시적으로 캐스팅해야 할 수도 있습니다.
// 여기서는 s.y가 Triangle에만 있으므로 default에서 처리합니다.
const triangle = s as { kind: "triangle"; x: number; y: number };
return (triangle.x * triangle.y) / 2;
}
}
타입스크립트(TypeScript)에서는 유니온 타입의 구체적인 타입을 구별하고 싶을 때, 수동으로 kind
같은 문자열 태그를 추가해서 구분해야 합니다.
이런 방식은 다소 번거로운데요.
이제 리스크립트(ReScript)가 이 문제를 어떻게 다루는지 살펴보겠습니다.
// rescript
// 배리언트를 사용해 shape 타입을 정의합니다.
type shape =
| Circle({radius: float}) // 원, 반지름(실수)을 가집니다.
| Square({x: float}) // 정사각형, 한 변의 길이(실수)를 가집니다.
| Triangle({x: float, y: float}) // 삼각형, 밑변과 높이(실수)를 가집니다.
let area = (s: shape) => {
switch s { // shape 타입 s에 대해 패턴 매칭을 수행합니다.
// 리스크립트(ReScript)에서 실수 연산자는 뒤에 점(.)을 붙입니다. 예: +., -., *.
| Circle({radius}) => Js.Math._PI *. radius *. radius // 원의 넓이
| Square({x}) => x *. x // 정사각형의 넓이
| Triangle({x, y}) => x *. y /. 2.0 // 삼각형의 넓이
}
}
let a = area(Circle({radius: 3.0})) // 반지름이 3.0인 원의 넓이를 계산합니다.
배리언트(Variant)를 사용해 합타입(Sum Type)을 구성하고, 패턴 매칭(Pattern Matching)으로 특정 타입을 식별하고 속성을 분해하면, 수동으로 태그를 추가할 필요가 없습니다.
작성 스타일과 경험이 훨씬 우아해지는데요.
컴파일된 자바스크립트(JavaScript) 코드를 보면 실제로는 태그를 사용해서 구분하지만, 리스크립트(ReScript)를 통해 우리는 대수적 데이터 타입(ADT)과 패턴 매칭(Pattern Matching)이 가져다주는 이점을 누릴 수 있습니다.
// 컴파일된 자바스크립트(JavaScript) 코드
function area(s) {
switch (s.TAG | 0) { // 내부적으로 TAG를 사용해 구분합니다.
case /* Circle */0 : // Circle 타입
var radius = s.radius;
return Math.PI * radius * radius;
case /* Square */1 : // Square 타입
var x = s.x;
return x * x;
case /* Triangle */2 : // Triangle 타입
return s.x * s.y / 2.0;
}
}
var a = area({
TAG: /* Circle */0, // Circle임을 나타내는 태그
radius: 3.0
});
NPE (Null Pointer Exception) 문제
NPE, 즉 Null 포인터 예외 문제에 대해 타입스크립트(TypeScript)는 strictNullCheck
와 옵셔널 체이닝(optional chaining)을 통해 효과적으로 해결할 수 있게 되었습니다.
리스크립트(ReScript)는 기본적으로 null
과 undefined
타입이 없는데요.
데이터가 비어 있을 수 있는 경우, 리스크립트(ReScript)는 내장된 option
타입과 패턴 매칭(Pattern Matching)을 사용해 이 문제를 해결합니다. 러스트(Rust)와 비슷한 방식입니다.
먼저 리스크립트(ReScript)의 내장 option
타입 정의를 살펴보겠습니다.
// rescript
// 'a는 제네릭 타입을 나타냅니다.
type option<'a> = None | Some('a) // None은 값이 없음을, Some('a)는 'a 타입의 값이 있음을 의미합니다.
패턴 매칭(Pattern Matching)을 사용하는 예시입니다.
// rescript
let licenseNumber = Some(5) // 면허 번호가 5인 경우
switch licenseNumber {
| None => // 값이 없는 경우 (None)
Js.log("The person doesn't have a car") // "이 사람은 차가 없습니다" 출력
| Some(number) => // 값이 있는 경우 (Some), number 변수에 값이 담깁니다.
Js.log("The person's license number is " ++ Js.Int.toString(number)) // "이 사람의 면허 번호는 [번호]입니다" 출력
}
레이블된 인수 (Labeled Arguments)
레이블된 인수(Labeled Arguments)는 사실 이름 있는 매개변수(named parameters)와 같습니다.
자바스크립트(JavaScript) 자체는 이 기능을 지원하지 않는데요.
보통 함수 매개변수가 많을 때, 객체 구조 분해(object deconstruction)를 사용해서 이름 있는 매개변수의 흉내만 내곤 합니다.
// javascript
const func = ({
a,
b,
c,
d,
e,
f,
g
})=>{
// 함수 내용
}
이 방법의 불편한 점은 객체를 위해 별도의 타입 선언을 작성해야 한다는 것인데, 꽤 번거롭습니다.
이제 리스크립트(ReScript)의 문법은 어떤지 살펴보겠습니다.
// rescript
// 물결표시(~)로 레이블된 인수를 표시합니다.
let sub = (~first: int, ~second: int) => first - second
sub(~second = 2, ~first = 5) // 결과는 3. 인수의 순서가 바뀌어도 괜찮습니다.
// 별칭(alias) 사용
let sub = (~first as x: int, ~second as y: int) => x - y // first를 x로, second를 y로 사용합니다.
파이프 (Pipe)
자바스크립트(JavaScript)에도 파이프 연산자(pipe operator)에 대한 제안이 있고, 현재 2단계에 있습니다.
파이프 연산자는 validateAge(getAge(parseData(person)))
처럼 중첩된 함수 호출 문제를 비교적 우아하게 해결할 수 있는데요.
리스크립트(ReScript)의 파이프는 기본적으로 '파이프 퍼스트(pipe first)' 방식입니다. 즉, 다음 함수의 첫 번째 매개변수로 값을 전달합니다.
// rescript
let add = (x,y) => x + y
let sub = (x,y) => x - y
let mul = (x,y) => x * y
// (1 + 5 - 2) * 3 = 12
let num1 = mul(sub(add(1,5),2),3) // 일반적인 함수 호출
// 파이프 연산자(->) 사용
let num2 = add(1,5) // 1과 5를 더하고 (결과: 6)
->sub(2) // 그 결과에서 2를 빼고 (결과: 4)
->mul(3) // 그 결과에 3을 곱합니다. (결과: 12)
보통 자바스크립트(JavaScript)에서는 아래와 같이 메서드 체이닝(method chaining)을 사용해 중첩된 함수 호출을 최적화합니다.
// typescript
let array = [1,2,3]
let num = array.map(item => item + 2).reduce((acc,cur) => acc + cur, 0)
리스크립트(ReScript)에는 클래스(class)가 없어서 클래스 메서드(class method)라는 것도 없고, 따라서 메서드 체이닝(method chaining)도 없을 것이라는 점은 주목할 만합니다.
리스크립트(ReScript)의 많은 내장 표준 라이브러리(예: 배열의 map
이나 reduce
)는 데이터 우선(data-first) 접근 방식과 파이프 연산자를 사용해 우리가 자바스크립트(JavaScript)에서 익숙하게 사용하던 메서드 체이닝(method chaining)과 유사한 효과를 냅니다.
// rescript
// 리스크립트(ReScript) 표준 라이브러리의 map과 reduce 사용 예시
// Belt는 리스크립트(ReScript)의 표준 유틸리티 라이브러리입니다.
Belt.Array.map([1, 2], (x) => x + 2) == [3, 4] // 배열의 각 요소에 2를 더합니다.
Belt.Array.reduce([2, 3, 4], 1, (acc, value) => acc + value) == 10 // 초기값 1부터 시작해 배열 요소들을 더합니다.
let array = [1,2,3]
let num = array
-> Belt.Array.map(x => x + 2) // 각 요소에 2를 더하고 [3,4,5]
-> Belt.Array.reduce(0, (acc, value) => acc + value) // 모든 요소를 더합니다 (0+3+4+5 = 12)
데코레이터 (Decorator)
리스크립트(ReScript)의 데코레이터(decorator)는 타입스크립트(TypeScript)처럼 클래스(class)의 메타프로그래밍(metaprogramming)에 사용되지 않습니다.
몇 가지 다른 용도가 있는데요.
예를 들어, 일부 컴파일 기능이나 자바스크립트(JavaScript)와의 상호 운용에 사용됩니다.
리스크립트(ReScript)에서는 다음과 같이 모듈을 가져오고 타입을 정의할 수 있습니다.
// rescript
// "path" 모듈의 dirname 메서드를 참조하고, 그 타입을 string => string (문자열을 받아 문자열을 반환)으로 선언합니다.
@module("path") external dirname: string => string = "dirname"
// "path"는 Node.js의 내장 모듈입니다.
let root = dirname("/Leapcell/github") // "/Leapcell"을 반환합니다. (실제 Node.js path.dirname 동작)
확장 포인트 (Extension Point)
데코레이터(decorator)와 비슷하게, 자바스크립트(JavaScript)를 확장하는 데 사용되지만 문법이 약간 다릅니다.
예를 들어, 프론트엔드 개발에서 우리는 보통 CSS를 가져오고 빌드 도구가 이를 처리하는데요.
하지만 리스크립트(ReScript)의 모듈 시스템에는 import
문이 없고 CSS 가져오기를 지원하지 않습니다.
이럴 때 보통 %raw
를 사용합니다.
// rescript
// %raw를 사용하여 JavaScript 코드를 직접 삽입합니다.
%raw(`import "./index.css";`) // index.css 파일을 가져옵니다.
// 컴파일된 자바스크립트(JavaScript)의 출력 내용
// import "./index.css";
리액트(React) 개발
JSX
리스크립트(ReScript)도 JSX 문법을 지원하지만, 프롭스(props) 할당 방식에 약간의 차이가 있습니다.
// rescript
// isLoading, text, onClick 프롭스를 전달합니다.
<MyComponent isLoading text onClick />
// 위 코드는 아래와 동일합니다.
// <MyComponent isLoading={isLoading} text={text} onClick={onClick} />
// 즉, 값 없이 프롭스 이름만 쓰면 해당 이름의 변수가 true로 전달되거나,
// 변수 이름과 프롭스 이름이 같으면 생략 가능합니다 (isLoading={isLoading} -> isLoading).
@rescript/react
@rescript/react
라이브러리는 주로 리액트(React)에 대한 리스크립트(ReScript) 바인딩(binding)을 제공하는데요.react
와 react-dom
을 포함합니다.
// rescript
// 리액트(React) 컴포넌트를 정의합니다.
module Friend = { // Friend라는 모듈(컴포넌트)을 만듭니다.
@react.component // 이 데코레이터는 이 함수가 리액트(React) 컴포넌트임을 나타냅니다.
let make = (~name: string, ~children) => { // make 함수가 컴포넌트의 실제 구현입니다.
// ~name은 레이블된 인수로 프롭스를 받습니다.
<div> // JSX를 사용합니다.
{React.string(name)} // 문자열 프롭스를 화면에 표시합니다.
children // 자식 요소들을 렌더링합니다.
</div>
}
}
리스크립트(ReScript)는 리액트(React) 컴포넌트 정의를 위해 @react.component
데코레이터(decorator)를 제공합니다.make
함수가 컴포넌트의 구체적인 구현이며, 레이블된 인수(labeled arguments)를 사용해 프롭스(props)를 받습니다.Friend
컴포넌트는 JSX에서 직접 사용할 수 있습니다.
// rescript
// Friend 컴포넌트를 사용합니다. name과 age 프롭스를 전달합니다.
// <Friend name="Leapcell" age=20 />
// 위 코드는 리스크립트(ReScript)가 JSX 문법 설탕을 제거하면 아래와 같이 변환됩니다.
// React.createElement(Friend.make, {name: "Leapcell", age:20})
// make 함수에 프롭스들이 객체 형태로 전달됩니다.
언뜻 보면 make
함수가 약간 불필요해 보일 수 있지만, 이는 몇 가지 역사적인 설계 이유 때문이라 여기서 너무 자세히 들어가진 않겠습니다.
생태계 (Ecosystem)
JS 생태계에 통합하기
자바스크립트(JavaScript) 방언(dialect)이 성공하는 핵심 요인 중 하나는 기존 자바스크립트(JavaScript) 생태계와 어떻게 통합되느냐입니다.
타입스크립트(TypeScript)가 이렇게 인기 있는 이유 중 하나는 기존 자바스크립트(JavaScript) 라이브러리를 재사용하기 쉽다는 점인데요.
좋은 d.ts
파일만 작성하면, 타입스크립트(TypeScript) 프로젝트에서 원활하게 가져와 사용할 수 있습니다.
사실 리스크립트(ReScript)도 비슷합니다.
자바스크립트(JavaScript) 라이브러리에 대해 관련된 리스크립트(ReScript) 타입만 선언해주면 되는데요.@rescript/react
를 예로 들어보겠습니다.
이 라이브러리는 리액트(React)에 대한 리스크립트(ReScript) 타입 선언을 제공합니다.
리액트(React)의 createElement
에 대한 타입을 어떻게 선언하는지 살펴보겠습니다.
(실제로는 createElement
보다는 render
함수 예시가 더 적절해 보입니다. 원문에서는 createElement
를 언급했지만, 코드 예시는 render
를 보여주고 있습니다. 여기서는 render
를 기준으로 설명하겠습니다.)
// rescript
// ReactDOM.res 파일 (리스크립트에서 파일 이름이 모듈 이름이 됩니다)
@module("react-dom") // "react-dom" 모듈에서 가져옵니다.
// external 키워드로 외부 함수를 바인딩합니다.
// render 함수는 React.element와 Dom.element를 받아 아무것도 반환하지 않는(unit) 함수로 선언합니다.
// "render"는 실제 "react-dom" 라이브러리의 함수 이름입니다.
external render: (React.element, Dom.element) => unit = "render"
// 리스크립트(ReScript)의 모듈 시스템에서는 각 파일이 모듈이고, 모듈 이름은 파일 이름입니다.
// import 할 필요 없이 바로 ReactDOM.render 처럼 사용할 수 있습니다. (단, 위 @module 선언이 필요)
// "#root" ID를 가진 DOM 요소를 찾습니다.
let rootQuery = ReactDOM.querySelector("#root")
switch rootQuery { // option 타입을 반환하므로 패턴 매칭으로 처리합니다.
| Some(root) => ReactDOM.render(<App />, root) // root 요소가 있으면 App 컴포넌트를 렌더링합니다.
| None => () // root 요소가 없으면 아무것도 하지 않습니다. (unit 타입 반환)
}
강력한 컴파일러
타입스크립트(TypeScript)의 컴파일러는 노드제이에스(Node.js)로 작성되어 컴파일 속도가 항상 비판받아 왔습니다.
그래서 타입 제거만 수행하는 esbuild나 swc 같은 타입스크립트(TypeScript) 컴파일러가 등장했지만, 여전히 타입 검사 필요성을 충족시키지는 못하는데요.
그래서 stc 프로젝트 (러스트(Rust)로 작성된 타입스크립트(TypeScript) 타입 검사기)도 많은 관심을 받고 있습니다.
리스크립트(ReScript)는 이 문제에 대해 크게 걱정할 필요가 없습니다.
리스크립트(ReScript)의 컴파일러는 네이티브 언어인 오캐멀(OCaml)로 구현되어 있어, 컴파일 속도가 리스크립트(ReScript) 프로젝트에서 걱정하고 해결해야 할 문제가 되지 않을 겁니다.
게다가 리스크립트(ReScript)의 컴파일러는 많은 기능을 가지고 있는데요.
이 부분에 대한 자세한 문서가 없어서, 여기서는 제가 조금 이해하고 있는 몇 가지 기능만 나열해 보겠습니다.
상수 폴딩 (Constant Folding)
상수 폴딩(Constant folding)은 상수 표현식의 값을 계산하여 최종 생성된 코드에 상수로 포함시키는 것을 의미합니다.
리스크립트(ReScript)에서는 일반적인 상수 표현식과 간단한 함수 호출은 모두 상수 폴딩(Constant folding) 대상이 될 수 있습니다.
// rescript
let add = (x,y) => x + y
let num = add(5,3) // 컴파일 시점에 5 + 3이 계산됩니다.
// 컴파일된 자바스크립트(JavaScript)
function add(x, y) {
return x + y | 0; // 정수 덧셈임을 명시 (비트 OR 연산)
}
var num = 8; // add(5,3)의 결과인 8이 직접 할당됩니다.
같은 코드를 타입스크립트(TypeScript)로 작성했을 때 컴파일 결과는 다음과 같습니다.
// typescript
let add = (x:number,y:number)=>x + y
let num = add(5,3)
// 컴파일된 자바스크립트(JavaScript)
"use strict";
let add = (x, y) => x + y;
let num = add(5, 3); // 함수 호출 형태로 남아있습니다.
타입 추론 (Type Inference)
타입스크립트(TypeScript)에도 타입 추론 기능이 있지만, 리스크립트(ReScript)의 타입 추론은 더욱 강력합니다.
문맥 기반 타입 추론을 수행할 수 있어서, 대부분의 경우 리스크립트(ReScript) 코드를 작성할 때 변수에 타입을 거의 선언할 필요가 없습니다.
// rescript
// 피보나치 수열, rec은 재귀 함수를 선언할 때 사용합니다.
let rec fib = (n) => { // n의 타입을 명시하지 않았습니다.
switch n { // n에 대한 패턴 매칭
| 0 => 0
| 1 => 1
// 여기서 n이 정수형(int)으로 사용되는 것을 보고 컴파일러가 n의 타입을 int로 추론합니다.
| _ => fib(n - 1) + fib(n - 2)
}
}
위 리스크립트(ReScript)로 구현된 피보나치 수열 함수에는 변수 선언이 없지만, 리스크립트(ReScript)는 패턴 매칭(Pattern Matching)의 문맥에서 n
이 int
타입임을 추론할 수 있습니다.
같은 예시에서 타입스크립트(TypeScript)는 n
에 대해 number
타입을 반드시 선언해야 합니다.
// typescript
// 매개변수 'n'은 암묵적으로 'any' 타입을 가집니다. (strict 모드가 아니라면)
// 또는 'noImplicitAny' 옵션이 켜져 있다면 에러가 발생합니다.
let fib = (n: number) => { // n에 타입을 명시해야 합니다.
switch (n) {
case 0:
return 0;
case 1:
return 1;
default:
return fib(n - 1) + fib(n - 2)
}
}
타입 레이아웃 최적화 (Type Layout Optimization)
타입 레이아웃 최적화의 기능 중 하나는 코드 크기를 최적화하는 것입니다.
예를 들어, 객체를 선언하는 것은 배열을 선언하는 것보다 더 많은 코드를 필요로 합니다.
// javascript
// 일반적인 객체와 배열 선언
let a = {width: 100, height: 200}
let b = [100,200]
// 코드 축소(uglification) 후 (변수명은 예시입니다)
// let a={a:100,b:100} // 키 이름도 짧아집니다.
// let b=[100,200]
위 예시에서 객체 선언의 가독성은 배열로 대체될 수 없습니다.
일상적인 사용에서 우리는 이런 종류의 최적화를 위해 코드 유지보수성을 희생하지 않을 겁니다.
리스크립트(ReScript)에서는 위에서 언급한 데코레이터(decorator)를 통해 코드를 작성할 때 가독성을 유지하면서, 컴파일된 자바스크립트(JavaScript)에서는 코드 크기를 최적화할 수 있습니다.
// rescript
// node 타입을 정의하며, @as 데코레이터를 사용해 컴파일 시 배열 인덱스로 매핑합니다.
type node = {@as("0") width : int , @as("1") height : int}
let a: node = {width: 100,height: 200}
// 컴파일된 자바스크립트(JavaScript)
// a는 객체가 아닌 배열로 변환되어 코드 크기가 줄어듭니다.
var a = [
100, // width 값
200 // height 값
];
독특한 자바스크립트(JavaScript) 방언으로서 리스크립트(ReScript)는 타입 시스템, 언어 기능, 리액트(React)와의 통합, 생태계 통합 측면에서 자체적인 장점을 가지고 있습니다.
강력한 컴파일러는 또한 개발에 많은 편의를 가져다주는데요.
타입스크립트(TypeScript)가 대세인 현재 환경에서 리스크립트(ReScript)는 여전히 상대적으로 틈새 언어일 수 있지만, 그 특징들은 개발자들이 깊이 이해하고 탐구할 가치가 있으며, 프로젝트 개발에 새로운 아이디어와 해결책을 가져다줄 수도 있습니다.
'Javascript' 카테고리의 다른 글
fetchpriority (페치프라이오리티)로 리소스 로딩 최적화하기 (0) | 2025.05.17 |
---|---|
React (리액트) Fast Refresh (패스트 리프레시): 차세대 핫 리로딩 완벽 해부 (0) | 2025.05.17 |
Node.js (노드제이에스) 프로세스 종료 전략: 시그널, 오류 그리고 우아한 종료 완벽 가이드 (0) | 2025.05.17 |
Base64 (베이스64) 인코딩 완벽 정복: 당신이 알아야 할 모든 것 (1) | 2025.05.17 |
웹 페이지 로딩 속도 개선의 핵심! HTTP 캐싱: 강력한 캐시와 협상 캐시 완전 정복 (0) | 2025.05.17 |