[USCF 시리즈 (4/4)] vvisp을 통해 DApp 업그레이드하기

Make your DApp Upgradeable with vvisp

Jihyeok Choy
HAECHI AUDIT
20 min readJan 2, 2019

--

블록체인 기술 전문 기업 ‘해치랩스(HAECHI LABS)’에서 업그레이드 가능한 스마트 컨트랙트 프레임워크에 대한 글을 연재합니다.

  1. 왜 우리는 업그레이드 가능한 스마트 컨트랙트가 필요한가?
  2. 업그레이드 가능한 스마트 컨트랙트를 위한 필요 조건과 배경지식
  3. 어떻게 스마트 컨트랙트를 업그레이드 가능하도록 바꿀 수 있을까?
  4. vvisp을 통해 DApp 업그레이드하기

지난 3편까지 Upgradeable Smart Contract에 대한 선행 연구들과 배경지식을 살펴보았으며 HAECHI LABS에서 설계한 Upgradeable Smart Contract Framework에 대해 알아보았습니다.

먼저, 1. 배포 후 발견된 버그를 수정할 수 있다는 점, 2. DApp이 비즈니스 로직을 업데이트할 수 있다는 점, 두 가지 이유를 통해 스마트 컨트랙트의 ‘Upgradeability(업그레이드 가능성)’의 필요성에 대해 논의한 후, Upgradeable Smart Contract Framework가 만족시켜야 할 조건들과 Storage, Function Call 그리고 DelegateCall의 배경지식을 살펴보았습니다. 이후, Proxy와 Registry를 이용한 Upgradeable Smart Contract Framework를 설명했습니다.

Upgradeable Smart Contract Framework의 진입 장벽

Upgradeable Smart Contract Framework(이하 USCF)을 이용하면, Atomic한 업그레이드와 동일한 Entry point를 통해 DApp 서비스의 질을 크게 향상시킬 수 있습니다. 하지만 여전히 서비스 운영에 있어 개발자의 어려움이 남아 있습니다. 업그레이드 가능한 서비스를 운영하기 위해서는 개발자가 USCF에 대한 모든 배경 지식을 알고 있어야 한다는 점입니다. 실제 서비스는 여러 컨트랙트들로 이루어져 있는 경우가 대부분이기 때문에 모든 컨트랙트들과 각 컨트랙트들에 해당하는 Proxy 컨트랙트들을 일일이 관리해야 하며, 배포 단계에서 배포와 업그레이드 함수 호출의 순서를 알고 있어야 합니다.

이러한 프레임워크에 대한 배경지식들은 진입 장벽이 되어 개발 속도를 현저히 떨어뜨립니다. 그렇다면 기존에 존재하던 여러 프레임워크들은 어떤 방식을 취하고 있을까요? 프레임워크는 그들만의 도구가 존재합니다.

가깝게는 스마트 컨트랙트 테스트시 사용하는 Truffle Framework를 살펴볼 수 있습니다. Truffle Framework는 자바스크립트 테스트 도구인 mocha를 사용하여 스마트 컨트랙트 테스팅 방법론을 제시합니다. 기존에 많이 쓰이는 mocha의 형태를 따라가기 때문에 직관적인 장점이 있지만, 스마트 컨트랙트를 테스팅 하기 위해서는 여러 전처리 작업이 필요합니다. 컴파일, 배포, 블록체인과 연결, 주소 저장 등의 작업은 필수적이지만 매 테스트 시 설정하기에는 상당히 번거롭습니다. 이를 위해, Truffle Framework는 truffle cli tool를 함께 제공하여 전처리 작업을 자동화합니다. 사실상 이 도구가 없으면 이 Framework를 사용하기에 많은 제약이 있다고 할 수 있습니다.

프론트엔드 개발자들에게 친숙한 React 역시 그를 위한 도구가 있습니다. React는 프론트엔드에서 UI(User Interface)를 만들기 위한 자바스크립트 프레임워크 중 하나입니다. 효과적인 환경을 제공하기에 최근 많은 인기를 얻고 있지만 React를 사용하기 위해 필요한 환경설정은 상당히 복잡합니다. 많은 라이브러리들이 필요하며, 독자적인 설정 파일과 파일 구조를 가지고 있어, 수동으로 설정하기에는 시간이 꽤 소요됩니다. 이를 위해, 이러한 개발 환경을 자동으로 만들어주는 create-react-app이란 도구가 존재합니다. create-react-app에서는 명령어 한줄로 즉시 개발과 테스팅이 가능한 환경을 만들어주기 때문에 개발자가 React 프레임워크에 빠르게 적응하게 도와줍니다.

