프론트엔드 개발 ReasonML이라 좋았던 점

ReasonML의 기본적인 정보는 그린랩스 기술 블로그의 자바스크립트 개발자를 위한 ReasonML을 참고해주세요!

저는 이전까지 자바스크립트와 타입스크립트 환경의 리액트 프론트엔드 프로젝트를 진행했었습니다. 그러나 그린랩스에선 프론트엔드 프로젝트에 ReasonML과 ReasonReact를 사용합니다. ReasonML은 강력한 정적 타입 시스템을 장착한 함수형 언어인 OCaml에 뿌리를 둔 ReScript 컴파일러에 의해 자바스크립트 코드로 변환되고, 이는 브라우저나 Node.js 기반에서 실행됩니다.

이 포스팅에서는 ReasonML 로 프로젝트를 진행하며 느낀 장점을 이야기합니다. 아마 제가 기존까지 사용했던 자바스크립트와 타입스크립트 환경과의 비교 과정일 것입니다.

1. 자바스크립트 import, export 구문과 ReasonML의 모듈 레졸루션

첫째로 말하고 싶은 장점은 모듈을 다루는 ReasonML/OCaml의 방식입니다.

프로젝트의 성장에 따라 새로 탄생하는 코드도 있고, 더이상 사용하지 않는 코드도 생깁니다. 만일 코드를 주기적으로 정리하지 않는다면, 어떤 모듈은 import 된 채 아무도 찾지 않는 유령처럼 프로덕션 코드 더미를 떠돕니다. 이렇게 되면 번들 사이즈가 불필요하게 증가합니다. 또한 사용처 없이 떠도는 import는 코드 히스토리를 잘 알지 못하면 지워야할지 남겨야할지, 나중에 그 코드를 확인하는 개발자는 혼동스러울 겁니다.

제가 경험한 자바스크립트나 타입스크립트 프론트엔드 프로젝트 엔트리 파일의 구조는 회사나 프로젝트마다 달랐습니다. 그러나 초기 환경구성작업은 대부분 비슷한 일을 필요로 합니다. 브라우저 지원 범위나 서비스의 크기에 따라 브라우저 폴리필 삽입, 글로벌 스타일 삽입, 기능에 따라 라우팅 설정을합니다. 또 리액트 프로젝트라면 각종 프로바이더를 엔트리 컴포넌트에서 불러오는 경우도 있었습니다.

환경 구성을 하기위해 모듈을 불러야 하고, 모듈은 import, export 구문을 사용하여 부릅니다. 물론 IDE 종류에 따라 import 구문이 자동 완성되는 경우도 있습니다.

리덕스같은 상태관리 도구를 프로젝트에 추가하는 경우를 생각해보겠습니다. 리덕스의 액션과 리듀서, 비동기 처리를 위해 미들웨어를 추가하고 배포 가능한 수준까지 만들기위해 여러 패키지와 import 구문을 계속 추가해야합니다. 만일 타입스크립트를 사용하기로 했다면 작성해야 할 코드의 양은 더 늘어납니다. immer-reducertypesafe-actions처럼 타입 안정성을 해치지 않으면서 코드의 양을 줄여주는 라이브러리들이 있지만 역설적이게도 이에 따른 import 구문은 더 늘어납니다.


끝나지 않는 export

그래서 누군가는 import 구문을 행사(Ceremony) 코드로 분류하기도 합니다. 행사 코드는 프로그램의 핵심 동작에 직접적인 영향을 끼치지 않지만 언어와 프레임워크에 의해서 작성이 강제되는 코드를 뜻합니다.

Ceremony vs. Essence Revisited
Plain JavaScript vs Svelte vs React vs Vue의 Ceremony 코드 비율 비교

ReasonML에서는 import 구문을 쓰지 않아도 컴파일러가 알아서 해당하는 모듈을 찾아줍니다.

ReasonML에서 모듈 레졸루션은 어떻게 동작하나요?
Reason/OCaml doesn't require you to write any import; modules being referred to in the file are automatically searched in the project. Specifically, a module Hello asks the compiler to look for the file hello.re or hello.ml (and their corresponding interface file, hello.rei or hello.mli, if available).

