Rescript로 React 사용해보기

Rescript라는 언어가 있다. Typescript로 Javascript에 타입을 추가해 사용하듯이, Rescript는 함수형 언어로써 Javascript를 사용할 수 있게 해준다. Rescript를 통해 간단하게 React를 사용해보자.
2023.01.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Rescript라는 언어가 있다. Typescript로 Javascript에 타입을 추가해 사용하듯이, Rescript는 함수형 언어로써 Javascript를 사용할 수 있게 해준다. Rescript를 통해 간단하게 React를 사용해보자.

본 블로그의 코드는 아래의 레포에서 확인하실 수 있다.

https://github.com/Tolluset/rescript-react-todo

Rescript란

ReScript is the language for folks who don't necessarily love JavaScript, but who still acknowledge its importance.

Rescript는 Javascript를 좋아하지는 않지만, 중요성을 아는 사람을 위한 언어다.

Rescript는 함수형 언어를 지향하는 언어로써 Typescript와 같이 Javascript를 다른 방식으로 작성할 수 있게 해준다. 패턴 매칭, 베리언트, 파이프 등등 함수형 언어에서 사용하는 기능들을 제공해줄 뿐만 아니라, 컴파일 과정에서 Javascript를 좀 더 빠르게 (문서에 따르면) 만들 수도 있다고 한다.

좀 더 자세히 알고 싶다면 직접 문서를 보는 것을 추천한다.

영어: https://rescript-lang.org/

한국어: https://green-labs.github.io/rescript-in-korean/

프로젝트 셋업

Rescript를 컴파일하면 결국 Javascript 결과물이 나오기 때문에 보통 React를 셋업하듯이 Vite를 사용할 수 있다.

Vite 설정은 아래와 같이 리액트만 추가해주면 된다.

// vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

패키지는 아래와 같이 추가해준다.

// package.json

{
  "scripts": {
    "dev": "vite",
    "build": "yarn res:build && vite build",
    "res:dev": "rescript build -with-deps -w",
    "res:build": "rescript build -with-deps",
    "res:clean": "rescript clean -with-deps"
  },
  "dependencies": {
    "@rescript/react": "^0.11.0",
    "@ryyppy/rescript-promise": "^2.1.0",
    "bs-fetch": "^0.6.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "rescript": "^10.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^3.0.0",
    "vite": "^4.0.0"
  }
}

추가적으로, Typescript에서 tsconfig.json을 설정해주듯이 Rescript는 bsconfig.json이라는 파일을 설정해주어야 한다. 여기서 왜 bs라는 프리픽스가 있나 싶을 수 있는데, Rescript의 전신이 BuckleScript라는 이름을 가지고 있어서 레거시처럼 남아있다고 보면 된다. 참고로 이 곳 이외에도 여기저기 bs혹은 Belt라는 네이밍을 볼 수 있는데, 같은 맥락이라고 보면된다.

https://rescript-lang.org/blog/bucklescript-is-rebranding

// bsconfig.json

{
  "name": "rescript-react-todo",
  "jsx": {
    "version": 4,
    "mode": "automatic"
  },
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    }
  ],
  "package-specs": [
    {
      "module": "es6",
      "in-source": true
    }
  ],
  "bs-dependencies": [
    "@rescript/react",
    "bs-fetch",
    "@ryyppy/rescript-promise"
  ]
}

Rescript에서 사용할 설정들에 대한 파일이다. jsx의 버전이라던지, 소스 디렉토리, Rescirpt관련 패키지들에 대한 설정 등등 여러가지 설정을 여기서 할 수 있다.

그러면 이제 진짜 만들어보자.

코드

index.html

제일 먼저 index.html이 필요하다. Vite에서는 public 디렉토리가 아니라 최상위 디렉토리에 두면 된다.

// index.html

...
<div id="root"></div>
<script type="module" src="/src/main.js"></script>

별로 다를 것 없다. React가 들어가 태그 (root)와 Javascript 파일, css 파일을 일반적으로 가져오듯이 가져오면 된다.

이제 root 태그와 연결되는 main 파일을 작성해보자.

main.res

// main.res