이처럼, 개발자에게 하여금 많은 학습시간을 필요로 하는 프레임워크에는 이들을 쉽게 사용하게끔 도와주는 도구가 대부분 존재합니다. USCF도 앞선 포스팅에서 설명드린 배경지식 없이는 이용하기 어렵습니다. HAECHI LABS는 이러한 지식 없이도 손쉽게 업그레이드 가능한 스마트 컨트랙트를 구현하고 배포하기 위해 vvisp이라는 툴을 개발하였습니다.

vvisp은 아래와 같은 배경지식이 없어도 쉽게 이용할 수 있습니다.

  • Proxy와 Registry Contract에 대한 이해
  • 업그레이드 함수 호출에 대한 이해
  • 배포 및 function call 순서에 대한 이해

위와 같은 이해가 부족한 상태에서 vvisp 없이 개발을 한다면 컨트랙트를 잘못 작성하거나, 함수 호출 순서를 잘못 부르거나, 잘못된 인자를 넘겨주는 등 실수할 여지가 많습니다. 경제적 가치를 지니는 암호화폐가 유통되는 DApp이라면 이러한 실수가 치명적인 결과를 초래할 수도 있습니다. 따라서 vvisp은 개발자가 USCF에 대한 이해가 부족하더라도 실수하지 않고 편하고 빠르게 배포할 수 있도록 돕습니다. vvisp이 제공하는 기능은 다음과 같습니다.

  1. Proxy와 Registry solidity 컨트랙트를 library로 제공
    Open Source 컨트랙트인 Proxy 컨트랙트와 Registry 컨트랙트를 vvisp에 탑재하였습니다. 개발자는 제공된 라이브러리를 사용하여 이 두 컨트랙트 개발에 대한 고민 없이 비즈니스 로직에 대한 컨트랙트만 개발할 수 있습니다.
  2. 여러 함수 호출 및 배포 순서 자동화
    USCF에서 가장 까다로운 부분이 바로 실제 배포 단계입니다. 도구 없이 배포하기 위해서는 다음과 같은 까다로운 절차가 필요합니다.
    - Registry 컨트랙트 배포
    - Business 컨트랙트와 그에 대응하는 Proxy 컨트랙트를 배포
    - Business 컨트랙트와 Proxy 컨트랙트를 Registry를 통해 atomic하게 연결
    vvisp은 이러한 배포 절차를 자동화하여 개발자의 고충을 해결합니다.
  3. 명령어 한 줄을 통한 서비스 배포 실행
    ‘사용이 쉽다’라는 전제를 만족하기 위해 가장 중요하게 생각한 요소입니다. vvisp은 위의 배포 과정을 $vvisp deploy-service한 줄을 통해 모든 작업을 자동 실행하여 수작업을 통한 실수를 제거합니다.

이제, 샘플 앱을 통해 이 작업이 어떻게 이루어지는지 살펴보도록 하겠습니다.

DApp 프로젝트 생성

간단한 DApp 프로젝트를 위해 새로운 디렉토리를 하나 생성합니다.

$ mkdir vvisp-sample

해당 디렉토리에서 프로젝트를 시작하기 위해 vvisp을 설치합니다.

$ npm install -g @haechi-labs/vvisp

혹은

$ yarn global add @haechi-labs/vvisp

이제 생성한 디렉토리에서 샘플 DApp 프로젝트를 시작합니다.

$ cd vvisp-sample
$ vvisp init

해당 안내와 함께 프로젝트를 위한 환경이 만들어집니다. 컨트랙트 테스트를 위한 truffle의 디렉토리 구조를 지원하여 보다 손쉽게 두 도구를 모두 이용할 수 있습니다. 또한, HAECHI LABS에서 코드 감사를 위해 사용하는 스크립트와 여러 유용한 라이브러리가 함께 있어 프로젝트 bootstrapping을 빠르게 할 수 있습니다.

이제 해당 디렉토리의 contracts/폴더 내에서 샘플 DApp 컨트랙트를 생성합니다.