Reason/OCaml은 import 구문이 필요없습니다. 모듈은 프로젝트 안에서 자동적으로 찾아집니다. Hello 라는 모듈은 컴파일러를 통해 Hello.re 또는 Hello.ml 파일을 찾습니다. (만약 그에 대응하는 인터페이스 파일인 Hello.rei, Hello.mli이 존재한다면 이것도 같이 찾습니다)

ReasonML을 사용해 Farm.re 컴포넌트를 만들어보겠습니다.

1[@react.component]
2let make = () => {
3 <>
4 <Farmer.Novice>
5 <Greenhouse> <Broccoli /> </Greenhouse>
6 <Greenhouse> <Pumpkin /> </Greenhouse>
7 </Farmer.Novice>
8 <Farmer.Expert>
9 <Greenhouse> <Potato /> </Greenhouse>
10 <Greenhouse> <Pumpkin /> </Greenhouse>
11 <Greenhouse.GableRoof> <Pumpkin /> </Greenhouse.GableRoof>
12 <Greenhouse.GableRoof> <Pumpkin /> </Greenhouse.GableRoof>
13 <Greenhouse.GothicArch> <Broccoli /> </Greenhouse.GothicArch>
14 </Farmer.Expert>
15 </>;
16};

어떠한 import 구문도 사용하지 않았지만 정상 컴파일됩니다. 결과적으로 import로 인한 Ceremony 코드를 줄일 수 있습니다. ReasonML로 된 파일을 열면, 긴 import 구문 대신, 타입 선언과 비즈니스 로직이 제일 위에 보입니다.

2. es-lint, prettier 설정이 필요없는 자체 포매터

ReasonML 포매터(refmt)의 장점은 모듈 레졸루션이 가지는 장점처럼 Ceremony 코드를 줄이는 효과도 있고 다른 장점도 있습니다.

자바스크립트와 타입스크립트로 프론트엔드 작업을 했을 때, 린팅 및 포매팅 자동화는 협업의 생산성을 높여줍니다. 하지만 유용하게 사용하기 위해서는 설치하는 패키지가 적지 않습니다.

린팅 규칙 설정은 개인 성향이나 회사 성향따라 조금씩 다릅니다. 그래서 특정 벤더의 린팅 규칙을 모태로 확장하거나 축소합니다. 그리고 변경되는 개별 룰에 대해서는 팀의 동의도 얻어야 합니다. 여기다 타입스크립트 규칙이나 리액트만 적용되는 규칙을 추가하고 또 웹 접근성 관련 린팅 규칙을 추가하다보면 .eslintrc 파일이 결국 프로젝트 라이프사이클에 포함됩니다.

ReasonML 에서는 refmt 라는 컴파일러 차원에서 지원해주는 포매터가 있습니다. 또한 ReasonML은 자바스크립트나 타입스크립트가 아니기 때문에 eslint 관련 패키지 설치와 관리가 전혀 필요 없습니다.

ReasonML의 핵심 기여자 중 한 명인 Cheng Lou는 refmt에 대해 몇가지 장점을 이야기합니다.

Cool Things Reason Formatter Does - 리즌 포매터가 하는 멋진 일들
Think of it as Prettier or Gofmt but for Reason. - 리즌을 위한 Prettier 또는 Gofmt(Golang 포매터)

  • 코드를 더 읽기 쉽게 시맨틱하게 변경
  • 변경 전과 후의 추상 구문 트리는 동일
  • 공짜 커리(Curring)를 🍛 얻을 수 있음
  • 자바스크립트 interop 때 코드를 알아서 구문 변환
  • 들여쓰기 자동 처리

여기서 Refmt (Reformat) 를 바로 테스트해볼 수 있습니다

3. 타입스크립트보다 읽기 쉬운 컴파일러 오류 메시지

타입스크립트는 정적 타이핑을 가능하게 해줍니다. 덕분에 바닐라 자바스크립트보다 협업하기 좋습니다. 현재 타입스크립트 생태계는 활발하고 문서도 많고 예제도 많기 때문에 타입스크립트로 처음 프론트엔드 개발을 시작하는 것이 어렵지 않습니다.

