Rescript: 2025년 최고의 JavaScript 대안
Grace Collins
Solutions Engineer · Leapcell

ReScript 소개
이 언어 자체는 더 강력한 타입 시스템, 더 순수한 함수형 프로그래밍 지원, 강력한 언어 기능 및 매우 높은 성능을 제공하는 네이티브 언어로 작성된 컴파일러와 같은 많은 놀라운 기능을 가지고 있습니다. 물론 그에 상응하는 단점도 있습니다. 이 기사에서는 ReScript의 강력한 기능, 주변 생태계 및 일상적인 사용과 가장 밀접한 관련이 있는 React와의 통합에 중점을 둘 것입니다.
언어 기능
ReScript의 구문은 JavaScript의 상위 집합인 TypeScript와 같지 않습니다. JavaScript와는 상당히 다릅니다. 사소한 구문에 대해서는 자세히 설명하지 않겠습니다. 대신 소개를 위해 몇 가지 일반적인 기능을 주로 나열하겠습니다.
타입 건전성
"타입 건전성"의 의미는 Wikipedia의 문장을 인용하여 소개할 수 있습니다.
"타입 시스템이 건전하면 해당 타입 시스템에서 허용하는 표현식은 일부 다른 관련 없는 타입의 값을 생성하거나 타입 오류로 인해 충돌하는 것이 아니라 적절한 타입의 값으로 평가되어야 합니다."
간단히 말해서 컴파일을 통과하는 타입 시스템은 런타임 시 타입 오류를 생성하지 않습니다. TypeScript는 타입 건전성이 없습니다. 다음 예에서 그 이유를 확인할 수 있습니다.
// typescript // 유효한 TypeScript 코드입니다. type T = { x: number; }; type U = { x: number | string; }; const a: T = { x: 3 }; const b: U = a; b.x = "i am a string now"; const x: number = a.x; // 오류: x는 문자열입니다. a.x.toFixed(0);
ReScript에서는 타입 컴파일을 통과하지만 런타임 시 타입 오류를 생성하는 코드를 작성할 수 없습니다. 위의 예에서 TypeScript는 구조적 타입이기 때문에 TypeScript는 컴파일할 수 있지만 ReScript는 명목적 타입입니다. const b: U = a;
코드는 컴파일되지 않습니다. 물론 이것만으로는 타입 건전성을 보장할 수 없습니다. 구체적인 증명 과정은 상당히 학문적이므로 여기서는 자세히 설명하지 않겠습니다.
타입 건전성의 중요성은 프로젝트의 보안을 더 잘 보장하는 데 있습니다. 대규모 프로젝트에서 TypeScript가 JavaScript보다 유리한 점과 마찬가지로 프로그램 규모가 점점 커질 때 사용하는 언어가 타입 건전하면 리팩토링 후 런타임 타입 오류에 대한 걱정 없이 두려움 없이 리팩토링을 수행할 수 있습니다.
불변성
가변성은 데이터 변경을 추적하고 예측하기 어렵게 만들어 버그로 이어질 수 있습니다. 불변성은 코드 품질을 개선하고 버그를 줄이는 효과적인 수단입니다. 동적 언어인 JavaScript는 불변성에 대한 지원이 거의 없습니다. TC39에는 현재 stage 2에 있는 Record & Tuple에 대한 관련 제안도 있습니다. ReScript에는 이미 이러한 두 가지 데이터 타입이 내장되어 있습니다.
레코드
ReScript 레코드와 JavaScript 객체의 주요 차이점은 다음과 같습니다.
- 기본적으로 불변입니다.
- 레코드를 정의할 때 해당 타입을 선언해야 합니다.
// rescript type person = { age: int, name: string, } let me: person = { age: 5, name: "Big ReScript" } // age 필드 업데이트 let meNextYear = {...me, age: me.age + 1}
ReScript는 특정 레코드 필드의 가변 업데이트를 위한 이스케이프 해치도 제공합니다.
// rescript type person = { name: string, mutable age: int } let baby = {name: "Baby ReScript", age: 5} // age 필드 업데이트 baby.age = baby.age + 1
튜플
TypeScript에도 튜플 데이터 타입이 있습니다. ReScript 튜플의 유일한 차이점은 기본적으로 불변이라는 것입니다.
let ageAndName: (int, string) = (24, "Lil' ReScript") // 튜플 타입 별칭 type coord3d = (float, float, float) let my3dCoordinates: coord3d = (20.0, 30.5, 100.0) // 튜플 업데이트 let coordinates1 = (10, 20, 30) let (c1x, _, _) = coordinates1 let coordinates2 = (c1x + 50, 20, 30)
베리언트
베리언트는 ReScript에서 상당히 특별한 데이터 구조로, 열거형 및 생성자(ReScript에는 클래스 개념이 없음)와 같은 대부분의 데이터 모델링 시나리오를 다룹니다.
// rescript // 열거형 정의 type animal = Dog | Cat | Bird // 생성자, 매개변수를 원하는 만큼 전달하거나 레코드를 직접 전달할 수 있습니다. type account = Wechat(int, string) | Twitter({name: string, age: int})
ReScript의 다른 기능과 결합하면 베리언트는 패턴 매칭과 같은 강력하고 우아한 논리적 표현 기능(다음에 설명)을 달성할 수 있습니다.
패턴 매칭
패턴 매칭은 프로그래밍 언어에서 가장 유용한 기능 중 하나입니다. ADT(대수 데이터 타입)와 결합하면 표현력이 기존의 if & switch 문보다 훨씬 좋습니다. 값을 판단할 수 있을 뿐만 아니라 특정 타입 구조도 판단할 수 있습니다. JavaScript에도 관련 제안이 있지만 stage 1에 불과하며 실제로 사용하려면 아직 멀었습니다. 이 강력한 기능을 소개하기 전에 먼저 TypeScript의 식별된 유니온의 예를 살펴보겠습니다.
// typescript // 태그된 유니온 type Shape = | { kind: "circle"; radius: number } | { kind: "square"; x: number } | { kind: "triangle"; x: number; y: number }; function area(s: Shape) { switch (s.kind) { case "circle": return Math.PI * s.radius * s.radius; case "square": return s.x * s.x; default: return (s.x * s.y) / 2; } }
TypeScript에서는 유니온 타입의 특정 타입을 구별하려면 구별하기 위해 kind 문자열 태그를 수동으로 추가해야 합니다. 이 형식은 비교적 번거롭습니다. 다음으로 ReScript가 이 형식을 어떻게 처리하는지 살펴보겠습니다.
// rescript type shape = | Circle({radius: float}) | Square({x: float}) | Triangle({x: float, y: float}) let area = (s: shape) => { switch 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}))
베리언트를 결합하여 합계 타입을 구성한 다음 패턴 매칭을 사용하여 특정 타입을 일치시키고 속성을 분해하면 태그를 수동으로 추가할 필요가 없습니다. 작성 스타일과 경험이 훨씬 더 우아합니다. 컴파일된 JavaScript 코드는 실제로 태그를 사용하여 구별하지만 ReScript를 통해 ADT 및 패턴 매칭으로 인한 이점을 누릴 수 있습니다.
// 컴파일된 JavaScript 코드 function area(s) { switch (s.TAG | 0) { case /* Circle */0 : var radius = s.radius; return Math.PI * radius * radius; case /* Square */1 : var x = s.x; return x * x; case /* Triangle */2 : return s.x * s.y / 2.0; } } var a = area({ TAG: /* Circle */0, radius: 3.0 });
NPE
NPE 문제에 대해 TypeScript는 이제 strictNullCheck 및 선택적 체이닝을 통해 효과적으로 해결할 수 있습니다. ReScript에는 기본적으로 null 및 undefined 타입이 없습니다. 데이터가 비어 있을 수 있는 경우 ReScript는 Rust와 유사하게 문제 해결을 위해 기본 제공 옵션 타입과 패턴 매칭을 사용합니다. 먼저 ReScript의 기본 제공 옵션 타입의 정의를 살펴보겠습니다.
// rescript // 'a는 제네릭 타입을 나타냅니다. type option<'a> = None | Some('a)
패턴 매칭 사용:
// rescript let licenseNumber = Some(5) switch licenseNumber { | None => Js.log("The person doesn't have a car") | Some(number) => Js.log("The person's license number is " ++ Js.Int.toString(number)) }
레이블이 지정된 인수
레이블이 지정된 인수는 실제로 명명된 매개변수입니다. 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 // 별칭 let sub = (~first as x: int, ~second as y: int) => x - y
파이프
JavaScript에도 파이프 연산자에 대한 제안이 있으며, 현재 stage 2에 있습니다. 파이프 연산자는 중첩된 함수 호출 문제를 비교적 우아하게 해결하여 validateAge(getAge(parseData(person)))
와 같은 코드를 피할 수 있습니다. ReScript의 파이프는 기본적으로 파이프 우선입니다. 즉, 다음 함수의 첫 번째 매개변수로 파이프합니다.
// rescript let add = (x,y) => x + y let sub = (x,y) => x - y let mul = (x,y) => x * y // (6 - 2)*3 = 12 let num1 = mul(sub(add(1,5),2),3) let num2 = add(1,5) ->sub(2) ->mul(3)
일반적으로 JavaScript에서는 메서드 체이닝을 사용하여 중첩된 함수 호출을 최적화합니다. 아래와 같이:
// typescript let array = [1,2,3] let num = array.map(item => item + 2).reduce((acc,cur) => acc + cur, 0)
ReScript에는 클래스가 없으므로 클래스 메서드와 같은 것은 없으며 메서드 체이닝도 없습니다. ReScript의 많은 기본 제공 표준 라이브러리(예: 배열의 map 및 reduce)는 데이터 우선 접근 방식과 파이프 연산자를 사용하여 JavaScript에서 익숙한 메서드 체이닝을 달성하도록 설계되었습니다.
// rescript // ReScript 표준 라이브러리에서 map 및 reduce를 사용하는 예 Belt.Array.map([1, 2], (x) => x + 2) == [3, 4] Belt.Array.reduce([2, 3, 4], 1, (acc, value) => acc + value) == 10 let array = [1,2,3] let num = array -> Belt.Array.map(x => x + 2) -> Belt.Array.reduce(0, (acc, value) => acc + value)
데코레이터
ReScript의 데코레이터는 TypeScript와 같은 클래스의 메타프로그래밍에 사용되지 않습니다. 몇 가지 다른 용도가 있습니다. 예를 들어 일부 컴파일 기능과 JavaScript와 상호 운용하는 데 사용됩니다. ReScript에서는 모듈을 가져와서 다음과 같이 타입을 정의할 수 있습니다.
// rescript // path 모듈의 dirname 메서드를 참조하고 타입을 string => string으로 선언합니다. @module("path") external dirname: string => string = "dirname" let root = dirname("/Leapcell/github") // "Leapcell"을 반환합니다.
확장 지점
데코레이터와 유사하게 JavaScript를 확장하는 데 사용되지만 구문이 약간 다릅니다. 예를 들어 프런트 엔드 개발에서는 일반적으로 CSS를 가져오면 빌드 도구가 그에 따라 처리합니다. 그러나 ReScript의 모듈 시스템에는 import 문이 없으며 CSS 가져오기를 지원하지 않습니다. 이 경우 일반적으로 %raw를 사용합니다.
// rescript %raw(`import "index.css";`) // 컴파일된 JavaScript의 출력 내용 import "index.css";
React 개발
JSX
ReScript는 JSX 구문도 지원하지만 props 할당에는 몇 가지 차이점이 있습니다.
// rescript <MyComponent isLoading text onClick /> // 와 동일함 <MyComponent isLoading={isLoading} text={text} onClick={onClick} />
@rescript/react
@rescript/react 라이브러리는 주로 react 및 react - dom을 포함하여 React에 대한 ReScript 바인딩을 제공합니다.
// rescript // React 컴포넌트 정의 module Friend = { @react.component let make = (~name: string, ~children) => { <div> {React.string(name)} children </div> } }
ReScript는 React 컴포넌트 정의를 위해 @react.component 데코레이터를 제공합니다. make 함수는 컴포넌트의 구체적인 구현으로 레이블이 지정된 인수를 사용하여 props를 가져옵니다. Friend 컴포넌트는 JSX에서 직접 사용할 수 있습니다.
// rescript <Friend name="Leapcell" age=20 /> // JSX syntactic sugar를 제거한 후의 ReScript 코드 React.createElement(Friend.make, {name: "Leapcell", age:20})
언뜻 보기에 make 함수가 약간 중복되어 보이지만 이는 몇 가지 역사적인 설계 이유 때문이므로 여기서는 너무 자세히 설명하지 않겠습니다.
생태계
JS 생태계에 통합
JavaScript 방언의 성공을 위한 핵심 요소 중 하나는 기존 JavaScript 생태계와 통합하는 방법입니다. TypeScript가 매우 인기 있는 이유 중 하나는 기존 JavaScript 라이브러리를 쉽게 재사용할 수 있기 때문입니다. 좋은 d.ts 파일을 작성하기만 하면 TypeScript 프로젝트가 원활하게 가져와서 사용할 수 있습니다. 사실 ReScript도 비슷합니다. JavaScript 라이브러리에 대한 관련 ReScript 타입을 선언하기만 하면 됩니다. @rescript/react를 예로 들어 보겠습니다. 이 라이브러리는 React에 대한 ReScript 타입 선언을 제공합니다. React의 createElement에 대한 타입을 선언하는 방법을 살펴보겠습니다.
// rescript // ReactDOM.res @module("react-dom") external render: (React.element, Dom.element) => unit = "render" // render 함수를 react - dom 라이브러리에 바인딩합니다. // ReScript의 모듈 시스템에서는 각 파일이 모듈이고 모듈 이름은 파일 이름입니다. 가져올 필요가 없으므로 ReactDOM.render를 직접 사용할 수 있습니다. let rootQuery = ReactDOM.querySelector("#root") switch rootQuery { | Some(root) => ReactDOM.render(<App />, root) | None => () }
강력한 컴파일러
TypeScript의 컴파일러는 Node.js로 작성되었으며 컴파일 속도가 항상 비판을 받아 왔습니다. 따라서 타입 삭제만 수행하는 esbuild 및 swc와 같은 TypeScript 컴파일러가 있지만 여전히 타입 검사가 필요한 요구 사항을 충족할 수 없습니다. 따라서 stc 프로젝트(Rust로 작성된 TypeScript 타입 검사기)도 많은 관심을 받고 있습니다. ReScript는 이 문제에 대한 걱정이 많지 않습니다. ReScript의 컴파일러는 네이티브 언어 OCaml로 구현되었으며 컴파일 속도는 ReScript 프로젝트가 걱정하고 해결해야 할 문제가 되지 않습니다. 또한 ReScript의 컴파일러에는 많은 기능이 있습니다. 이 측면에 대한 자세한 설명서가 없으므로 여기서는 제가 조금 이해하고 있는 몇 가지 기능만 나열합니다.
상수 폴딩
상수 폴딩은 상수 식의 값을 계산하고 최종 생성된 코드에 상수로 포함하는 것을 의미합니다. ReScript에서는 일반적인 상수 식과 간단한 함수 호출을 모두 상수 폴딩할 수 있습니다.
let add = (x,y) => x + y let num = add(5,3) // 컴파일된 JavaScript function add(x, y) { return x + y | 0; } var num = 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);
타입 추론
TypeScript에도 타입 추론이 있지만 ReScript의 타입 추론이 더 강력합니다. 컨텍스트 기반 타입 추론을 수행할 수 있습니다. 대부분의 경우 ReScript 코드를 작성할 때 변수 타입을 거의 선언할 필요가 없습니다.
// rescript // 피보나치 수열, rec는 재귀 함수를 선언하는 데 사용됩니다. let rec fib = (n) => { switch n { | 0 => 0 | 1 => 1 | _ => fib(n - 1) + fib(n - 2) } }
ReScript에서 구현된 위의 피보나치 수열 함수에는 변수 선언이 없지만 ReScript는 패턴 매칭 컨텍스트에서 n
이 int
타입임을 추론할 수 있습니다. 동일한 예에서 TypeScript는 n
에 대해 number
타입을 선언해야 합니다.
// typescript // 매개변수 'n'에는 암시적으로 'any' 타입이 있습니다. let fib = (n) => { switch (n) { case 0: return 0; case 1: return 1; default: return fib(n - 1) + fib(n - 2) } }
타입 레이아웃 최적화
타입 레이아웃 최적화의 기능 중 하나는 코드 크기를 최적화하는 것입니다. 예를 들어 객체를 선언하려면 배열을 선언하는 것보다 더 많은 코드가 필요합니다.
let a = {width: 100, height: 200} let b = [100,200] // 난독화 후 let a={a:100,b:100} let b=[100,200]
위의 예에서 객체 선언의 가독성은 배열로 대체할 수 없습니다. 일상적인 사용에서 이러한 종류의 최적화를 위해 코드 유지 관리성을 희생하지 않습니다. ReScript에서는 위에서 언급한 데코레이터를 통해 코드를 작성할 때 가독성을 유지할 수 있으며 컴파일된 JavaScript는 코드 크기를 최적화할 수도 있습니다.
type node = {@as("0") width : int , @as("1") height : int} let a: node = {width: 100,height: 200} // 컴파일된 JavaScript var a = [ 100, 200 ];
고유한 JavaScript 방언인 ReScript는 타입 시스템, 언어 기능, React와의 통합 및 생태계 통합 측면에서 자체적인 장점을 가지고 있습니다. 강력한 컴파일러는 개발에 많은 편의를 제공합니다. 현재 TypeScript가 인기 있는 환경에서는 ReScript가 여전히 틈새 시장일 수 있지만, ReScript가 가지고 있는 기능은 개발자가 심층적으로 이해하고 탐구할 가치가 있으며 프로젝트 개발에 새로운 아이디어와 솔루션을 제공할 수 있습니다.
Leapcell: 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼
마지막으로 웹 서비스를 배포하는 데 가장 적합한 플랫폼인 Leapcell을 소개합니다.
1. 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발합니다.
2. 무료로 무제한 프로젝트 배포
- 사용량에 대해서만 지불합니다. 요청이나 요금이 없습니다.
3. 타의 추종을 불허하는 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불합니다.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 지표 및 로깅.
5. 간편한 확장성 및 고성능
- 높은 동시성을 쉽게 처리하기 위한 자동 스케일링.
- 제로 운영 오버헤드 — 구축에만 집중하십시오.
Leapcell Twitter: https://x.com/LeapcellHQ