해당 DApp은 크게 두 가지 컨트랙트로 이루어져 있습니다. 사용자는 Haechi라는 가상의 펫을 키울 수 있습니다. HaechiV1 컨트랙트에서는 Haechi의 소유권과 각 Haechi의 능력치에 대해 구현되어 있습니다. 가상의 달리기 시합을 한다고 할때, 사용자는 HaechiV1.solrun 함수를 불러 Haechi를 달리게 할 수 있습니다. 이때, Haechi의 빠르기 velocity는 Haechi의 고유 능력치이며, 이를 HaechiGym.sol에서 ‘강화’할 수 있습니다.

간단한 DApp이지만 이를 업그레이드 가능하도록 배포하기 위해서는 앞서 이야기한 장벽이 존재합니다. 이제 vvisp을 통해 이를 어떻게 단순화하였는지 살펴보겠습니다.

DApp 배포

배포에 앞서 vvisp 프로젝트는 사용할 네트워크와 계정의 설정이 필요합니다. 생성된 프로젝트 내의 .env 파일에서 몇 가지 환경 변수를 설정합니다.

  • NETWORK: 연결하고자 하는 네트워크의 이름입니다. local을 제외하고, vvisp은 infura를 지원합니다.
  • PORT: local 선택시, 연결하고자 하는 포트 번호입니다.
  • INFURA_API_KEY: 외부 네트워크 연결시 필요한 infura api key입니다.
  • MNEMONIC: 트랜잭션을 생성할 대상의 mnemonic key입니다.
  • PRIV_INDEX: MNEMONIC으로 생성될 private key의 index입니다. 기본 값은 0입니다.
  • GAS_PRICE: 트랜잭션 발생시 설정하고자 하는 gas price입니다. 기본값은 10Gwei이며, 입력 단위는 wei입니다.

현재 입력된 정보는 local:8545에 연결된 노드에서 MNEMONIC의 첫 번째 계좌로부터 10Gwei로 트랜잭션을 발생시키겠다는 뜻입니다. 해당 .env 파일의 작성이 끝나면 이제 본격적인 배포를 시작합니다.

* 기본적으로 vvisp의 컨셉은 Terraform을 참고하였습니다.

참고: 기존에 클라우드 인프라(AWS 등)를 수동으로 배포할 경우, 설정 등 일관성이 깨져 매번 다른 인프라가 배포될 수 있습니다. Terraform과 같은 IaaC(Infrastructure as a Code) 툴은 인프라 배포 과정을 코드로 일관성과 가독성 있게 관리하는 데 도움을 주는 도구입니다. Terraform은 인프라 배포를 위한 설정 파일을 작성하게 하여 일관성과 가독성을 제공합니다.

Terraform과 비슷하게 vvisp 역시 서비스 배포를 위해서는 컨트랙트들의 정보를 기록한 배포 설정 파일, service.vvisp.json이 필요합니다. 이 파일에 배포 정보를 어떻게 기록하는지 살펴보도록 하겠습니다.

  • serviceName: 배포하고자 하는 서비스의 이름입니다.
  • variables: 이 파일에서 사용할 constant 변수를 지정합니다. 현재 기입된 owner의 주소는 ${variables.owner}로 접근 가능합니다.
  • contracts: 이번 버전 서비스를 구성하는 contract들의 정보를 적는 곳입니다.
  • contracts/contractName: 컨트랙트의 key 값입니다. 실제 컨트랙트 명 또는 파일 명이 아닌 별도의 추상적인 이름으로 버전을 관리할 수 있습니다. (예를 들어, HaechiV1.solHaechiV2.sol → … 으로 업그레이드할 컨트랙트를 Haechi라는 key 값으로 관리할 수 있습니다.)
  • contracts/contractName/path: 해당 컨트랙트 파일의 저장 위치입니다.
  • contracts/contractName/upgradeable: 업그레이드 가능하도록 배포할 것인지 정의합니다.
  • contracts/contractName/initialize: initialize가 필요한 경우 함수와 인자를 정의합니다. 없다면 비워도 무방합니다.
  • ${contracts.Gym.address}는 이미 배포된, 혹은 배포할 컨트랙트의 주소를 전달할 때 사용합니다. 위 예시는 Gym 컨트랙트의 address를 생성자의 인자로 받겠다는 의미입니다.