저도 이전까지 타입스크립트에 타입 추론과 타입 체킹에 감탄했고 그 생태계에 여전히 지지를 보냅니다만 아쉬운 점 하나를 말해보겠습니다.

타입스크립트는 컴파일 오류 메시지가 읽기 어렵습니다. 친절한 거 같은데 너무 과도해서 무엇이 잘못됐는지 헛갈릴 때가 많습니다. 특히 ts(2322), ts(2345) 등이 그랬습니다. 그럼 오류를 안나오는 코드를 만들면 될텐데 사실 그게 쉽지는 않습니다. 🥲

특히 계층 구조 객체 특정 필드의 타입 지정 오류가 생겼을 때, 오류 메시지가 너무 장황하고 길어 정확하게 어떤 부분이 오류인지 찾기 힘들었습니다.


자세하고 읽기 어려운 타입스크립트 오류 메시지

그러나 ReasonML 컴파일러는 오류 메시지는 타입스크립트 오류 메시지보다는 적당히 😅 구체적입니다. 때문에 오류를 찾을 때 더 짧은 시간이 걸렸습니다.

4. 배리언트(Variant)와 패턴 매칭으로 논리적인 코드 생산

잘 돌아가는 코드를 일부러 복잡하게 만드는 일은 누구도 원하지 않겠지만, 조건 렌더링이 필요한 상황은 종종 찾아옵니다.

조건의 조건, 조건의 조건의 조건, 조건의 조건의 조건의 조건에 따라 각기 다른 컴포넌트가 렌더링 되어야 할 때도 있습니다. 이 경우 코드를 분리하거나 공통 부분을 추출해 리팩토링 하면 좋을 것입니다.

ReasonML은 배리언트를 이용해 논리 전개를 직관적으로 작성할 수 있습니다. 배리언트는 다른 언어의 enums과 비슷하지만 선택적으로 데이터를 포함시키는 것이 가능합니다. 아래는 로딩, 로드 됨, 오류 3가지 상태에 따라 렌더링하는 간단한 예제입니다.

1type state =
2 | Loading
3 | Loaded(detail)
4 | Error(error);
5...
6 id->doSomething->Js.Promise.then_((result) => {
7 setData(_prevState => Loaded(result));
8 })
9 |> Js.Promise.catch((error) => {
10 setData(_prevState => Error(error));
11 });
12 switch (data) {
13 | Loading => <Loading />
14 | Loaded(contents) => <Detail contents>
15 | Error(error) => <Error error/>;
16 };

GraphQL 쿼리를 포함한 코드도 한 번 보겠습니다. 패턴 매칭을 통해 각 상태를 렌더링합니다.

1let farm = Query.MyFarm.use({id: int_of_string(id)});
2switch (farm) {
3 | {loading: true, data: None} => <Spinner />
4 | {error: Some(error)} => <Error error=error/>
5 | {data: None, error: None, loading: false} => <Error />
6 | {data: Some({myFarm})} =>
7 <Farm key={string_of_int(myFarm.id)} farm=myFarm />
8}

각 패턴마다 무엇을, 왜 렌더링하는지 쉽게 읽힙니다. 배리언트와 패턴매칭은 상태 관련 렌더링 코드 쓰기에 효과적입니다.

패턴매칭과 배리언트의 또한가지 장점은 서술되지 않은 케이스에 대해서는 Warning으로 알려줘서 모든 case에 대해 렌더링 코드를 작성하게끔 도와줍니다.

처음 예시의 코드에서 Loaded 케이스를 제외해보겠습니다.

1type state =
2 | Loading
3 | Loaded(detail)
4 | Error(error);
5...
6switch (data) {
7 | Loading => <Loading />
8 | Error(error) => <Error error/>;
9};

이러면 나머지 케이스(즉, 여기서는 Loaded) 대해 렌더링 코드를 작성하지 않았음을 명시적으로 알려줍니다.

1Warning 8: You forgot to handle a possible case here, for example:
2 Loaded _

배리언트와 패턴매칭은 코드로 쓰여진 논리가 허술하지 않도록 도와줍니다.