// Use getExn to get error when can't find root tag
// ReactDOM.querySelector("#root1")
// ->Belt.Option.getExn
// ->ReactDOM.Client.createRoot
// ->ReactDOM.Client.Root.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>,
// )

let root = ReactDOM.querySelector("#root")

switch root {
| Some(root) =>
  root
  ->ReactDOM.Client.createRoot
  ->ReactDOM.Client.Root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  )
| None => Js.Exn.raiseError("Not found root tag")
}

2가지 방법이 있을 수 있다. 위에 주석처리한 부분은 만약 index.html에 root를 아이디로 가진 태그가 없다면 에러를 명시해줄 것이다. Belt.Option.getExn 부분을 통해서 런타임에 찾지 못하면 에러를 내뱉어준다. 아래와 같이 작성한다면 똑같이 | None => Js.Exn.raiseError("Not found root tag") 를 통해 런타임에서 에러를 뱉게 된다. 뭐가 다른걸까.

여기서 switch 문 같은데 요상한 | Some(root) 형식을 사용하는게 보인다. 이것은 Rescript에서 제공하는 패턴 매칭이라고 부르는 기능이다.

패턴 매칭이란, 간단하게 말하면 있을 수 있는 모든 타입에 대해 처리를 다해야 컴파일러가 안전한다고 생각하여 컴파일한다. 아주 간단한게 설명한 것이고 좀 더 자세한 내용은 검색하면 매우 딥하게 찾을 수도 있다. 여러 함수형 언어들에서 지원하는 경우가 많으니까 알아두면 나쁠 것 없다.

그럼 나는 왜 패턴 매칭으로 처리를 했을까. root의 타입은 option 와 같게 된다. 여기서 option은 있을 수도 없을 수도 있다는 것이고 해당 타입은 Dom.element를 가리킨다. querySelector로 태그를 검색하게 되면 위에 언급했듯이 html 파일에 해당 태그가 존재할 수도 존재하지 않을 수도 있다. 그렇다면, 컴파일러는 존재할 수도 존재하지 않을 수도 있는 경우의 수에 대해 모두 처리하지 않으면 컴파일을 실패하게 되고 개발자는 이에 대헤 처리를 하게 된다. 사실 둘다 같은 결과를 가져온다 볼 수 있지만, 언어의 철학에 맞게 런타임에 에러가 날 수도 있다고 표현하는 것보다는, 모든 경우의 수에 대해 명시적으로 처리해두는 것이 좋다 생각한다.

그럼 엔트리와 같은 main 파일에서 App 컴포넌트를 렌더링을 하고 있으니 App 파일을 봐보자.

App.res

// App.res

%%raw("import './styles/main.css'")

@react.component
let make = () => {
  let url = RescriptReactRouter.useUrl()

  switch url.path {
  | list{"todo"} => <Todo />
  | _ => <Todo />
  }
}

첫 라인부터 요상한 문법이 보인다. %%raw는 Javascript 코드를 직접 사용하게 해주는 문법이다. Rescript는 기본적으로 파일들이 모듈과 같다는 개념으로 파일명 자체가 모듈의 이름이 되며 모두 유니크하게 존재해야 한다. 따라서 import 문이 없기 때문에 이와 같이 Javascript 코드의 방법으로 import를 하게된다.

아래는 뜬금없는 어노테이션이 보이고 make라는 이름이 보인다. Javascript로 변환된 파일을 살펴보자.

// Generated by ReScript, PLEASE EDIT WITH CARE

import * as Todo from "./pages/Todo.js";
import * as JsxRuntime from "react/jsx-runtime";
import * as RescriptReactRouter from "@rescript/react/src/RescriptReactRouter.js";

import './styles/main.css';

function App(props) {
  RescriptReactRouter.useUrl(undefined, undefined);
  return JsxRuntime.jsx(Todo.make, {});
}

var make = App;

export {
  make ,
}
/*  Not a pure module */

