온보딩 프로젝트 개발기 - 1부
멀티 패러다임 프로그래밍을 통한 프론트엔드 클린코드
안녕하세요 ! 이번에 데이터메이커 연구개발팀 프론트엔드 파트에 새로 합류하게된 윤성연 이라고 합니다.
데이터메이커 연구개발팀에서는 신규 입사자 분들이 입사후 온보딩 프로젝트를 통해 팀에 빠르게 적응할수 있도록 돕고 있습니다.
사실 이 내용은 제 온보딩 프로젝트 개발기 이기도 하지만, 부제가 달린 이유가 있습니다.
이번 온보딩 프로젝트를 마무리 하며 발표자료 준비간 왜 이렇게 구현했는지, 어떤 문제를 해결하기위해 그렇게 했는지 등을 적었는데요, 그런 중심으로 적다보니
평소에 코드로 무엇을 이야기 하고 싶은지, 개발할때 담아내고 싶은것은 무엇인지, 어떤것을 고민하는지 좀더 깊이 생각해보게 되었습니다.
그렇게 내용을 쓰다보니 전체적인 내용에서 이 이야기를 하고 싶었던것 같습니다.
개요
프론트엔드 파트 온보딩 프로젝트에서는 ‘미니 이미지 어노테이터’ 를 개발하는 프로젝트를 수행하고 있습니다.
어노테이터란, 인공지능이 학습하기 위한 데이터를 만들기위해 라벨링 이라는 과정이 필요한데 사용되는 툴을 이야기 합니다.
데이터메이커의 이미지 어노테이터의 간소화된 버전을 개발하며, 프로젝트는 다음과 같은 목적을 가집니다.
- 어노테이터를 개발하는 개발자가 어떤것들을 고민해야하고 생각해야 할지 경험
- 기술적인 이해와 능력향상
- 새로운 아이디어와 노하우 공유
저의 온보딩 프로젝트 개요는 아래와 같습니다.
- 기간 : 1/5 ~ 2/2 (29일간, 약 한달)
- 스킬셋 : Typescript, Canvas API, Vue 3 Single File Component / Composition API, vanilla-extract
- 요구사항 달성도 : 70%
요구사항
실제 요구사항은 이보다 좀더 길지만 핵심적인것만 추려서 정리해보았습니다.
- 공통
- 편집한 어노테이션 정보가 JSON 형식의 텍스트로 저장 될수 있어야 한다.
- 모든 요소는 브라우저 비율 ( 확대 / 축소 ) 변화, 레티나 디스플레이 대응이 되어야 한다.
- 모든 요소는 줌에 의해 선의 두께나 폰트 등이 변경되지 않는 것처럼 보여지도록 해야한다.
- Classification
- name, color 가 포함되는 데이터 분류 목록이 존재해야한다
- Panel
- Classification
- 현재 생성할 Annotation 이 어떤 Classification 을 따를지 display 하고 선택할수 있어야 한다.
- Select Box 의 형태를 띄어야 한다.
- Annotation
- Annotation 의 순서, 이름 정보가 포함된 각 Annotation 의 속성을 수정할수 있어야한다.
- 각 Annotation 의 순서 수정 및 삭제를 할수 있어야한다.
- History
- Editor, Panel 등을 통해 어플리케이션이 실행한 동작 이력을 볼수 있으며, redo / undo 를 할수 있어야 한다.
- 동작에 대한 정의는 Annotation 의 크기 혹은 위치 변경 및 생성 / 삭제 시점에 히스토리가 발생해야한다.
- Classification
- Editor
- Cross Hair
- 화면에 마우스의 위치를 따라다니는 십자선이 존재해야한다
- 키보드의 + / - 키 로 선의 굵기를 조절할수 있어야한다.
- Image
- 이미지의 원본 가로x세로 비율이 항상 동일해야 한다.
- 이미지의 원본 화질 또한 항상 동일 해야하며 흐릿하지 않고 선명하게 display 되어야 한다.
- 브라우저 창, 에디터 엘리먼트의 레이아웃 사이즈 변경시에 이미지가 원본 비율에 맞게 가운데에 정확히 위치해야 한다.
- 이미지 로드 & 페인팅을 할수 있어야한다.
- 마우스 휠을 통해 이미지를 포인터를 중심점으로 점차 확대 / 축소 할수 있어야 한다.
- 스페이스바 + 마우스 좌클릭 / 휠 버튼 클릭 으로 드래그 할수 있어야 한다.
- 마우스 커서에 패닝 상태를 표시 해야한다.
- Annotation
- 바운딩 박스의 경우, x y w h 좌표로 구성되어야한다.
- 해당 좌표를 통해 이미지 위에 표시 하되, 이 좌표는 원본 이미지를 기준으로 함을 기본으로 하며 화면에 그려진 이미지의 사이즈에 따라 정확히 목표했던 위치에 Annotation 이 나타나야한다.
- classfication name, color, 순서 를 화면에 나타내야한다.
- 수정, 이동등 Annotation 의 위치 정보가 변할경우 이 정보가 최종 JSON 에 반영될수 있어야 한다
- 줌 인, 줌 아웃 시나 패닝 시, 레이아웃 사이즈 변경시에도 의도했던 위치에 Annotation 이 위치해야하며, 선 굵기 / 폰트 크기등의 디자인적인 요소가 유지되야한다.
- Annotation 은 단일 혹은 다중으로 선택이 가능해야한다.
- 단일 선택된 상태인 Annotation 은 수정이 가능해야한다.
- 선택된 상태인 Annotation은 이동이 가능해야한다.
- Cross Hair
- Tool
- Select
- 에디터 상에서 마우스 드래그를 통해 직사각형 형태로 선택할 영역을 잡아 각 Annotation 을 선택할수 있어야 한다.
- 줌 인 / 줌 아웃, 패닝, 브라우저 확대 / 축소, 레티나 디스플레이, 브라우저 창 사이즈 / 엘리먼트 사이즈 변경에 영향을 받지 않고 선택된 영역내의 어노테이션을 정확하게 잡아내야 한다.
- Bounding Box
- 에디터 상에서 마우스 클릭을 통하여 새로운 Annotation 을 생성할수 있어야 한다.
- x y w h 좌표는 원본 이미지 사이즈를 기준으로 하여 좌표를 구성한다.
- Bounding Box 생성시 에디터와 패널 모두 추가된 바운딩 박스의 정보가 실시간으로 반영되어야 한다.
- 마우스 첫번째 클릭 - 두번째 클릭으로 새 Annotation 을 생성할수 있어야 한다.
- 두개의 클릭 포인트 중 항상 좌상단에 위치하는 포인트가 첫번째 포인트 이어야 한다.
- Select
아이디어
과제 시작에 앞서, 전반적인 방향성을 어떻게 잡고 개발을 할까 생각해보았습니다.
- 각 어노테이션이나 클래스 등은 모두 class 를 사용해서 객체 지향적으로 만들기
- 캔버스위의 그래픽 객체 라고 생각하기
- 동작 (이벤트 핸들링) 은 최대한 공통적인 부분을 모아서 공용 함수 사용하기
게임 개발, 그래픽스 프로그래밍에서는 주로 그래픽 요소들을 object 라고 부르는것 처럼, 이번 프로젝트에서는 저도 객체 지향적인 사고를 좀더 개발에
녹여내고 싶은 생각이 들었습니다.
익숙하지 않은 접근 방법 이었지만, 개발자가 접근을 쉽게 하기 위한 방법중 하나라고 생각하고 실제로는 어떤면에서 효과적일지 궁금하기도 했습니다.
추가로 이런 객체들의 동작, 움직임, 인터렉션들은 공통적인 부분을 모아 메서드를 통해 핸들링을 해보고 싶었습니다.
저는 2D 그래픽 요소들을 구현하기 위해 Canvas API 를 선택했는데요, Canvas 를 다루는 라이브러리들은 Canvas 에 그래픽 요소를 그리기 위해 어떻게
접근하고 있는지 조사를 해보던중, Two.js 라는 라이브러리를 참고하게 되었습니다.
해당 라이브러리에 대해 깊게 이해를 해본것은 아니지만, 위에서 얘기했던 캔버스에 그래픽 요소를 그리기 위한 접근 아이디어가 비슷했다고 생각했습니다.
아래는 Two.js 공식문서에서 제공하는 Example 코드입니다.
이 라이브러리를 사용하려고 찾아온 개발자들의 입장에서 본다면, 중요한 것은 다음과 같은 의문입니다.
- 내부 동작을 알아야 하는가?
- 내개 원하는대로 그릴수 있는가?
- 그려진 오브젝트의 상태나 원하는 정보를 알아낼수 있는가?
- Vue 와 같은 프레임워크를 사용함에도 컴포넌트 라이프사이클에 맞추어 핸들링 할수 있는가?
위 코드를 보면 라이브러리의 내부동작은 모르더라도, 왠지 우리는 이 라이브러리를 통해 그래픽 요소를 그릴수 있을것 같은 생각이 듭니다. 왜냐하면 makeCircle, makeRectangle 메서드를 통해 ‘원’ 과 ‘사각형’ 을 그린다는 정보를 유추할수 있기 때문입니다.
우리는 그래픽 요소를 쉽게 그리고 싶기 때문에 이 라이브러리를 찾아왔지 ‘원’ 이 어떻게 그려지는지 아는것은 관심사가 아니기 때문에, 내부동작을 모른다고 한들 큰 문제가 되지 않을것 입니다.
이는 소프트웨어 개발 관점으로써 풀어내면, 관심이 없는 내용들을 걷어내고 추상화된 인터페이스를 제공하고 있다고 볼수 있습니다.
이러한 관점이 저는 그동안 프론트엔드 개발을 하며 항상 생각하고 공감을 느꼈던 부분이어서, 제 온보딩 프로젝트에서 녹여내보고 싶었습니다.
캔버스와 그래픽 요소에 대한 개념적인 접근
그래서, 개발자가 캔버스에 실제로 그래픽 요소를 그린다면 어떠한 문제들을 겪길래 이러한 생각들을 하는 것 일까요?
캔버스를 개발한 개발자의 결과물 === 플립북 애니메이션
캔버스에 무엇인가 연속적인 동작을 하는 그래픽 요소를 그려낸다고 하면, 그 개발자의 결과물은 플립북 애니메이션과 비슷하다고 말할수 있습니다.
매 이벤트 혹은 프레임마다 그리고, 지우고, 그리고, 지우고 … 이러한 동작의 연속입니다.
캔버스에서 어떠한 요소가 애니메이션 으로 이동하는것을 구현하기 위해서 이러한 경험을 한번씩 해보셨을 거라고 생각합니다.
이러한 동작의 특성 때문에, 코드는 매우매우 명령적이고 길어져 처음 개발자가 생각하던데로 잘 그려지지 않습니다.
어느순간 개발자는 원래 구현하고자 했던 목적이나 전체 그림을 잊고 머릿속은 매 프레임에 어떻게 그려내야할까 고민하게 되어버립니다.
특히 캔버스에 움직일 그래픽 요소가 여러개거나, 정보가 유지되거나, 폭죽 가루가 공중에서 휘날리는 애니메이션과 같이 유저 인터렉션과 별개로 부가적인 동작을 해야할 경우, 개발자가 일일히 컨트롤 하기 쉽지 않습니다.
이러한 접근 방식과 더불어 canvas context API 인터페이스 자체가 개발자의 관점을 방해하게 됩니다.
아래는 MDN 에 있는 canvas tranformation 예제 코드 입니다.
코드만 보고 어떠한 그림이 그려질지 예상이 가시나요?
무슨동작을 하는것인지 사실 눈으로 캔버스를 보기 전까지는 예상하기 어렵습니다. 저도 마찬가지로 그렇습니다.
▼ 정답
그래서 캔버스를 개발하는 개발자는 아래와 같은 니즈가 있을수 있습니다.
Needs …
- 개발자 김xx : 각 요소가 자기 자신이 어디에 위치 해야하는지 독립적으로 알고있으면 좋겠어 !
- 개발자 박xx : 각 요소 관점에서 캔버스 자체의 동작에 관심이 없었으면 좋겠어 !
- 개발자 윤xx : 각 요소가 인터렉션에 유연하게 반응했으면 좋겠어 !
왜 이런 니즈가 생겼을까요?
바로 사람이 생각하는대로 기대하고 프로그래밍 할수 있기 때문입니다.
이를 니즈를 만족하기 위해 그래픽 요소 하나하나가 ‘object’ 라면 어떨까요?
To Be …
- 화면상의 위치 정보, 클라이언트 인터렉션 간 상태 정보, 스타일 정보 등을 각 오브젝트가 담고 있고, 자신을 핸들링 할 방법을 메서드를 통해 제공한다면 그 오브젝트의 세부 사항은 더이상 외부에서 알 필요가 없어집니다.
- 오브젝트를 그리는 주체인 캔버스를 핸들링 하는것은 연속적인 애니메이션을 위한 ‘지우기’ ‘그리기’ 에 초점을 두어 관심사의 완전한 분리를 시켜줘야 할것입니다.
- 그 오브젝트 들은 모두 캔버스 위에서 동작하므로 캔버스의 특성을 모든 오브젝트가 공유해야 할것입니다.
객체 지향 적인 사고 에서도, 저는 특히 ‘객체’, ‘사물’ 의 개념에 좀더 집중했습니다. 그래픽 요소를 하나의 ‘사물’ 처럼 다루는 것이죠.
자바스크립트에서 객체 지향 프로그래밍은 조금 생소 할수도 있습니다. 보통 프론트엔드 개발을 할때 객체 지향적으로 개발을 할일이 많이 없기 때문이죠.
React 와 같은 라이브러리나 프레임워크의 진화 방향도 그 영향에 한몫을 하는것 같습니다.
좀더 클래식한 자바스크립트 문법을 떠올려 보면, 개념적으로 객체 지향을 따릅니다.
Prototype 상속을 통한 체이닝이 초창기 자바스크립트가 객체 지향을 구현한 방식입니다.
우리도 충분히 이 인터페이스를 따라서 객체 지향을 구현할수는 있습니다.
하지만 타 언어와 코드 생김새부터 이질적이고 생소한점이 많습니다.
하지만 ES6 에서 도입된 Class 와 타입스크립트를 활용하면, 우리가 아는 그 맛을 구현가능합니다.
타입스크립트와 class 를 활용하면 타 언어와 거의 유사하게 class 를 작성할수 있으며, public, private, protected 와 같은 명시적인 접근 제한자 (access modifier) 키워드를 제공합니다.
이는 타입스크립트 에서 제공하는 키워드이고, 타입스크립트의 타입 체커 환경에서만 동작합니다.
좀더 나아가서
위에서 다뤄본 개념적인 사항들을 적용함으로써 얻는 이득은 무엇이 있을까 하고 생각해봤습니다.
- 그래픽 요소가 자체적으로 정보을 담고 있어, 컴포넌트나 외부 요소에 독립적으로 관리 되도록 관심사의 분리 가 되는 목적이 있습니다.
- canvas element 나 canvas context 등을 자신 (인스턴스) 에게 할당하여 this 로 참조하고 재사용 할수 있습니다.
스코프가 달라짐에 따라 매번 canvas element를 셀렉팅 하거나 canvas context 를 얻어올 필요가 없습니다. - 주의 할점으로는, canvas element 나 canvas context에 live binding 되어 자체적으로 관리되는 attribute 나 value 를 굳이 인스턴스가 관리할 이유는 없습니다.
성능적인 문제가 아니라면 더더욱 그럴 필요가 없습니다.
예를 들면 canvas element 의 clientHeight 와 같은 값을 인스턴스 멤버로 값 자체를 할당하고 계속해서 참조 한다면, 실제 canvas element 의 clientHeight 가 변했을때에 인스턴스에 할당되어져 있는 clientHeight 값은 그대로 입니다.이는 일일히 값을 업데이트 하거나 하는등의 불필요한 작업을 추가로 해줘야 합니다. - 그래픽 오브젝트 전반적으로 자주 사용되는 메서드나 프로퍼티 를 Canvas Object 같은 클래스를 상속 받도록 구현한다면, 반복되는 코드를 줄일수 있을것 입니다.
CanvasObject.ts
거의 대부분의 클래스들의 Entry Class 인 CanvasObject 입니다.
캔버스에서 동작하기위한 메서드와 멤버들을 가지고 있습니다. 코드가 길고 복잡하지만, 사실 어떤 동작을 하는지 깊게 이해하고 알 필요가 없습니다.
이 클래스를 사용하는 개발자 입장에서는 그저 그리기, 지우기, 밀기 등의 메서드가 존재하는구나 라고 이해하면 됩니다.
그정도로만 넘어가도 사용하는데 큰 문제가 없습니다.
Line.ts
CanvasObject 를 상속받는 Line 클래스는, 이름에서 알수 있듯이 ‘선’ 을 그릴수 있게 해주는 클래스 입니다.
points 라는 멤버를 통해 Line Object 는 자기가 어느 점에서 시작해서 어느 점까지 이어질지 알고있습니다.
setPoints 메서드를 통해 Line 인스턴스가 이어질 지점들을 전달받고, render 메서드를 통해 지우고 화면에 그리는 동작을 할수 있습니다.
이 클래스를 사용하는 개발자는 이어질 점만 제공하고, 각 메서드들을 실행시키기만 하면 됩니다.
useCrossLine.ts
내부동작은 모르더라도, Line 클래스를 통해 ‘선’ 이라는 그래픽 요소를 그린다는 점과 render 메서드를 통해 ‘선’ 을 그리고 있다는것을 알수 있습니다.
‘선’은 점과 점의 연결이므로, setPoints 메서드를 통해 시작점, 종료점을 제공 해줍니다.
현재 코드에서는 mousemove 이벤트가 발생할때마다 그려지고 있습니다.
추상화된 인터페이스 제공함으로써, 개발자가 원래 원했던 ‘선 그리기’ 라는 목표에 좀더 빠르게 접근할수 있었습니다.
아래는 요구사항대로 구현했던, 십자선을 캡쳐한 이미지입니다.
전체 어플리케이션 흐름도
온보딩 프로젝트 간 어플리케이션이 어떤 구조가 되어 동작할지에 대해 생각하던것을 design document 로 작성 해보았습니다.
Client Side 데이터 관리 ( 어노테이션 )
전역 상태 관리 전략
- 전역 상태는 간단하게 reactive 로 반응형 변수를 생성후, 모듈로써 import 하여 store 로 사용하였습니다.
- classfication 과 같은 정적인 데이터는 자바스크립트 Map 객체를 사용하여 구성하였습니다.
큰 범주의 client state 관리는 위의 그림처럼, Editor 나 Panel 에서 Client Interaction 이 발생하면, ref store 에 있는 값이 변경되고, 반응형으로 변경된 값은 Editor 와 Panel 을 리렌더링하는 구조로 작성하였습니다.
대부분의 SPA 어플리케이션의 구조와 비슷합니다.
Store 구성
Panel 구성
annotationStore 와 annotationOrderList 를 import 하여 그대로 사용 하였습니다. 굳이 사용할 필요는 없지만 명시적으로 composable 을 통한 사용이 아니어서 조금 아쉽다고 느꼈습니다.
annotationOrderList 가 Array 프로토타입 메서드를 통해 변경될때마다 반응형으로 컴포넌트가 업데이트 되는 형태입니다
AnnotationViewer 구성
useAnnotationViewer 내에서 annotationViewer 인스턴스를 생성하고, 라이프사이클을 관리 하는데 여기서도 annotationStore 가 변경되면 다시 렌더링 하도록 하고 있습니다.
지금은 반응형 변수가 변경될때 어떤 방식으로 리렌더링을 하고 있는지 에만 초점을 두었습니다.
annotationViewer 의 메서드가 어떤 일을 하는지는 뒤에서 좀더 이야기 하도록 하겠습니다.
이어서…
다음 내용은 2부로 이어집니다.
1부에서는 캔버스에 그래픽 요소를 그리기 위한 개발적인 접근 방법에 대한 내용이 주된 내용이었다면, 2부에서는 추상화와 클린코드에 대한 관점을 좀더 집중적으로 다뤘습니다.
다음 포스트에서 뵙겠습니다 !