Published on

tWIL 2022.09 3주차

Authors

이번 주 밖에서 라이딩을 하지 못했다. 하지만 집에서 와후키커로 고강도 인터벌 라이딩을 했다. 음 300W로 1분 달리고 쉬다가 다시 1분씩 3km.

밖에서 라이딩해야 몸도 마음도 상쾌해진다.

skyloud

이번 주는 연휴 이후라 개발자들 세미나 자료를 만들고 OJT를 진행하였다. 그리고 Slack과 Github Team을 오픈하고 마이그레이션을 수행하였고, Jira와 Github action으로 티켓의 자동화를 위한 기본작업을 하였다. 원래 이번주에 계획되었던 건 인프라 수정이었는데 AWS CDK와 Terraform 학습이 필요하였다. 여기는 다음 주에 워크샵을 진행해보며 가능성을 판단해보고 진행할 예정이다. 그리고 Threejs로 패키지 소프트웨어 솔루션 결과물을 어느정도 렌더링할 수 있으며 쓸 수 있는 데이터를 파악해보기 위해 적합성 테스트 먼저 진행하였다.

웹개발자를 위한 OJT

사실 트레이닝을 먼저하고 프로젝트에 착수했어야 했는데, 급하다보니 프로젝트 후 OJT를 진행하게 되었다. 여기서 현재까지 개발자들이 어려워 했던 것이나 작업이 매끄럽지 못했던 이유를 가늠하고 이부분에 대해 디테일한 자료를 만들었다.

첫번째로 git workflow. git bare 저장소(개발을 시작한지 얼마 안되는 사람들은 git을 github와 동일시 여기거나 로컬에서 bare 저장소를 활용하는 방법을 몰랐다)를 소개하고, git의 refHEAD를 설명했다. 여기서 snapshot의 개념을 이해시키고 hooks를 소개했다. 일반적으로 사용되지 않는 기술이긴 하지만 자체 git 서버를 구축한다면 필요한 작업이기도 하다. post-receive에 스크립트를 만들어서 git commit 이 후 서버가 어떤 작업을 처리할 수 있는지 이 hook을 통해 할 수 있는 가능성을 이야기했다. 예시를 들어 내가 오래전에 작업한 MQTT로 tags를 확인하여 업데이트 가능한 상태를 IoT기기에 메시지를 보내는 용도로도 활용 가능하다고...(이해 했는진 모르겠지만...)

이후 Express로 Nodejs 서버를 만드는 방법 그리고 미들웨어로서 GraphQL 서버를 이해하기. 그 전에 Plain GraphQL은 순수 JavaScript이기 때문에 백엔드든 프론트엔드에서든 사용이 가능하다는 내용. 꼭 GraphQL이 서버로 동작할 필요가 없고 내부통신의 한 인터페이스로서도 활용이 가능하다는 이야기. 그렇게 GraphQL fundamental을 먼저 소개하고 우리의 백엔드가 어떻게 구성되어있는지, Code-first 스키마를 어떻게 만드는지 간략하게 진행했다.

왜 주니어 프론트 개발자에게 이것들을 이야기해야하는지 의문을 가질 수 있는데, 요즘은 이걸 확실히 선을 그을 수 없다는 것은 누구나 동의할 것 같다. 프론트 개발자라고 API 이후의 영역만 공부해서 깊이를 더할 수 없다는 것과 백엔드 개발자라고 해서 프론트를 전혀 이해하지 못하는 경우는 없기 때문이다. 그래서 언제나 나의 팀원이나 신입들에게 강조하는 것은 기본적인 스탠스는 풀스택을 지향해야 하며, 각 파트에 맞는 심도있는 경험과 학습으로 둘을 연결해야 한다고 한다. 한 예로 프론트 신입에게 이제 Github로 터전을 옮겼으니, GPG 키를만들어 signed commit을 하라고 했는데, "GPG는 SSH같은 건가요?" 라는 질문을 받았다. 이게 백엔드 지식은 아니지만, 일단 Secure shell을 이해하지 못하는 경우도 있었다. 음 리눅스를 먼저 가르쳐야 할지 프론트 개발자는 몰라도 되는 지식인지 잠시 고민을 했다. 그리고 장기적인 관점으로 봤을 때 개발자 커리어에 이 지식들은 필수라고 생각을 하고, 다시 리눅스 기초와 가상화, Docker가 왜 핫하며 반가상화가 무엇인지, 클라우드는 어떻게 우리에게 서버를 제공하며 리소스는 어떻게 관리되는지를 이해시켜야 한다고 생각했다. 또한 SSR의 개념을 이해시키려면 서버 렌더링이 무엇인지 이해시켜야 했다. 이 경우 서버가 어떻게 동작하는지 알아야하는데 서버를 만들어 본적이 없다. create-react-appcreate-next-app을 실행하여 클라이언트 설정만 해봐서, 웹팩설정을 해본적 없고 dev서버를 가동하여 React 클라이언트를 가동시키고 있다는 점도 이해하지 못한다. 아 심지어 웹팩이 뭔지 모르는 경우도 있다. (요즘은 몰라도 큰 문제가 없을 것 같다는 생각을 하기도 하지만...) 이렇게 프론트 신입이 갖추어야 할 조건이 참 모호한 상태이다. 어쨌든 CRA로 만든 프로젝트를 빌드한 파일들을 static으로 제공하는 업체에 올리기만 하면 되는 것 아닌지? 그 이외의 지식은 불필요한 것인가 고민을 하게 되었다.