코드를 살펴보면 결국 jsx를 리턴하는 것을 보여주는데 여기서도 Todo.make를 사용하고 있고 밑에서도 var make = App, export { make } 라는 형태가 보인다. 이것은 내가 정한 네이밍이 아니라 Rescript-React에서 정한 규칙으로 파일명이 컴포넌트의 이름이 되게된다. 특이한점은 props를 지정하지 않더라도 props를 받는 것 처럼 Javascript 코드가 생성된 것이다. 이것은 오류가 아니라 의도적으로 생성하는 모습이다. 아마 Rescript-React 개발의 용이함을 위함 혹은 어떠한 필요성에 따라 추가된 것으로 보이지만, 문서에는 딱히 써있진 않았다. 코드를 뒤져보면 알수도 있을 것 같은데, Ocaml이라는 근본 함수형 언어를 읽어야 하기 때문에 이번에는 패스한다. 문서에 @react.component는 암시적으로 () (unit, Javascript의 undefined 같은 위치)를 props의 마지막에 넣어준다는 문장이 있는데 관련이 있을 수도 있겠다.

React-router가 아니라 RescriptReactRouter가 보이는데 이는 Rescript-React에서 직접 만든 모듈이다. 생각보다 크지 않은 규모로 만들어져있는 것을 볼 수 있다.

https://github.com/rescript-lang/rescript-react/blob/master/src/RescriptReactRouter.res

코드를 보면 RescriptReactRouter로 부터 url에서 path를 가져와 "Todo" 이면 Todo 컴포넌트를 보여주고 아니더라도 Todo 컴포넌트를 보여주고 있다. | _ => 부분이 이외 모든 url에 대한 부분이라 보면 되는데 와일드카드같은 역할을 한다. 원래는 NotFound 같은 페이지를 보여주면 좋다, 하지만 생략한다.

그러면 이제 Todo 컴포넌트로 넘어가보자.

Todo.res

// Todo.res

@module external styles: {..} = "./Todo.module.css"

type todo = {
  userId: int,
  id: int,
  title: string,
  completed: bool,
}

external typeTodos: Js.Json.t => array<todo> = "%identity"

let url = "https://jsonplaceholder.typicode.com/users/1/todos"

@react.component
let make = () => {
  let (todos, setTodos) = React.useState(_ => [])

  React.useEffect0(() => {
    let break = ref(false)

    let getTodos = async () => {
      open Fetch
      open Promise

      setTodos(_ => [])

      let todosJson = await fetch(url)->then(Response.json)

      if !break.contents {
        let todos = typeTodos(todosJson)
        setTodos(prev => prev->Js.Array2.concat(todos))
      }
    }

    getTodos()->ignore

    Some(() => break := true)
  })

  let onClickTodo = (_, ~id: int) => {
    setTodos(prev =>
      prev->Js.Array2.map(item => {
        switch item.id == id {
        | true => {...item, completed: !item.completed}
        | false => item
        }
      })
    )
  }

  let onRemoveTodo = (_, ~id: int) => {
    setTodos(prev => prev->Js.Array2.filter(item => item.id !== id))
  }

  <div>
    {todos
    ->Js.Array2.map(todo =>
      <div
        key={Belt.Int.toString(todo.id)}
        className={styles["item"]}
        onClick={e => onClickTodo(e, ~id=todo.id)}>
        <span>
          {switch todo.completed {
          | true => "✅"->React.string
          | false => "❎"->React.string
          }}
        </span>
        <span
          style={ReactDOM.Style.make(
            ~textDecoration=switch todo.completed {
            | true => "line-through"
            | false => "none"
            },
            (),
          )}>
          {todo.title->React.string}
        </span>
        <button onClick={e => onRemoveTodo(e, ~id=todo.id)}> {"삭제"->React.string} </button>
        <hr />
      </div>
    )
    ->React.array}
  </div>
}

외부 api에서 json을 받아와 화면에 그리고 투두 완료 및 취소, 삭제는 api를 사용하지 않고 어플리케이션이 갖고 있는 데이터를 직접 수정하는 간단한 컴포넌트다.

부분 부분 나눠서 보자.

css module

@module external styles: {..} = "./Todo.module.css"

는 css 모듈을 불러오는 코드다. 일반적인 css 파일을 불러오기는 Javascript를 통해 임포트를 했지만 external이라는 키워드를 사용하여 모듈을 가져온다. 또한 {..}는 타입을 잘 모른다는 것을 표현한 것인데 {"root": string}와 같이 css를 다 타입화 하는 것은 불필요하니까 있는 표현이다. 이후 이 변수는 아래와 같이 사용된다.