만약 토큰 컨트랙트와 같이 쉽게 변경이 가능하면 위험성이 커져 upgradeability를 배제하고 싶은 컨트랙트(이하 nonUpgradeable Contract) 역시 서비스에 포함시킬 수 있습니다. 이 경우 upgradeable 속성을 비워두거나 false로 두어 아래와 같이 작성하면 됩니다.

  • contracts/contractName/constructorArguments: nonUpgradeable 컨트랙트의 경우, constructor를 실행하기 위한 인자들을 정의할 수 있습니다.
  • contracts/contractName/initialize: nonUpgradeable 컨트랙트 역시 initialize 함수를 등록할 수 있습니다. 해당 함수는 모든 배포가 끝난 후 일괄 호출됩니다.

* vvisp이 truffle과 비교하여 어떤 점이 다른가요?
vvisp은 truffle과 달리 스마트 컨트랙트 업그레이드를 지원한다는 것 외에도 배포 설정 파일 service.vvisp.json을 작성하는 방식을 채택했다는 점이 특징입니다.
명시적인 별도의 파일을 작성한 후 이를 통해 배포하는 방법은 커맨드 라인과의 대화형 질문을 통해 배포하는 방법에 비해 실수할 여지가 적으며 같은 상황을 재현하기 쉽습니다. truffle의 경우 개발자에게 script를 작성하여 배포하도록 합니다. 이 방법은 자유도는 높지만, 모든 컨트랙트의 내용과 배포 순서를 모두 염두한 채로 script를 작성해야 하기 때문에 복잡합니다. vvisp이 지원하는 json 형식의 배포 설정 파일은 간편할 뿐 아니라, 개발자/비개발자 모두 한 눈에 알기 쉬운 형태로 표현되어 있어 가독성 측면에서도 장점을 가지고 있습니다.

배포 설정 파일 service.vvisp.json의 작성이 끝난 후, 다음과 같이 해당 서비스를 배포할 수 있습니다.

DApp의 첫 배포가 끝나면 자동으로 state.vvisp.json이 생성됩니다.

state.vvisp.json은 배포된 서비스와 이를 구성하는 컨트랙트들의 정보를 보여주는 파일입니다. 다음과 같은 항목이 기입됩니다.

  • serviceName: 배포된 서비스 명
  • registry: Registry 컨트랙트의 주소
  • contracts: 현재 배포된 버전을 구성하는 컨트랙트 종류
  • contracts/contractName: service.vvisp.json에서 정의된 컨트랙트의 key 값
  • contracts/contractName/upgradeable: 업그레이드 가능한지 여부 표현
  • contracts/contractName/address: Business 컨트랙트의 주소
  • contracts/contractName/fileName: 배포한 Business 컨트랙트의 파일(컨트랙트) 명
  • contracts/contractName/proxy: 업그레이드 가능한 컨트랙트의 Proxy 컨트랙트 주소(업그레이드가 불가능한 컨트랙트는 해당 항목이 존재하지 않음)

해당 파일에 기록되는 정보는 Registry 컨트랙트에도 저장되어 있기 때문에, 만약 state.vvisp.json 파일이 변경/삭제되어도 Registry 컨트랙트에 기록된 정보를 토대로 언제든 복구가 가능합니다. (추후 Registry 컨트랙트를 바탕으로 자동으로 state.vvisp.json을 복구하는 기능 또한 추가할 예정입니다.)

DApp 업그레이드

이제 앞서 생성한 샘플 DApp을 vvsip으로 업그레이드하는 과정을 살펴보겠습니다. HaechiV1에서 Haechi는 달리기를 할 수 있었다면, 이제 날 수 있도록 업그레이드하려고 합니다. 개발자는 다음과 같은 두 가지 절차를 따릅니다.

1. 이전 버전 컨트랙트인 HaechiV1.sol을 상속받은 HaechiV2.sol을 생성한 후 변경할 로직을 작성

2. 배포 설정 파일 state.vvisp.jsoncontracts/Haechi를 다음과 같이 변경

  • 파일명을 변경
  • initialize 항목을 삭제(HaechiV2.sol 컨트랙트는 initialize 하지 않음)

위 과정에서 개발자는 변경할 비즈니스 로직과 배포 정보 이외에는 신경쓰지 않아도 되며 간단하게 업그레이드를 준비할 수 있습니다.

이제 $vvisp deploy-service를 통해 다음 버전으로 서비스를 업그레이드 합니다.

배포가 끝나면 변경사항이 state.vvisp.json에 저장됩니다.