배리언트 공식 문서
패턴매칭/구조분해 공식 문서

5. 컴파일 시점에 CSS 타입 체크

올바른 CSS를 작성했는지 확인하기 위해 스타일 관련 린팅 도구를 설치해본 적 있으신가요? State of CSS 2020 에서는 설문에 참여한 43.2%75%의 개발자가 Stylelint를 사용한다고 답변했습니다.

자바스크립트 생태계에서 CSS를 작성하는 방식은 여러가지가 있습니다. 그린랩스는 CSS-in-CSS와 CSS-in-JS를 상황에 따라 결정해 사용합니다. 여기서는 CSS-in-JS 자체에 대해서는 깊게 다루지 않겠습니다. (위의 State of CSS 2020을 참고해주세요!)

저는 프론트엔드 개발자로서 스타일링 버그도 프로젝트에 따라 서비스에 치명적인 상처를 줄 수 있는 부분이라 생각합니다. 개발자가 단위를 혼동하거나 쓸 수 없는 속성을 넣을 경우, 기대했던 디자인이 나오지 않거나 어쩌면 기능이 오동작하기도 합니다. 때문에 간단한 스타일링 실수로도 나쁜 사용자 경험을 만들수 있습니다.

ReasonML에서는 bs-css 패키지를 통해 컴파일 시점에 CSS 체크가 가능합니다. bs-css의 사용 방법은 CSS-in-JS 라이브러리로 유명한 Emotion인터페이스와 유사합니다. (Emotion을 ReasonML에서 쓸 수 있도록 바인딩했기 때문입니다. 바인딩에 대해서는 다른 포스팅에서 다루겠습니다)

1module Styles = {
2 open Css;
3 let card = style([
4 display(flexBox),
5 flexDirection(column),
6 alignItems(stretch),
7 backgroundColor(white),
8 boxShadow(Shadow.box(~y=px(3), ~blur=px(5), rgba(0, 0, 0, 0.3))),
9 ]);
10 let actionButton = disabled =>
11 style([
12 background(disabled ? darkgray : white),
13 color(black),
14 border(px(1), solid, black),
15 borderRadius(px(3)),
16 ])
17};
18<div className=Styles.card>
19 <h1 className=Styles.title> "Hello"->React.string </h1>
20 <button className=Styles.actionButton(false) />
21</div>

스타일링과 관계 된 패키지 중 또 하나 소개하고 싶은 라이브러리가 있습니다. ReasonML 문법 안에서 styled-component 형식으로 스타일 작성을 도와주는 styled-ppx 입니다.

styled는 느낌적인 느낌으로 알겠는데 ppx는 뭘까요?

ReasonML의 부모인 OCaml에는 **PPX(PreProcessor eXtensions)**라는 기능이 있습니다. PPX를 통해 문법적 확장이 가능합니다. 마치 새로운 문법이 만들어진 것처럼 구문을 작성할 수 있습니다.

다음은 styled-ppx를 이용해 컴포넌트를 작성한 예시입니다.

1module Title = [%styled.h1
2 {|
3 font-size: 1.5rem;
4 font-weight: 700;
5 |}
6];
7module Description = [%styled.p
8 {|
9 color: #76798A;
10 |}
11];
12<>
13 <Title> {j|Like-Styled-Component|j}->React.string </Title>
14 <Description> {j|How About This?|j}->React.string </Description>
15</>

PPX에 대한 설명은 쉽지 않기 때문에 다른 포스팅에서 더 깊게 다루겠습니다. 😅 궁금하신 분은 밑의 링크를 참고해주세요!

What is PPX?

중요한 것은 bs-css를 사용하든 styled-ppx를 사용하든 컴파일 시점에 CSS 프로퍼티에대해 타입체킹 받을 수 있다는 사실입니다. 각 프로퍼티에 맞지 않는 타입을 넣으면 컴파일이 안 됩니다! 👍  

혹시 스타일까지 타입체킹을 받는 건 가혹하다 생각하시나요? 😉 이 부분은 팀마다 다른 결론이 나올 것 같습니다만 그린랩스 웹 개발팀에서는 bs-css의 사용비중을 높게 가져가고 있습니다!