...
      <div
        key={Belt.Int.toString(todo.id)}
        className={styles["item"]}

대괄호를 통해 접근하여 부여하고 싶은 클래스명을 해당 태그에 넣어주면 사용할 수 있다.

타입

type todo = {
  userId: int,
  id: int,
  title: string,
  completed: bool,
}

타입이다. Typescript에서 보던 타입과 유사하다.

let item = {
  userId: 1,
  id: 1,
  title: "Big ReScript",
  completed: "false"
}

필드명이 같다면 자동으로 타입을 추론해주는 방식을 사용한다. 물론 다른 모듈에 있다면 아래와 같이 모듈명을 명시해줘서 사용해야 한다.

let item: Todo.todo = {
  userId: 1,
  id: 1,
  title: "Big ReScript",
  completed: "false"
}

다음은 useState를 봐보자.

useState

  let (todos, setTodos) = React.useState(_ => [])

결과값이 배열이 아니고, 초기값이 함수로 들어있다. 결과값은 튜플이라는 Rescript의 자료형을 사용하게 되는데 Rescript에서 배열을 Javascript처럼 사용되지만 값들이 모두 같은(homogeneous) 하게 사용해야 하기 때문에 튜플이지 않을까 싶다. 초기값으로 함수를 받는것은 의아한데 바인딩(Typescript의 타입과 비슷함)을 봐보자.

https://github.com/rescript-lang/rescript-react/blob/master/src/React.res#L155

/*
 * Yeah, we know this api isn't great. tl;dr: useReducer instead.
 * It's because useState can take functions or non-function values and treats
 * them differently. Lazy initializer + callback which returns state is the
 * only way to safely have any type of state and be able to update it correctly.
 */