업그레이드를 실행했던 HaechifileNameaddress가 바뀐 것을 알 수 있습니다. Proxy 컨트랙트의 주소는 바뀌지 않았기 때문에 서비스는 유저에게 동일한 Entry point를 제공할 수 있습니다.

추가적인 주요 기능

앞에서는 언급되지 않았지만 vvisp의 deploy-service에는 몇 가지 주요 기능들이 더 있습니다.

Cyclic Dependency

스마트 컨트랙트의 생성자에 다른 컨트랙트의 address를 인자로 넘겨주는 것은 흔한 패턴입니다. 특히, 여러 컨트랙트가 얽혀 있는 DApp의 경우, 한 컨트랙트가 많은 컨트랙트의 주소를 참조하고 있을 수 있습니다.

위 코드는 ContractAContractB의 address를 생성자에서 참조하고 있는 코드입니다. 이 때 컨트랙트가 배포되기 전까진 해당 컨트랙트의 주소를 알 수 없습니다. 따라서 ContractBContractA보다 먼저 배포되어야 합니다. 그런데 아래와 같이 ContractB도 반대로 ContractA를 참조하고 있다면, 즉 서로가 서로를 참조하고 있다면, 컨트랙트 배포 순서를 정할 수 없게 됩니다.

따라서 두 컨트랙트를 모두 배포할 때, 먼저 배포되는 컨트랙트에는 올바른 인자를 전달할 수 없습니다. 이를 Cyclic Dependency가 존재하는 상황이라고 합니다. 컨트랙트 수가 3개라면, A가 B를, B가 C를, C가 A를 참조하게 될 경우 Cyclic Dependency가 형성됩니다.

이는 사실상 잘못된 설계이며 올바른 배포가 일어나지 않습니다. 따라서, vvisp은 배포 전 이를 자동으로 확인하는 과정을 거치며, 만약 Cyclic Dependency가 존재할 경우 배포를 시작하지 않습니다.

Cyclic Dependency가 발견되지 않는다면, vvisp은 컨트랙트들 간의 dependency 순서도(DAG)를 만들고 그 순서대로 자동 배포하여 컨트랙트의 주소를 올바르게 전달합니다.

참고: 위 사항은 업그레이드 가능한 컨트랙트에 대해서는 적용되지 않습니다. 업그레이드 가능한 컨트랙트는 미리 모든 컨트랙트들을 배포한 후 연결 작업만 시행하기 때문에, 주소가 모두 공개되어 있어 Cyclic Dependency를 고려하지 않아도 됩니다.

Deployment Resuming

vvisp이 DApp 서비스를 배포할 때에는 여러 트랜잭션을 순서대로 발생하게 됩니다. 이 때, 갑작스럽게 네트워크/노드에 장애가 일어나거나 다른 클라이언트에서 같은 주소로 동시에 트랜잭션을 날려 nonce가 겹치는 문제가 발생할 수 있습니다. 이 경우, 배포를 진행중이던 vvisp은 작업을 중단하게 됩니다.

생각보다 이러한 문제는 자주 발생합니다. 그런데 이 때마다 처음부터 다시 배포한다면 시간도 오래 소요될 뿐더러 많은 양의 가스를 소모하게 됩니다. 따라서 도중에 배포가 중단되었다면 중단된 곳부터 다시 재시작하는 기능이 꼭 필요합니다.

재시작을 위해서는 중단된 시점과 중간 상태에 대한 정보가 필요합니다. 즉, 서비스의 현 상태에 대해 저장된 정보가 필요합니다. vvisp에서는 이를 간단한 State Machine을 도입하여 구현하였습니다.

$vvisp deploy-service는 아래 순서대로 트랜잭션을 발생시킵니다.

  1. Deploy Registry: 서비스가 처음 배포된다면 Registry 컨트랙트를 배포합니다.
  2. Deploy Business Contracts: 업그레이드 가능하도록 정의된 컨트랙트들의 Business 컨트랙트를 배포합니다.
  3. Deploy Proxy Contracts: 업그레이드 가능하도록 정의된 컨트랙트들의 Proxy 컨트랙트를 배포합니다.
  4. Deploy Non-Upgradeable Contracts: 업그레이드 불가능하도록 정의된 컨트랙트들을 배포합니다.
  5. Upgrade: 업그레이드 가능하도록 정의된 컨트랙트들의 Business 컨트랙트, Proxy 컨트랙트를 Registry 컨트랙트와 연결합니다.