어쨌든 우리 개발자들은 알았으면 좋겠다는 생각에 진행하였고, 다음 주엔 클라이언트들을 돌려보며 이것 저것 필요한 기술들을 알려주어야 한다.

Frontend Browser API

React Fundamental

  • Design pattern: Presenter, Container
  • Style in JavaScript
  • Context API / Render Props
  • Hooks
    • useState
    • useEffect
    • useCallback
    • useMemo
    • Custom Hooks
  • State management: RTK, RTKQ
  • apollo-client
  • GraphQL-codegen

NextJS

  • CSR, SSR, SSG
    • getServerSideProps, getInitalProps
    • getStaticProps, getStaticPath
  • Vercel
  • useSWR
  • react-query
  • Next API

이정도로 요약하고, 프로젝트 진행하며 필요한 기술들은 그때 그때 진행하려고 한다.

Three.js & Forge SDK

glTF Converter

프로젝트에서 패키지 소프트웨어와 솔루션 팀을 통해 도출 되는 결과는 모델링(Navisworks) 파일과 엑셀 결과물이다. 이 모델링 파일은 Forge SDK를 사용하여 브라우저로 렌더링한다. 이 Forge SDK는 Three.js를 기반으로 만들어져있다. Three.js는 MIT License이다. 따라서 Forge SDK도 오픈소스로 제공되어 지지만 OSS사용과 요청 건수 그리고 컨버팅이 유료이다. 추측하면 Autodesk 인프라 사용료를 받는 격인듯하다. 그렇다면 추후 우리가 마음대로 모델링 파일들을 렌더링 하면서 수정가능하게 하려면 결국 Forge에서 제공하는 OSS를 사용하지 않아야하는데 이부분은 구축하는데 꽤 시간이 걸릴 듯 하지만 가능성이 있는지 테스트를 시도해보았다.

우선 최종 결과물을 Three.js로 렌더링이 가능한지 파악해야 가능성에 대한 답을 얻을 것 같아 적합 테스트를 위해 Three.js에서 사용하는 3D모델을 찾아보았다. GLTFLoader

glTF (GL Transmission Format) is an open format specification for efficient delivery and loading of 3D content. Assets may be provided either in JSON (.gltf) or binary (.glb) format. External files store textures (.jpg, .png) and additional binary data (.bin). A glTF asset may deliver one or more scenes, including meshes, materials, textures, skins, skeletons, morph targets, animations, lights, and/or cameras.

그리고 이 glTF파일로 변환을 지원하는 glTF패키지 프로젝트들의 모음에서 Autodesk Forge 패키지를 찾을 수 있다. 그리고 지원 변환 포멧에서도 확인이 가능하다.

테스트해 볼 내용은 Forge Data Management SDK로 모델을 업로드하고 변환된 상태의 urn으로 glTF형식으로 다운로드가 되는지였다.

여기서 2개의 패키지가 필요하다. forge-server-utils, forge-convert-utils 이 두개를 설치하고 테스트해보았다.

import path from "path";
import { ModelDerivativeClient, ManifestHelper } from "forge-server-utils";
import { SvfReader, GltfWriter } from "forge-convert-utils";