@module("react")
external useState: (@uncurry (unit => 'state)) => ('state, ('state => 'state) => unit) = "useState"

흥미롭다. 주석이 달려있는데, 함수와 함수가 아닌 경우 다르게 다뤄지기 때문에 이러한 useReducer를 권장한다. 중간에 @uncurry는 Rescript가 기본적으로 함수를 커링시키며 이러한 행위는 동적언어인 Javascript에서 위험할 수 있고, 계산 또한 많이들어서 사실 @uncurry를 권장한다.

따라서 아래와 같은 Rescript 코드가 어떻게 Javascript로 변환되는지 보면 알 수 있다.

let add = (x, y, z) => x + y

Js.log2(add(3), "add(3)")
Js.log2(add(3)(4), "add(3)(4)")
Js.log2(add(3, 4), "(add(3, 4)")
Js.log2(add(3, 4)(5), "add(3, 4)(5)")

let uncurryAdd = (. x, y, z) => x + y

Js.log2(uncurryAdd(. 3, 4, 5), "uncurryAdd(. 3, 4, 5)")

--- 

// Generated by ReScript, PLEASE EDIT WITH CARE


function add(x, y, z) {
  return x + y | 0;
}

console.log((function (param, param$1) {
        return 3 + param | 0;
      }), "add(3)");

console.log((function (param) {
        return 7;
      }), "add(3)(4)");

console.log((function (param) {
        return 7;
      }), "(add(3, 4)");

console.log(7, "add(3, 4)(5)");

function uncurryAdd(x, y, z) {
  return x + y | 0;
}

console.log(uncurryAdd(3, 4, 5), "uncurryAdd(. 3, 4, 5)");


export {
  add ,
  uncurryAdd ,
}
/*  Not a pure module */

인자 맨 앞자리에 . 을 찍는 것은 uncurried 하라고 명시하는 것이다. 코드를 살펴보면 일반적으로 생성한 add 함수는 커링이 가능하며, add(3, 4)(5) 의 경우 7로 최적화 되었지만, 다른 경우는 되지 않은 모습을 알 수 있다. 최적화가 되지 않았다기 보다 커링을 염두해 두어야하기 때문에 위와 같이 만들어졌다고도 볼 수 있다. 하지만 커링을 하지 않은 경우 최적화를 하지 않는다. 하지만 최적화를 하지 않음으로써 직관적인 코드가 되었고 컴파일러도 부담이 줄며 변환이 줄기에 Javascript 코드에 좀 더 가깝다고 볼 수 있다.

돌아와서 useEffect를 봐보자.

useEffect

  React.useEffect0(() => {
    let break = ref(false)

    let getTodos = async () => {
      open Fetch
      open Promise

      setTodos(_ => [])

      let todosJson = await fetch(url)->then(Response.json)

      if !break.contents {
        let todos = typeTodos(todosJson)
        setTodos(prev => prev->Js.Array2.concat(todos))
      }
    }

    getTodos()->ignore

    Some(() => break := true)
  })

useEffect옆에 숫자가 달려있다. 앞서 언급한 커링문제가 또 한번 발생한다.

https://github.com/rescript-lang/rescript-react/blob/master/src/React.res#L168-L192

@module("react")
external useEffect: (@uncurry (unit => option unit>)) => unit = "useEffect"
@module("react")
external useEffect0: (@uncurry (unit => option unit>), @as(json`[]`) _) => unit =
  "useEffect"
@module("react")
external useEffect1: (@uncurry (unit => option unit>), array) => unit = "useEffect"
@module("react")
external useEffect2: (@uncurry (unit => option unit>), ('a, 'b)) => unit = "useEffect"
@module("react")
external useEffect3: (@uncurry (unit => option unit>), ('a, 'b, 'c)) => unit = "useEffect"
@module("react")
external useEffect4: (@uncurry (unit => option unit>), ('a, 'b, 'c, 'd)) => unit =
  "useEffect"
@module("react")
external useEffect5: (@uncurry (unit => option unit>), ('a, 'b, 'c, 'd, 'e)) => unit =
  "useEffect"
@module("react")
external useEffect6: (@uncurry (unit => option unit>), ('a, 'b, 'c, 'd, 'e, 'f)) => unit =
  "useEffect"
@module("react")
external useEffect7: (
  @uncurry (unit => option unit>),
  ('a, 'b, 'c, 'd, 'e, 'f, 'g),
) => unit = "useEffect"

커링을 하지않게 만들다 보니 파라미터 개수대로 useEffect를 여러개 만들게 되었다. 그중에 나는 useEffect0를 사용하였는데 바인딩에@as(json`[]`) 라는 코드가 보인다. 비슷하게 작성하여 컴파일된 Javascript 코드를 봐보자.

type json = {@as(json`[]`) j: string}

let jsoff = {
  j: "son",
}

Js.log(jsoff.j)

---

// Generated by ReScript, PLEASE EDIT WITH CARE


console.log("son");

var jsoff = {
  "[]": "son"
};

export {
  jsoff ,
}
/*  Not a pure module */

@as는 컴파일된 Javascript 코드를 변환시켜주는 것을 볼 수 있는데 Rescript상에서는 다른 이름으로 사용하지만, 실제 Javascript를 변경시킬 수 있다. 따라서 useEffect0 는 예시 코드처럼 디펜던시가 비어있는 배열을 생성하게 된다.

코드를 진행해보자.

Mutation

 let break = ref(false)

ref라는 함수가 나온다. Rescript의 변수는 모두 immutable 하다. 그렇기에 변수를 재할당 시키려면 위와 같이 특수한 처리를 하게 된다. 또한, 실제 사용과정도 보자.

      if !break.contents {
        let todos = typeTodos(todosJson)
        setTodos(prev => prev->Js.Array2.concat(todos))
      }

변수명.contents 형식으로 직접 사용하는 것이 아니라 contents라는 내부의 값을 사용하고 변경 시에도 아래와 같이 특이하게 한다.

    Some(() => break := true)

break := true 이 부분인데 break.contents = true를 축약한 표현이다. React에서도 useRef를 사용할 때 ref의 current를 사용하게 되는데, 무언가 관련이 있거나 어디선가 존재하는 개념이 따로 있을지도 모르겠다.

거의 다 왔다. api를 불러오는 코드를 봐보자.

Api call

    let getTodos = async () => {
      open Fetch
      open Promise

      setTodos(_ => [])

      let todosJson = await fetch(url)->then(Response.json)

      if !break.contents {
        let todos = typeTodos(todosJson)
        setTodos(prev => prev->Js.Array2.concat(todos))
      }
    }

    getTodos()->ignore

Promise를 사용하고 있다. 사실 기본적으로 Rescript에서 Promise를 제공 안 한다. 위에 설치한 패키지 중에 "@ryyppy/rescript-promise"라는 패키지에서 제공한다. 좀 이상하게 느껴질 수도 있지만, 예전 Typescript 생태계가 부족하여 타입이 여기저기 흩어져있던 것과 비슷한 상황이라 보면된다. 이 경우는 라이브러리가 아니라 언어 자체에서 지원해야하는 경우라 더 중요하게 느껴지는데, 사실 저 바인딩을 만든 저자가 Rescript에 많은 커밋을 하고 있으며 저 패키지 또한 정식으로 편입될 예정인 것 같다.

Fetch도 마찬가지로 "bs-fetch"라는 패키지를 통해서 사용하게 되는데, 이 패키지는 reasonml-community라는 깃허브 조직 내에 있는 패키지다. 이건 위 패키지와 다르게 Ocaml로 작성되어 있다. 그런데, 왜 Rescript도 아니고 위에서 언급한 Bucklescript도 아닌 ReasonML인가 하면 리브랜딩 및 합병 등으로 네이밍이 레거시처럼 남아있는 상황이다. 이러한 상황들을 알고나니 좀 아찔하긴 했다. 아무튼 위 패키지들을 사용할 때 Fetch.fetch 처럼 사용할 수도 있지만, open 이라는 키워드를 사용하여 바로 내부의 함수들을 꺼내 쓸 수 있다. 그런데 -> 요건 무엇일까.

await fetch(url)->then(Response.json)

파이프다. a(b)b->a 와 같은 형태로 변형해준다. 그런데, 위 코드 대로라면 아래처럼 될 것만 같다.

await then(fecth(url), Response.json)

하지만, 실제 컴파일된 코드를 봐보자.

await fetch(url).then(Fetch.$$Response.json);

Javascript의 메소드 체이닝처럼 코드가 구성되었다. 바인딩을 확인해보자.

@bs.send external then: (t<'a>, @uncurry ('a => t<'b>)) => t<'b> = "then"

https://github.com/ryyppy/rescript-promise/blob/7eeb332ac2d6749e2b4886aac621658aa888b861/src/Promise.res#L12

파이프에서 오는 값을 첫번째로 받기 때문에 메소드 파이프로 작성해도 메소드 체이닝처럼 컴파일이 된다.

첫번째 인자로 들어오는 것은 Rescript가 data-first 규칙을 지키기 때문이다. 관심이 있다면 아래의 블로그를 확인해보자.

https://www.javierchavarri.com/data-first-and-data-last-a-comparison/

계속해서 코드를 진행해보자.

타입 변환

external typeTodos: Js.Json.t => array = "%identity"

...

      if !break.contents {
        let todos = typeTodos(todosJson)
        setTodos(prev => prev->Js.Array2.concat(todos))
      }

우리가 api에서 받아온 데이터는 아직 Json 타입이다. 하지만, 배열로 사용하고 싶다면 타입을 변환시켜주어야 한다.

external typeTodos: Js.Json.t => array = "%identity" 는 타입을 변환 시켜주는 방법인데 type escape hatch라 부른다. Typescript의 as와 비슷하다 생각하면 될 것 같다. 지양해야겠지만, Json 데이터를 특정 타입으로 변환시켜주기 위해 사용하였다. 좀 더 나은 방법이 있을 것 같지만, 현재는 모르겠다. 관련 라이브러리로는 bs-json이라는 패키지가 있다.

https://github.com/glennsl/bs-json

Array spread syntax

        setTodos(prev => prev->Js.Array2.concat(todos))

이 코드에서 왜 스프레드 문법을 쓰지 않고 concat을 썻나 생각할 수 있다. 왜냐하면 스프레드 문법을 지원안하기 때문이다. 그렇다고 치고, 대체 Js.Array2는 대체 무엇일까. 배열 관련 함수를 3가지 방법으로 부를 수 있다.

  • Belt.Array
    • 기존 BuckleScript에서 온 표준 라이브러리
  • Js.Array
    • Javascript의 배열관련 모듈이지만 함수가 data-first가 아니라 data-last로 바인딩
  • Js.Array2

그럼 Belt와 Js 모듈 차이는 무엇이냐.

For other APIs that aren't available in regular JavaScript (and thus don't exist in our Js bindings), use Belt. For example, prefer Js.Array2 over Belt.Array.

Javascript에서 지원하는 API들 이외에 Belt에서 제공하는 API가 따로 존재하는 것 같다.

https://rescript-lang.org/docs/manual/latest/api

조금 더 검색해보면, 타입 안정성이나, 퍼포먼스등 미묘한 차이들이 존재하는 것 같지만, 현 상황은 언어가 발전하면서 겪는 문제로 생각된다. 아마 둘 중 하나가 통합되거나 하지 않을까 싶다. 처음 사용하는 입장에서는 매우 당혹스럽다.

JSX

  <div>
    {todos
    ->Js.Array2.map(todo =>
      <div
        key={Belt.Int.toString(todo.id)}
        className={styles["item"]}
        onClick={e => onClickTodo(e, ~id=todo.id)}>
        <span>
          {switch todo.completed {
          | true => "✅"->React.string
          | false => "❎"->React.string
          }}
        </span>
        <span
          style={ReactDOM.Style.make(
            ~textDecoration=switch todo.completed {
            | true => "line-through"
            | false => "none"
            },
            (),
          )}>
          {todo.title->React.string}
        </span>
        <button onClick={e => onRemoveTodo(e, ~id=todo.id)}> {"삭제"->React.string} </button>
        <hr />
      </div>
    )
    ->React.array}
  </div>

태그를 사용하는 것은 비슷해보인다. 특이한점은 함수형 언어가 타입을 매우 중요시 여기는점에서 발생한다.

  {todo.title->React.string}

그냥 값을 표현해도 되지 않나 싶지만, 명확하게 React의 string 타입 엘리먼트를 만드는 함수를 사용해 표현한다. React.string(todo.title) 처럼 표현해도 동일하다.

Event handling

<button onClick={e => onRemoveTodo(e, ~id=todo.id)}> {"삭제"->React.string} </button>

React에서 다루는 방법과 동일하다. 하지만 ~id라는 부분이 신경쓰인다. Rescript에서는 함수에 Labeld arguments를 지원해서, 인자의 이름을 명시해서 파라미터로 넘겨줄 수 있다.

Inline style

        <span
          style={ReactDOM.Style.make(
            ~textDecoration=switch todo.completed {
            | true => "line-through"
            | false => "none"
            },
            (),
          )}>

ReactDOM.Style.make라는 함수를 이용하여 인라인 스타일을 지정할 수 있다. 위 의경우 text-decoration을 지정해서 넘겨주는 모습이다. React에서처럼 카멜 케이스로 사용한다. 특정 값에 따라서 다르게 스타일을 넘겨주는 것도 가능하다.

후기

매우 재밌다 언어가. 하지만, 실제로 프로젝트를 사용하기에 허들이 존재한다. 일단 함수형 언어에 대한 이해를 더해 Rescript에서 존재하는 개념들과 이전에 있던 역사를 어느정도 이해하고, 생태계를 사용하는 것이 아니라 직접 만들어 나가야 할 것이다.

생태계를 만들어갈 수 있다는 점에 매력을 느껴 참여해보려 했지만, 쉽지 않았다. 일단 Ocaml 이라는 근본 함수형 언어에 대한 공부도 필요해보인다. 그리고 기존의 Typescript에서 제공하는 많은 생태계를 놓고 와야하는 것도 쉽지 않다고 본다.

그럼에도 매우 매력적인 언어고 훌륭한 걸음을 내딛고 있다고 생각한다. 나는 함수형 언어에 대한 공부를 하고 싶어 기존의 Javascript 그리고 React를 알던 지식을 이용하여 배우려 했지만 쉽지 않다.

실제로 메인 프로젝트에서 사용하는 회사들도 있는 것 같고, 커뮤니티도 매우 활발한 것 같다. 내가 이 언어를 메인으로 사용할 것 같지는 않지만, 시야를 넓게 해주었다. 이러한 새로운 시도를 하는 언어들이 좀 더 많아지고 잘 되었으면 좋겠다.