만약 배포 도중 어떤 문제상황이 발생하여 배포가 중단되었다면 아래와 같은 메시지를 출력합니다.

이 때, 중간 상태에 대한 정보가 state.vvisp.json에 기록됩니다.

이는 Token 컨트랙트의 Proxy 컨트랙트를 배포하던 중 중단되었다는 뜻으로, 해당 정보 이외에도 많은 중간 상태들이 파일에 기록되어 있습니다. 이 때, 안내된 메시지와 같이 $vvisp deploy-service를 재입력하면 중단되었던 곳부터 배포를 재시작할 수 있습니다.

Registry 컨트랙트 내의 배포 정보

state.vvisp.json을 통해 배포 정보를 보여주는 것은 가시적이며 간단합니다. 그러나 불가변성이라는 장점을 지닌 블록체인을 두고 local file에만 의존하는 것은 중요한 기능을 놓치고 있는 것이라 생각합니다. 그래서 배포된 contract의 정보들(file 이름 등)을 Registry 컨트랙트에 쓰는 형태를 택하여, state.vvisp.json 뿐만 아니라 Registry 컨트랙트 내에도 모든 정보를 보관할 수 있도록 하였습니다.

현재 버전(v0.1.9)의 vvisp에서는 Registry에 기록을 저장하는 기능까지만 구현되어 있습니다. 추후에는 배포 전 Registry 컨트랙트의 정보를 읽어 현재 local file(state.vvisp.json)이 최신인지 여부를 판단하는 기능과 해당 정보를 통해 state.vvisp.json을 복구하는 기능도 추가할 예정입니다.

지금까지 vvisp의 메인 기능인 deploy-service에 대해 살펴보았습니다. 이외에도 vvisp은 스마트 컨트랙트 개발을 도와주는 다양한 기능들을 지원하고 있습니다. 자세한 사항은 아래 vvisp github을 참고하시기 바랍니다. :)

마무리하며

Facebook을 비롯한 오늘날의 서비스들은 한 달에도 서너 번의 업데이트를 진행하며 서비스의 품질을 향상시키고 있습니다. 만약, 어떠한 서비스가 업그레이드를 하지 않아 시장의 변화와 이용자의 니즈를 반영하지 못한다면 그 서비스는 도태되고 말 것입니다. 스마트 컨트랙트를 이용한 DApp 서비스도 예외는 아닙니다.

그러나 스마트 컨트랙트 위에서 Upgradeability를 구현하기란 쉽지 않습니다. 앞서 언급드린 Proxy와 Registry의 개념, 스마트 컨트랙트의 storage 구조 등을 모두 이해하고 있어야 하며 업그레이드를 어떻게 진행해야하는지에 대한 과정 역시 개발자가 숙지하고 있어야 합니다.

HAECHI LABS에서 개발한 Upgradeable Smart Contract Framework(vvisp)은 이러한 작업을 간소화하였습니다. 필요한 컨트랙트 라이브러리를 지원하고, 단 한 줄의 명령어로 배포를 자동 실행해주어 개발자가 실제 서비스에 필요한 Business 로직 개발에만 집중할 수 있는 환경을 제공하고 있습니다.

HAECHI LABS는 블록체인이 정말 세상을 바꿀 만한 기술임을 입증하기 위해서는 훌륭한 적용 사례가 등장해야 한다고 믿습니다. 또한 이를 위해서, 많은 개발자/회사들이 쉽고 편리하게 블록체인 서비스를 만들 수 있어야 한다고 생각합니다. HAECHI LABS는 누구나 손쉽게 블록체인 위에 서비스를 개발할 수 있는 세상을 만든다는 비전을 위해 나아가고 있으며, vvisp은 이러한 발걸음 중 하나입니다. 앞으로 vvisp은 많은 기능들이 추가되어 발전될 예정입니다!

해당 오픈 소스 프로젝트가 더 많은 사람들이 다양한 서비스를 만드는 데에 작은 도움이 되었으면 좋겠습니다. 또한 오픈 소스를 더 발전시키기 위해 여러 개발자 분들과 함께 이야기하고 싶습니다. 관심있으신 분들은 언제든지 편하게 vvisp 오픈 소스 프로젝트에 합류해주세요! :)

HAECHI LABS는 언제나 함께 세상을 바꾸어 갈 분들을 향해 문을 활짝 열어두고 있습니다.

채용에 관련된 자세한 사항은 여기를 참고해주세요.

--

--