ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Bun 1.0 릴리즈 후, 서비스 개발에 문제 없을지 리서치 해본 결과
    개발 블로깅/Web EcoSystem 2023. 10. 4. 11:43

     

    최근에 1.0 Release가 되었다는 소식에 다시 한번 리서치를 해보았다.
    작년쯤에도 한번 리서치를 해보면서 그땐 pacakge Manager, RunTime까지만 제공하는 걸로 알고 있었는데, 그 당시에는 발견할 수 없었던 Test Runner, Builder 등 웹 개발에 필요한 여러 툴도 Bun에서 직접 제공하는 것을 알게 되었다.

    Bun이 자체적으로 Yarn, pnpm보다 몇십배 빠르다고 주장하니, 만약 요게 맞다면 우리는 CI/CD 환경에서 돌아가는 Install, build 시간을 굉장히 크게 단축 시키고 빠른 배포가 가능하게 될 것이다.

    그래서 이번에 Bun이 현재 Production 서비스 개발에 이용함에 문제가 없을지 중점으로 한번 리서치해본 내용을 정리해 보려고 한다.

     

     

    해당 글에서는 Bun의 사용법에 대해서는 다루지 않습니다.
    Bun을  직접 써보며 Bun 환경에 대한 이해도를 위해  주요하다고 생각되는 내용들을 다룹니다.

     

     

    Bun 환경 구성

    Bun은 대부분 Zig 언어로 구현되어 있으며, C++로 구현되어있는 Webkit과 Javascript Core(Javascript Engine)를 포함하고 있다.

    재밌는 부분이 Javascript Engine을 V8이 아닌 Javascript Core를 사용한다는 것인데,
    왜 Chrome과 Node.js 환경에서 주력으로 사용되는, V8이 아닌 Javascript Core를 선택한 것일까?

    찾아보니, Nexus.js 프레임워크에도 Javascript Core를 이용하였는데, 해당 글에서 답을 얻을 수 있었다.

     

    Why use JavaScriptCore and not v8 like nodejs does? · Issue #6 · voodooattack/nexusjs

     

    github.com

     

    V8엔진은 브라우저 내 여러 스레드 중 하나라도 자바스크립트 컨텍스트를 호출해서 실행시키면 가상머신 자체를 Lock 하는 형태로 디자인 되어있다. 이로 인해 자바스크립트 컨텍스트들이 클로저나 mutable한 변수 등 서로 공유되는 요소가 없더라도 여러 스레드가 동시에 스크립트 코드를 실행시키는 병렬성을 가질 수 없다.

    JavascriptCore는 Lock이 가상머신이 아닌, 각 자바스크립트 컨텍스트가 가지고 있는 형태로 디자인이 되어있기 때문에, 여러 스레드 병렬 처리가 가능하다. 각 스레드가 연관없는 코드들끼리 수행시에는 바로 병렬로 처리되면서, 여러 스레드가 하나의 컨텍스트에 접근하려고 하면 해당 컨텍스트에 Lock을 걸면서 스레드 하나씩 접근하게 된다.

     

    Package Install

    우선 Bun이 어떤 원리로 다른 툴들보다 빠른 속도를 가질 수 있는 것인지도 같이 확인해보자. Bun의 공식 홈페이지에 의하면 yarn보다 33배, pnpm보다 17배 빠르다고 주장하는데, 이는 어떤 원리일까?

    우선 Bun이 package.json에 있는 모듈들을 빠르게 설치하는 원리는 다음과 같다.

    의존성 분석

    Bun은 프로젝트의 루트 디렉토리에 있는 'package.json'파일을 읽어서 프로젝트의 의존성(dependeices)목록과 버전 정보를 파악한다. 이 파일은 프로젝트에서 사용하는 모든 외부 라이브러리와 모듈에 대한 정보를 포함하고 있다.
    하위 디펜던시에 의존되고 있는 수 많은 모듈을 통해 의존성 그래프를 그린다.

     

    의존성 해결

    Bun은 pakcage.json 파일에 명시된 의존성들을 해결한다. 여기서 의존성을 해결한다는 뜻은, 각 하위 디펜던시 내에서 정상적으로 돌아가기 위해 설치가 되어야 하는 적절한 버전을 찾아야 하는데, 서로 엉켜있는 의존성으로 인해 정확한 버전을 찾는데 많은 시간이 들어갈 수도 있고, 최악에는 정확한 버전을 찾지 못하는 경우도 생길 수도 있다. 이 처럼 문제 없이 호환이 되는 버전을 찾아 의존성 그래프를 그리는 것을 의존성 해결이라고 한다.

     

    패키지 설치

    의존성이 해결이 되면, Bun은 선택된 모듈과 라이브러리를 다운로드하면서, 프로젝트 디렉토리 내 node_mldules 폴더에 설치한다. 

     

    캐시와 병렬 설치

    Bun은 이전에 한번이라도 설치했던 패키지를 global Cache 공간에 저장 후, 향후 현재 로컬의 어느 프로젝트이던간에 다시 한번더 설치를 시도하면 Global Cache 공간에서 바로 가져와서 설치를 하게된다. 또한 병렬 설치를 지원하여 의존성이 많은 프로젝트의 경우에도 빠른 설치를 가능하게 한다.

     

    Bun의 설치과정은 위 처럼 4가지 순서로 이루어지는데, 사실 이러한 원리는 Yarn, pnpm 등 다른 PackageManager도 동일한 방식이지만 Bun은 특히 이 중에 빠른 실행 속도캐싱 기능을 강조하고 있다.

     

    Global cache

    한번 설치된 패키지는 로컬의 `~/.bun/install/cache` 디렉토리에 바이너리 파일로 저장이 된 것을 확인할 수 있다. 이를 통해 로컬에서 해당 패키지를 재 Install할때마다 cache 디렉토리를 먼저 확인하여 설치하므로 굉장히 빠른 인스톨이 가능하다.

    하지만 이는 동일한 로컬에서의 수행에서만 유의한 cache이므로, 만약 항시 새롭게 세팅되는 CI/CD 환경의 경우에는 큰 의미가 없는 기능이 될 것 같다.

    패키지 설치 시 Global Cache로 빠르게 다운로드를 한다고 해서, Bun은 여전히 node_modules로 복사하는 과정이 필요한데, 실제로 install할 패키지 파일 내용들이 꽤 많기 때문에 시간이 꽤 걸릴 수 있음에도, 여전히 굉장히 빠르다. 이러한 빠른 복사가 가능한 이유는 빠른 System Call을 이용하게 때문인데, Linux는 hardllinks, MacOS에서는 clonefile을 이용한다.

     

    bun.lockb

    bun install하면 생성되는 파일로, yarn.lock.json이나 package.lock.json과 같이 의존성 디펜던시 정보들이 담겨있는 파일이다. 
    차이점은 이 파일은 바이너리 파일로 되어 있어서 직접 해당 내용을 확인해볼 수는 없다.

    대신 해당 파일을 yarn.lock 형식으로 볼 수 있는 기능을 제공한다. 이를 통해 실제 내부 의존성 디펜던시의 정보들을 확인해볼 수 있다.

    $ bun bun.lockb

     

    bun.lockb 파일에는 아래와 같은 정보들이 들어있다.

    • 설치할 패키지와 이에 의존하는 하위 디펜던시 정보.
    • 디펜던시가 해결된 패키지 정보.
    • 디펜던시 각 버전.
    • MetaData.
    • 호이스팅 된 Install 순서.

    이러한 정보들을 담고있는 bun.lockb이 어떻게 빠를 수 있는걸까?

    bun.lockb에서는 위 정보들을 Linear(선형)배열 형태로 저장하여 사용한다.
    "선형 배열 형태로 담는다"는 것은 데이터들을 일렬로 연결하여 저장하는 방식을 말한다. 즉, 데이터를 연속적으로 저장하며 패키지들이 auto-incrementing되는 정수 ID 혹은 패키지 이름의 해시 값을 사용하여 참조하게 된다. (해시 값은 8자보다 긴 중복되지 않는 String 값을 사용한다.)

    우선 이러한 방식을 쓰는 이유가, 이처럼 배열 형태를 이용하면, 참조하는 곳에서는 인덱스만 알면 원하는 곳을 직접 접근이 가능하기 때문에 빠르게 원하는 곳을 참조할 수 있고 파일의 구조를 단순하게 유지하고 데이터를 효율적으로 검색하고 수정할 수 있다. 하지만 이에 대한 단점은 아무래도 정렬되지 않은 순차적 저장 방식이다보니 최적화된 형태로 저장이 되어있지 않아 메모리 낭비가 심해질 수 있다.

     

    Scope Dependency

    특정 Scope를 지정해서 사용하는 디펜던시의 경우 npmrc, yarnrc 등을 선언하여 사용하는데, bun에서는 아직 npmrc 파일 읽기가 호환되지 않는다. (참고)

    대신, bunfig.toml파일을 통해 스코스 지정을 할 수 있다.

    [install.scopes]
    # registry as string
    "@myorg1" = "https://username:password@registry.myorg.com/"
    
    # registry with username/password
    # you can reference environment variables
    "@myorg2" = { username = "myusername", password = "$NPM_PASS", url = "https://registry.myorg.com/" }
    
    # registry with token
    "@myorg3" = { token = "$npm_token", url = "https://registry.myorg.com/" }

     

     

    Workspace

    Yarn Workspace와 비슷하게, Bun에서도 Monorepo 환경을 구성할 수 있는 Workspace 기능을 제공한다.
    Bun Workspace에서도 각기 package.json을 보유하고 있는 프로젝트 단위를 "패키지" 라는 용어를 쓴다. (Yarn과 동일하다.)

    tree
    <root>
    ├── README.md
    ├── bun.lockb
    ├── package.json
    ├── tsconfig.json
    └── packages
        ├── pkg-a
        │   ├── index.ts
        │   ├── package.json
        │   └── tsconfig.json
        ├── pkg-b
        │   ├── index.ts
        │   ├── package.json
        │   └── tsconfig.json
        └── pkg-c
            ├── index.ts
            ├── package.json
            └── tsconfig.json

     

    Bun 역시 같은 Workspace 내에서 의존성을 가진 디펜던시를 가지고 있는 경우, npm Registry를 통한 다운로드를 하지 않고도 심링크를 통해 바로 로컬 내에서 사용함으로써 불필요한 인스톨 과정을 없앨 수 있다. 

    여러 패키지에서 동일한 디펜던시를 가지고 있는 경우, 중복 Install을 하지 않고 최상단 node_modules에서 호이스팅해서 디펜던시 용량을 줄일 수 있다.

    전체적으로는 Yarn Workspace와 동일하게 기능이 지원되나, 아직 보완되지 않은 한 가지 단점이 있다면, Workspace 내 특정 패키지에 의존성을 설치할 때, Yarn workspace에서는 `yarn workspace {package name}`을 통해서 지정된 패키지에만 cli를 수행할 수 있도록 할 수 있었는데, Bun에서는 아직 그러한 커맨드가 지원이 되지 않는다.
    따라서 특정 패키지의 디펜던시 설치가 필요로 한 경우, 어쩔 수 없이 해당 디렉토리까지 이동 후 직접 커맨드를 수행해야 한다.

    $ cd packages/package-a
    $ bun add react

     

     

     

    Configuring a monorepo using workspaces | Bun Examples

    Bun's package manager supports npm "workspaces". This allows you to split a codebase into multiple distinct "packages" that live in the same repository, can depend on each other, and (when possible) share a node_modules directory. Clone this sample project

    bun.sh

    

     

    Server Side 호환성 문제

    우선 Bun 환경에 아직 Node환경에 대한 호환성이 높지가 않다.
    Bun이 Node와 다른 환경임에도 굳이 적합해야 하는 이유가 있을까? 라는 의문점이 든다면... 이미 완성도가 높은 수많은 라이브러리들이 Node환경의 의존되서 만들어진게 많기 때문이다. 따라서 Node라는 환경과 호환성을 무시할 수 없다.

     

    Node.js compatibility – Runtime | Bun Docs

    Bun aims for complete Node.js API compatibility. Most npm packages intended for Node.js environments will work with Bun out of the box; the best way to know for certain is to try it.This page is updated regularly to reflect compatibility status of the late

    bun.sh

     

    위 링크에서 확인해보면, 23.09.30일 기준으론 아직 node:cluster, node:fs, node:chile_process 등 호환되지 않는 것들이 꽤 많은데, 이러한 요소들로 인해 웹 서버 환경에서의 모니터링 모듈 등은 아직도 Bun에서는 정상적으로 동작하지 않았다.

    Next.js 프레임워크에서 Bun을 이용하여 Sentry/nextjs를 설치 후 가동시켜보았으나, 빌드 및 실행할 때 fs, chile_process not found 에러가 뜨면서 정상적으로 동작하지 않았다.
    별도로 @Sentry/bun이 존재하긴 하는데, 직접 설치하면 여전히 내부적으로 @sentry/node가 설치된다. 이는 ServerSide 단의 Sentry 모니터링이 여전히 Node 환경에 굉장히 의존적인 상태여서 Bun에서 Node의 호환성이 더 높아지지 않는 한 아직은 사용하는데는 한계가 있을 것으로 보인다. 아마 APM 같은 Server Side 모니터링 모듈들은 전부 정상 동작하지 않을 것으로 예상된다.

     

    추가로 Sentry의 경우에는 모듈 내 build 완료될 때마다 sourceMap upload하는 cli 모듈을 설치하는 등 postInstall 디펜던시도 포함되어 있는데, Bun에서는 Script LifeCycle을 실행하지 않는 문제가 있다. 

    여기서 말한 Script LifeCycle이란, preInstall, postInstall 등, install 하기 전의 액션을 하는 행위인데, Bun에서는 이러한 행위가 보안에 위협적인 요소라고 판단하여 실행시키지 않는 것 같다.

    대신 Script LifeCycle 기능을 대체하는 trustedDependencies라는 프로퍼티를 이용해서 LifeCycle을 실행시킬 수 있다.

    {
      "name": "my-app",
      "version": "1.0.0",
      "trustedDependencies": ["my-trusted-package"]
    }

    대신, 이 또한 Node.js 형식과는 맞지 않는 방식이므로, 기존의 Node.js 방식인 LifeCycle 형태로 구현된 모듈이라면 모두 호환이 안 될 것이다.

     

    Client Side 단 애플리케이션 개발 역시 불완전한 모습

    Server Side의 주요 모니터링 디펜던시가 아직 정상적이지 않았지만, Client Side Application 단 개발은 문제가 없을지 가벼운 테스트를 해보았지만, 아래의 아쉬운 부분들로 인해 아직은 사용하기엔 쉽지 않아보인다.

     

    React  Lazy관련 빌드가 안정적이지 않다 (23.10.03일 기준)

    import * as React from "react";
    import { RenderCounter } from "./RenderCounter";
    
    const Button  = React.lazy(() => import('./Button'));
    
    export function App() {
      return (
        <RenderCounter name="App">
          <div className="AppRoot">
            <h1>This is the root element</h1>
    
            <Button>Click</Button>
          </div>
        </RenderCounter>
      );
    }

     

    위와 같은 코드를 빌드 한다면, Button 컴포넌트의 코드가 알아서 코드 스플리팅 처리가 되면서 index.js 파일에서 분리된 별도의 js 파일이 생성이 되고, runTime에서 해당 컴포넌트가 필요 시 알아서 로드되어 렌더링이 되어야 한다.

    Bun 역시 위 코드를 Build를 하면, 빌드된 코드에서 알아서 Button.js 파일이 분리되어 생성되면서 정상적으로 번들이 되긴 하지만, 아래와 같이 번들된 코드에서 스플리팅한 코드를 import하는 구문에서 경로를 이상하게 잡는 에러가 발생한다.

     

    './component/Button.js'가 되어야 하는데, 'coponent/Button.js'로 번들이 되어서 JS 에러가 발생한다. 경로를 수동으로 제대로 수정하면 정상적으로 import가 된다. 

     

    아직은 완전하지 않은 react-router 환경

    react 자체에서 라우팅 처리로 많이 사용하는 react-router-dom을 bun으로 컴파일 했을 때도 제대로 라우팅이 될지 확인해 보았다.

    우선 실제 라우팅 말고, bun dev 같은거로 로컬에는 문제가 없는지 확인해보고 싶었다. 그러나 bun dev 같은 명령어가 존재하지 않는다.
    (github bun 관련 프로젝트들을 보면 1.0 이전에는 있었던 듯 한데, 지금은 사라진 상태처럼 보인다.)

    그래서 직접 Bun으로 빌드 한 뒤 bunx serve로 실행을 시켜보았으나, 정상적으로 라우팅이 되지 않았다. 

    빌드 자체에 문제가 있는가 확인을 해보았으나, 빌드된 청크파일 자체에는 라우팅 관련 코드가 잘 들어가 있는 듯 보여 빌드하는데에는 문제가 없었나보다. 그러나 실제로 해당 파일을 S3 파일로 올리고 정적 호스팅 처리 후 실행시켜보면 라우팅 컴포넌트를 찾지 못한다.

    S3를 올려 정적 호스팅 웹 페이지로 실행을 시켰을 때는 라우팅이 잘 되는 것 같다. 
    그러면 개발 환경에서만 파일 시스템 구조에서 라우팅을 할 수 있는 방법을 잘 찾으면 될 것 같다.

     

    https://bun.sh/docs/api/file-system-router

     

    FileSystemRouter – API | Bun Docs

    Bun provides a fast API for resolving routes against file-system paths. This API is primarily intended for library authors. At the moment only Next.js-style file-system routing is supported, but other styles may be added in the future.The FileSystemRouter

    bun.sh

     

    HTML과 Webpack을 묶어서 HotReload 할 수있는 플러그인처럼, Bun 전용 HotReload 관련 플러그인은 아직 없는 것 같지만,  Bun 자체에서 File System 기반으로 라우팅 해주는 기능을 제공해 주기 때문에 이를 이용해서 bun dev와 같은 환경을 세팅해보려고 했으나, 해당 파일 시스템에서는 jsx, tsx 파일을 트랜스파일한 뒤 제대로 화면에 보여주긴 하지만, React 스크립트 같은 코드는 실행시키지 않는 것 같다. File System Router를 통해 내려받은 페이지에서는 useEffect 같은 함수가 동작하지 않는다.

     

    결론, 아직은 불완전하지만,  조금 더 확인해 봐야할 것 같다.

    23.10.04 기준, 현재 Bun에서 크게 개발하기에는, Node.js Runtime이나 React lazy 관련 번들링 에러 등 아직은 무리가 있는 요소들이 몇몇가지가 보이긴 하다.
    초반에는 Server Side Application 개발에만 문제가 있을 줄 알고, Client Side Application에는 잘 써먹을 수 있겠다 생각했는데, 리서치를 할 수록 아직 안되는게 너무 많이 보인다. 어쩌면 Bun 자체에 기여할 요소도 많아 보여서 좀 더 제대로 파보는 것도 좋을 것 같다.

    아무튼 결론은, 아직은 개발용으론 무리가 있지만, 인스톨, 빌드속도 실행속도는 진짜 빨라서 호환성이 높아질수록 정말 탐나는 녀석은 확실하다.

     

     

    반응형

    댓글

Designed by Tistory.