6. GraphQL 사용할 때 전방위적 타입 체크

그린랩스 백엔드와 프론트엔드팀은 GraphQL 스키마를 공통으로 사용합니다. 프론트엔드에서는 GraphQL에 대한 타입체킹 및 ReasonML 문법 확장을 위해 graphql-ppx를 사용합니다.

이런 스키마가 있습니다.

1...
2type MyFarm {
3 id: Int!
4 name: String!
5 area: Float!
6 crop: Crop!
7}
8...

ReasonML 설정 파일인 bsconfig.json에 graphql-ppx 의존성을 추가하면 위 스키마로 Fragment와 Query를 ReasonML 문법으로 작성할 수 있습니다.

1[%graphql
2{|
3 fragment Farm on MyFarm {
4 id
5 name
6 crop {
7 ...Crop
8 }
9 }
10|}
11];
12module MyFarm = [%graphql
13 {|
14 query myFarm($id:Int!){
15 myFarm(id:$id){
16 ...Farm
17 }
18 }
19 |}
20];

위 ReasonML 코드를 작성하면 graphql-ppx가 MyFarm에 대해 타입을 자동 생성합니다. (이 코드를 개발자가 작성할 필요 없습니다)

1type t = {
2 __typename: string,
3 id: int,
4 name: string,
5 crop: Crop.t
6}

그럼 컴파일러는 위 타입을 기준으로 타입검사를 해줍니다. IDE에서 자동완성과 타입힌트 역시 추가적으로 누릴 수 있습니다. 덕분에 쿼리 또는 뮤테이션을 올바르게 작성했는지 알려주고, 사용처에서 올바른 타입을 사용하는지 알려줍니다.

프로덕션 스키마의 크기는 예제와 비교할 수 없을만큼 늘어날겁니다. 그럴 때 주의하지 않으면 놓칠 수 있는 부분을 ReasonML + graphql-ppx의 도움으로 컴파일 시점에 방지할 수 있습니다.

7. 그밖에 장점이라 느낀 것

  • 비슷한 규모의 타입스크립트 프로젝트보다 빠른 빌드 속도를 체감했습니다. 현재 Github 액션을 통해 빌드 및 배포를하고 있습니다. ReasonML 빌드 시간은 11초가 걸립니다. Webpack 빌드를 포함하면 1분 30초 이내로 모든 빌드가 완료됩니다.^1
  • 타입스크립트로 프로젝트를 진행했을 때보다 타입정의를 덜 했지만, 더 타입추론이 잘 된다고 느꼈습니다. 때문에 인터페이스나 타입작성을 많이 할 필요가 없었습니다.[^2]

정리

위에 나열한 장점 말고도 다른 팀원 분이 올려주신 포스팅에도 장점이 많이 설명 되어있습니다. 시간이 괜찮으시다면 읽어보시는 걸 추천드립니다. ReasonML의 매력을 좀 더 느낄 수 있습니다.

물론 이 포스팅에서는 장점만 이야기했습니다. 그러나 IDE 지원 미비, 불편한 유니코드 사용 방법, 아직 부족한 공식문서 등은 단점입니다. 단점은 다른 포스팅에서 더 자세히 다룰 예정입니다.

그리고 개발 환경구성을 타입스크립트로 구성할 때보다 금방할 수 있었습니다. 불필요한 패키지 설정과 트랜스파일 설정이 필요없기 때문입니다.

불필요한 패키지 설치와 린팅 설정, import 구문 사용은 결국 Ceremony 코드에 가깝습니다. 그린랩스 웹 개발팀은 ReasonML을 사용해 프론트엔드 개발을 하며 많은 Ceremony 코드를 줄였고 핵심 로직 개발에 더 집중할 수 있었습니다.

저희와 ReasonML에 대해 같이 이야기 나누실 분을 애타게 기다리고 있습니다. 😃🐫

Gravatar for yousleepwhen@gmail.com
윤정식프론트엔드 개발자
2020. 12. 15.


2
5 examples with TypeScript, Flow and ReasonML


Thanks to

프리뷰 해주신 change.my.uniform 채널 분들께 감사드립니다.


추천 콘텐츠