async function run(urn: string, outputDir: string) {
  const auth = {
    client_id: process.env.FORGE_CLIENT_ID,
    client_secret: process.env.FORGE_CLIENT_SECRET,
  };
  const modelDerivativeClient = new ModelDerivativeClient(auth);
  const manifestHelper = new ManifestHelper(await modelDerivativeClient.getManifest(urn));
  const derivatives = manifestHelper.search({
    type: "resource",
    role: "graphics",
  });
  const readerOptions = {
    log: console.log,
  };
  const writerOptions = {
    deduplicate: true,
    skipUnusedUvs: true,
    center: true,
    log: console.log,
  };
  const writer = new GltfWriter(writerOptions);
  for (const derivative of derivatives) {
    const reader = await SvfReader.FromDerivativeService(urn, derivative.guid, auth);
    const scene = await reader.read(readerOptions);
    await writer.write(scene, path.join(outputDir, derivative.guid));
  }
}

run("dx.....ndj", "./output");

서버쪽에서 간단히 실행해보니 변환된 파일이 생성되었다. output.gltf, 0.bin 몇가지의 Warning과 함께...

모델링 데이터는 용량이 꽤 컸으며, primitives 메타데이터들을 많이 얻을 수 있었다. 바이너리 파일은 byteOffsetbyteLength를 가지고 bufferViews 항목을 가지고 있었다.

여기서 우리는 이 복잡한 모델 데이터를 JSON형태의 파일로 처리를 할 것이냐, 별개의 DB로 구축을 해놓을 것이냐의 고민도 해봐야할 것 같다.

forge-convert-utilsCustomization 방식을 제공하고 있다.

  • The samples/custom-gltf-attribute.js script adds the dbID of each SVF node as a new attribute in its mesh
  • The samples/filter-by-area.js script only outputs geometries that are completely contained within a specified area

그리고 Property Pipeline을 통해 인하우스 개발 솔루션의 가이드라인을 주고 있다. (The property data pipeline is powered up by an in house developed solution called property database. This section describes requirements, design, implementation and applications of property database in the context of LMV / viewing.)

여기까지 가능성을 염두해 두고, 추후 Forge 렌더러를 제거하고 우리만의 렌더러를 만들 수 있을 것이라 생각한다. 이렇게 되면 더욱 더 다양한 시도를 할 수 있을 것 이라 생각하는데(material 커스터마이징, 추가, 제거, 변경등...), 아직까지는 Forge SDK는 기본적인 유용한 것들을 제공하고 있어서 써야한다.

Client (Three.js)

이제 변환된 glTF데이터를 가지고 렌더링 테스트를 시도해보았다. 언제나 빠르게 클라이언트를 시작하려면, 내가 만들어 둔 boilerplates들(vanillajs-typescript-webpack, vanillajs-typescript-gulp)이 있지만, CRA로 Vanillajs를 진행할 수 있다.

create-react-app threejs-gltf-render-test --template=typescript

우선 앞서만든 output.gltf파일과 0.binpublic에 복사해둔다. 그리고 React는 쓰지 않고 Vanilla로 작성할 예정이기 때문에 src/app.ts를 만들고 src/index.ts

src/index.ts
export * from "./app"

그리고 public/index.html<div id="webgl-container"></div>를 추가해준다.

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>Three.js test</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="webgl-container"></div>
  </body>
</html>

src/app.ts는 아래와 같이 Three.js로 시작한다.

src/app.ts
import "./index.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

const backgroundColor = 0x000000;

type RenderCalls = () => void;
const renderCalls: RenderCalls[] = [];
function render() {
  requestAnimationFrame(render);
  renderCalls.forEach(callback => {
    callback();
  });
}
render();

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(
  80,
  window.innerWidth / window.innerHeight,
  0.1,
  800
);
camera.position.set(0, 0, 100);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(backgroundColor); //0x );

renderer.toneMapping = THREE.LinearToneMapping;
renderer.toneMappingExposure = Math.pow(0.94, 5.0);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;

window.addEventListener(
  "resize",
  function () {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  },
  false
);
const divContainer = document.querySelector("#webgl-container");
divContainer?.appendChild(renderer.domElement);

function renderScene() {
  renderer.render(scene, camera);
}
renderCalls.push(renderScene);

const controls = new OrbitControls(camera, renderer.domElement);

controls.rotateSpeed = 0.3;
controls.zoomSpeed = 0.9;

controls.minDistance = 3;
controls.maxDistance = 120;

controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2;

controls.enableDamping = true;
controls.dampingFactor = 0.05;

renderCalls.push(function () {
  controls.update();
});

const light = new THREE.PointLight(0xffffcc, 20, 200);
light.position.set(4, 30, -20);
scene.add(light);

const loader = new GLTFLoader();
loader.load(
  "output.gltf",
  function (data) {
    const object = data.scene;
    object.position.set(0, 0, 0);
    scene.add(object);
  }
);

Three.js 렌더러와 Orbital control, camera, scene, light를 위한 설정 때문에 꽤 코드가 길어졌는데 여기서 glTF 렌더링을 위한 코드는 77-85라인 뿐이다.

three-js-test

Property pipeline을 사용한다면 이 모델 우리 입맛대로 수정 가능할 것 같다.

AWS CDK

이제 인프라 수정이다. 다음 주는 CDK를 Github action으로 수행하고 인프라를 CD pipeline에서 구동시켜볼 예정이다.

목표는 AWS CDK로 API 컨테이너, Auth 컨테이너 두개를 dev, stage, release 상태로 배포하는 것이다. AWS 인프라를 코드 베이스로 배포하는 것이라서 예전에 docker stack으로 traefik을 사용해서 배포했던 과정보다 복잡할 것 같다. Terraform도 공부를 더 해 보겠지만, 우선 AWS CDK 워크샵을 수행해보고 생각해봐야겠다.

NextJS 배포 블로그를 보면 좀 멘붕이 오는데 문서가 거의 스크린샷이다. 잘 정리해줘서 매우 고마운데 매번 인프라를 이렇게 구축할 수 없겠다 싶다. 지난 회사에서는 이렇게 마우스로 삽질을 많이했는데 앞으로는 이것도 CDK로 IaC로 정리할 수 있다면 해봐야겠다.

Github action for JIRA

Bitbucket의 pipeline은 월 50분 무료인것 같다.(심각) Github action은 3000분 무료인데 차이가 좀 심한것 같다. 일단 우리는 JIRA 워크플로를 만들고 이 워크플로는 개발자의 경우 상태는 모두 commit에 담긴 메시지로 이슈를 분리하고 Github PR 상태로 이 상태를 변경하려고 하며, CI/CD를 통한 배포(dev, stage, release)를 고려하여 상태를 변경하려고 한다. 이러한 방식이 운영쪽에서도 각 이슈별 상태를 직관적으로 확인할 수 있기 때문에...

jira-workflow

여기서 각 전환에서 트리거를 수행시킬 수 있다.

jira-trigger

대표적으로 상태 전환시 담당자 변경등을 수행할 수 있다.

그리고 Atlassian은 Github action for JIRA, 일명 gajira를 만들어 주었다.

이걸 통해 일명 Synchronize JIRA 액션을 만들어 보았다는 아니고, 만드는 중이다. 다음 주에 테스트를 해보고 모든 프로젝트에 이걸 적용하려고 한다. 일단 시작은 pull_request 타입을 얻고 commit 메시지에서 이슈키를 찾은 후 PR타입으로 해당 이슈를 transition 하는 방식이다.

예시는 이렇다.

.github/workflows/jira-sync.yml
name: Synchronize JIRA
on:
  pull_request_review:
    types: [submitted]
  pull_request:
    types:
      [
        opened,
        reopened,
        synchronize,
        review_requested,
        review_request_removed,
        closed,
      ]

jobs:
  sync-action:
    runs-on: ubuntu-latest
    name: Sync Jira tickets of PR
    steps:
      - name: Status Log
        run: echo ${{ github.event_name }}
      - name: Event type
        run: echo ${{ github.event.action }}
      - name: Login
        uses: atlassian/gajira-login@master
        env:
          JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
          JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
          JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
      - name: Find in commit messages
        uses: atlassian/gajira-find-issue-key@master
        id: issueKeys
        with:
          from: commits
      - name: Find in commit messages
        uses: atlassian/gajira-find-issue-key@master
        id: issueKeys
        with:
          from: commits
      - name: Log issue key
        run: echo ${{ steps.issueKeys.outputs.issue }}
      - name: Transition issue
        uses: atlassian/gajira-transition@master
        with:
          issue: ${{ steps.issueKeys.outputs.issue }}
          transition: "PASSED"

이렇게 다음 주 할 일을 더 많이 만들었다.