Tech

온보딩 프로젝트 개발기 - 2부

멀티 패러다임 프로그래밍을 통한 프론트엔드 클린코드

1부에서는 주로 캔버스를 개발하기위한 접근 아이디어와 객체지향적인 사고에 대한 이야기를 했습니다.
이번 포스트에서는 추상화와 클린코드에 대해 좀더 다뤘습니다.


Editor 의 구성

  • 각 캔버스는 하나의 레이아웃에 겹쳐있고, 가장 왼쪽 위부터 아래로 겹쳐져 있습니다
  • 각 캔버스 HTML element 에 매핑 되는 Vue 컴포넌트가 독립적인 인스턴스 형태로 존재합니다. 렌더링또한 각자의 역할이 존재합니다.
  • 이 인스턴스 들은 자기 관심사에 맞는 그래픽 요소를 그리기 위한 상태 데이터를 가지고 있습니다.

ImageViewer 의 State Info 를 다른 캔버스들이 참조하고, AnnotationViewer 의 State Info 를 ToolCanvas 가 참조합니다.

Tool 을 통해 사용자가 범위내 어노테이션 선택, 바운딩 박스 생성등의 동작을 하면 Custom Event 를 통해 그 정보를 전역으로 전달합니다.
AnnotationViewer 의 리스너들은 그 정보를 전달받아 선택, 생성등의 작업을 처리합니다.

각 Annotation 또한 독립적인 인스턴스 입니다. AnnotationViewer 는 각 Bounding Box 를 그리기만 할뿐, 각 Bounding Box 의 정보를 알고있지 않습니다.

이미지 어노테이터 특성상, ImageViewer 와 AnnotationViewer 는 항상 존재 하지만, Tool 은 언제든 다른 인스턴스로 교체될수 있습니다.
Tool 교체시에도 ImageViewer 나 AnnotationViewer 에게 영향을 주지 않도록 위와 같이 구성하였습니다.

Editor 영역을 나타내는 컴포넌트 입니다. 여기가 모든 Canvas 의 Entry 지점입니다.

useGetInstance 라는 컴포저블을 통해 전역에서 각 인스턴스를 가져오고, AnnotationViewerCanvas 는 imageViewer 가 존재할때, ToolCanvas 는 annotationViewer 와 imageViewer 모두 존재할때 렌더링 되도록 되어있습니다.

Canvas 컴포넌트들이 어떤일을 하는지 AnnotationViewerCanvas.vue 로 예시를 들어 보겠습니다.

element 의 렌더링은 CommonCanvas 에게 위임하고, 이 컴포넌트는 AnnotationViewer 인스턴스를 생성하고, 핸들링 하기 위한 컴포넌트 입니다.

IMAGE_VIEWER_ZOOM 이라는 커스텀이벤트가 발생할때, annotationViewer 는 setTransformRect 메서드를 통해 무엇인가 그려내는 일을 하고 있습니다.

useAnnotationViewer 컴포저블은 AnnotationViewer 클래스의 인스턴스를 생성하고, 컴포넌트 라이프 사이클과 반응형 상태 업데이트를 수행합니다.

annotationStore 에서 초기 데이터를 기반으로 instanceCreator 라는 함수를 통해 annotationViewer 인스턴스를 생성합니다.

첫 렌더링은 IMAGE_VIEWER_COMPLETE 이벤트가 발생할때 실행됩니다.

onUnmounted 에 의해 컴포넌트가 언마운트 될때, 내부의 각종 이벤트 리스너 등을 해제하는 destroy 메서드를 실행합니다.

watch 에 의해 annotationStore 의 반응형 상태가 변경될때, 변경된 어노테이션 데이터를 setAnnotations 메서드를 통해 업데이트 하고 다시 렌더링 합니다.

Rect 클래스는 annotationViewer 캔버스에 사각형 ( 바운딩 박스 ) 을 그리기 위한 클래스 입니다.

처음에는 캔버스 오브젝트로 다뤄서 작성을 진행 했지만, 바운딩 박스가 항상 사각형 이면서 이벤트 핸들링을 할 일이 많다고 느꼈습니다.
특히 캔버스 오브젝트 일 경우 화면에 영역을 그려내기 위한 계산을 개발자가 직접 해야하는 것이 많은데, 저는 브라우저의 레이아웃 계산을 사용하고 싶었습니다.

또한 처음 한번만 Rect 를 DOM 을 사용하여 그려내면, 지우고 다시 그려내는 일련의 과정을 반복할 필요가 없다는 장점이 있었습니다.
따라서 이벤트 리스너도 한번만 add 해주고 나면 캔버스 상에서 Rect 가 제거 될때까지 계속 사용할수 있는 장점도 있었습니다.

그래서 DOM 을 이용하여 구현을 하는것으로 재작성 하였습니다. 덕분에 단일 선택 상태시 resize 를 하기 위한 8개의 point 를 달아주는것도 css 를 이용해 간단히 구현할수 있었습니다.

위와 같이 자신의 정보와 그릴수 있는 메서드를 가진 rect 인스턴스는 AnnotationViewer 에 의해 editor 상에 렌더링 됩니다.

아래는 AnnotatinoViewer 상에 그려진 바운딩 박스를 조작하는 모습입니다

마지막으로, Editor.vue 에서 볼수 있었던 CommonCanvas 컴포넌트 입니다.

CommonCanvas 는 canvas element 를 그려내고, 모든 캔버스에서 공통적으로 사용되는 UI 로직을 재사용 하기위한 Wrapper Component 입니다.

CommonCanvas 의 자식 컴포넌트는 각 캔버스의 그래픽 인스턴스가 실행되는 컴포넌트로 canvas 의 context 를 가져와서 그래픽 요소를 그려냅니다.

위의 예시 에서는, CommonCanvas 의 하위 컴포넌트로 존재하는 ImageViewerCanvas 가 실제로 canvas element를 사용하여 드로잉 로직을 수행하는 인스턴스 입니다.

하위에 어떤 인스턴스가 존재하는지는 중요하지 않습니다. CommonCanvas 는 동일한 로직을 제공할 뿐입니다.

canvas 엘리먼트가 css 상으로는 width: 100%, height: 100% 으로 다루고 있지만, 영역은 그만큼 차지 하더라도 canvas context 의 메서드를 통해 무엇인가 그려낼때는 canvas DOM Attribute 의 width, height 를 참조하게 됩니다.

따라서 첫 렌더링 시 실제 영역 및 Resize 에 의한 레이아웃 변경시 width, height 어트리뷰트 동기화 로직이 필요하고, 해당 컴포넌트가 수행하고 있습니다.

이러한 컴포넌트를 사용함으로써 얻을수 있는 이득 들은, 캔버스를 핸들링하기 위한 UI 로직을 템플릿 구문만으로 선언적으로 재사용 할수 있었으며 자식 컴포넌트들은 항상 Canvas 엘리먼트가 렌더링 되어 있음을 보장 받을수 있다는 점입니다.

또한 자식 컴포넌트 들은 엘리먼트를 렌더링 하지 않더라도 그래픽 오브젝트 인스턴스들이 Vue 컴포넌트의 라이프사이클에 맞추어 동작이 가능해집니다.

CommonCanvas 는 마크업과 스타일을 같이 제공하는 목적도 있지만, 선언적으로 재사용 할수 있고 관심있는 내용만을 모아 두는데 의미가 있습니다.
이로써 canvas 엘리먼트를 렌더링 하는 대신, CommonCanvas 컴포넌트를 사용함으로써 원하는대로 동작할것이라고 기대하고 원래 개발하려고 했던것에 집중하여 개발할수 있습니다.
이렇게 추상화 함으로써 개발자 경험을 향상시킬수 있습니다.


컴포넌트 구현방향

저는 기본적으로 재사용 가능한 단위 부터 Bottom - Up 방식 컴포넌트 디자인을 선호합니다.
도메인에 얽메이지 않는 컴포넌트로 시작하여 내 프로젝트에만 사용할수 있는 컴포넌트로 끝나는 것이죠.
원자 단위의 컴포넌트에서 시작하여 페이지가 되는 것과 같은 맥락 입니다.

그렇지만 과제에서 크게 중요한 관점은 아녔습니다.
이러한 패턴과 전략을 완벽하게 적용하기에는 시간도 부족할 뿐더러, 유지 보수성과 미래의 생산성을 위한 방법론 이기 때문에, 완벽하게 따르는것이 오히려 적합하지 않은 방법이라고 생각했습니다.
그저 저의 성향, 영혼 같은 것 이라고 생각합니다.

제가 컴포넌트를 분리하는 기준들을 몇가지 이야기 해보자면 첫번째로, 레이아웃이나 다루고 있는 state, 데이터 별로 컴포넌트의 관심사가 가급적 1개 이상 늘어나지 않도록 구성 하려고 하는 편 입니다.

두번째로, 도메인이 포함되지 않은 컴포넌트들은 UI 컴포넌트로써 동작하고, UI 컨트롤 로직이 포함되어 있을수 있습니다.
stateless 한 컴포넌트로써 단순히 props 를 전달받아 렌더링 하는 컴포넌트 이거나, 이벤트를 핸들링 하고 사용자의 인터렉션 상태를 다루는 컴포넌트 들로 이루어집니다.

세번째로, 도메인이 포함된 컴포넌트들은 주로 화면에 나타내야할 데이터를 다룹니다. 다른 컴포넌트들의 조합으로 이루어 지며 데이터를 자식 컴포넌트나 전역 상태로 넘겨주는 역할을 합니다.


점진적인 컴포넌트 설계

여기서 이야기 하는 내용은 어떻게 보면 당연하고 기본적인 것들 이지만, 스스로도 개발하다보면 놓치기 쉬운것들 이라고 생각합니다.

프로젝트 내에서 전반적으로 공유할 버튼의 스타일 같이 도메인 색깔이 쏙 빠진 컴포넌트 들로 시작하는것이 가장 좋다고 생각합니다.
UI 컴포넌트 (Presentational Component) 가 가장 Best 입니다. 또한 단순히 Prop 을 받아 그리기만 하고, 상태가 존재하지 않는 Stateless Component 가 좋습니다. Stateless 함은 Business 로직은 물론, UI 컨트롤 로직이 존재하지 않는 다는 말과 같은 개념입니다.

몇가지 컴포넌트로 예시를 들어보겠습니다. 아래는 제가 프로젝트를 진행하며 작성했던 컴포넌트 들 입니다.


이 Button 컴포넌트는 단순히 스타일을 제공하고 자식 컴포넌트를 받아 렌더링 하기만 하는 UI 컴포넌트 입니다.

이렇게 아주 간단한 UI 컴포넌트 하나로 먼저 시작해서 점차 다른 컴포넌트에 활용 될것입니다.

ToolButton 컴포넌트는 Button 컴포넌트를 사용하고 몇가지 Prop 을 전달받아 icon 과 스타일을 단순히 그려내는, Stateless 한 UI Component 입니다. 사용 범위가 Button 컴포넌트에 비해 줄어들었습니다.

SelectTool 컴포넌트는 ToolButton 을 사용해서 특정한 아이콘과 이벤트 핸들링을 하는 컴포넌트 입니다. 재사용 하기위한 컴포넌트를 가져와서 도메인이 포함된, 내가 실제로 UI 상에 나타낼 컴포넌트가 되었습니다.

Button 컴포넌트의 또다른 활용 예시가 있습니다.

해당 컴포넌트는 ToolBar 컴포넌트와 Button 컴포넌트를 사용하고, 컴포넌트의 click 이벤트를 부모로 emit 하고 있습니다.
이와 같이 컴포넌트가 컴포넌트들의 조합으로 이루어질때 반복적일수 있는 코드를 선언적으로 재사용 가능하게 잘 설계하였다고 볼수 있습니다.
제 생각에는 좋은 컴포넌트 설계는 다른 컴포넌트의 조합으로 이루어지도록 설계하고 구성하는것이 아닐까 생각합니다.
이런 컴포넌트들은 개발자의 경험을 향상시키는데 큰 역할을 합니다. 디자인 시스템을 통해 컴포넌트를 만들때의 이점 이라고도 볼수 있습니다.

최대한 도메인을 포함하지 않은 컴포넌트로 시작해야한다고 했는데, 점점 포함될수록 컴포넌트에는 어떤 차이점이 있을까요?

위의 그림은 도메인이 포함될수록 하는 일의 양이 얼마나 늘어나는지 도식화 한 그림입니다. 도메인이 입혀지면 입혀질수록 해당 컴포넌트는 하는일이 많습니다.
도메인이 많이 포함된 컴포넌트는 디자인을 포함한 API 응답 데이터, 클라이언트 인터렉션에 의한 상태등이 포함될수 있습니다.

컴포넌트의 활용도와 재사용성의 관점에서는 반대의 결과가 있습니다. Button 은 활용도가 높고 여기저기서 빈번하게 사용될수 있지만, SelectTool 은 프로젝트내에서 딱 한번만 쓰일수도 있습니다.

어찌보면, 당연한 이야기인 것처럼 들리기도 합니다. 😅

결론적으로 생각은 누구나 할수 있고, 항상 그렇듯 실천이 중요하다고 생각합니다.
계속해서 바텀 업으로 컴포넌트를 개발하고, 개념적으로 이 컴포넌트는 어떤 상황에서 항상 사용될것이라 기대하며 설계하고, 이 컴포넌트를 사용하는 개발자의 관점에서 컴포넌트의 사용성을 고려해야합니다.
탑다운 방식의 개발이 익숙하다면 컴포넌트를 바텀업 방식으로 설계하는것이 생각보다 쉽지 않고 시간이 오래걸림을 느끼게됩니다.

이런 프로그래밍 방향성을 CDD ( Component Driven Development ) 라고 합니다. 이 단어를 처음 사용하신분은 Tom Coleman 이라는 분이 먼저 이야기 하셨다고 합니다. ( https://twitter.com/tmeasday )

누구나 한번쯤은 해보았고 익숙할법한 Page → … → Component 작업의 역순이 바로 CDD 방식입니다.
컴포넌트 네이밍 부터 도메인을 빼는 습관을 들이고, 컴포넌트를 함수형 프로그래밍에서 이야기하는 단순한 역할을 하는 함수라고 생각하며 그 함수의 조합으로 페이지가 완성되어져 간다고 기대하며 작업하는 방식입니다.
디자인 시스템을 구축하는 방식도 이러한 관점을 길러나가기 좋다고 생각합니다.


재사용 가능한 로직

컴포넌트 에서는 다양한 클라이언트 인터렉션에 의한 상태 처리뿐만 아니라, 컴포넌트 라이프사이클에 실행하는 로직들이 포함되어있을수 있습니다.
이러한 로직을 컴포넌트와 함께 담아 재사용 컴포넌트로 사용하는 패턴도 좋지만, 이러한 로직만을 분리하여 컴포저블 ( 커스텀 훅 ) 과 같은 형태로 분리하는 것도 좋은 방법입니다.
다만, 대부분의 로직이 함수에 감춰져 컴포넌트가 무슨일을 하는지 알수 없게 하는것은 대표적인 안티패턴 이므로 주의해야합니다.

아래는 제가 온보딩 프로젝트 진행간 설계하고 유용하게 사용했던 컴포저블들을 소개합니다.

useEventListener 는 Vue 공식문서에서 거의 그대로 채용한 이벤트 리스너를 컴포넌트 라이프사이클에 맞춰 등록하고 제거하는 로직을 추상화한 컴포저블 입니다.
거의 모든 컴포넌트의 이벤트 등록에 사용할수 있었습니다.

eventManager 는 클로져를 이용하여 원할때 이벤트 리스너를 remove 하기 쉽게 도와주는 간단한 유틸 함수입니다.
위에서 소개한 useEventListener 는 Vue 컴포넌트에서만 사용할수 있습니다. eventManager 는 컴포넌트가 아닌 경우에 사용하기 위해 만들어졌습니다.

위에서 봤었던 AnnotationViewer 클래스와 같이, annotationViewer 인스턴스 내부로직에서는 컴포넌트 라이프 사이클메서드를 사용할수가 없습니다.
이런 상황에서 이벤트 리스너를 스코프에 제약받지 않고 언제든 등록과 제거를 쉽게 할수 있도록 추상화한 유틸함수를 만들어 대부분의 클래스 에서 사용하였습니다.


여기까지 오며

이 글에서 앞서서 객체 지향, 선언형, 명령형, 함수형, 컴포넌트 디자인 패턴 등을 이야기 해왔던 이유는 무엇일까요? 혹은 부제가 ‘멀티 패러다임 프로그래밍을 통한 프론트엔드 클린코드’ 인 이유가 무엇이었을까요?
‘자바스크립트는 왜 그럴까 ?’, ‘프론트엔드는 왜 그럴까 ?’ 라는 의문을 가져본적이 있다면 그 답이 될수 있을것 같습니다.

먼저, 자바스크립트는 태생이 멀티 패러다임 프로그래밍 언어입니다. 다양한 관점으로 코드를 설계하고 작성할수 있기때문에 작성자나 코드를 읽는 개발자에 입장에서는 혼동을 느끼기 쉽습니다.
그렇지만 멀티 패러다임 프로그래밍 언어이기 때문에 얻을수 있는 장점도 많이 있습니다.
여러 프로그래밍 관점으로 ‘적절한’ 추상화를 통해 코드의 가독성을 높이고 개발자가 접근하고자 했던 본질, 문제해결에 도움을 줄수 있습니다.
그때 그때 상황에 맞게 적절한 전략을 취해서 좋은 코드를 만들어 낼수 있는것 이죠. 이런것이 ‘자바스크립트 다움’ 이 아닐까 감히 생각해봅니다.

그래서 Vue 와 같은 특정 프레임워크나 라이브러리를 사용함으로써, 일관된 관점과 코드 스타일을 가지고 접근하도록 하는것도 좋은 전략이 될수 있습니다. 개발자의 혼동을 줄여주는 장치인 셈입니다.

이러한 다양한 접근 방법을 통해 적절한 추상화를 이루는것이, 프론트엔드 클린코드 라고 생각합니다.

클린한 코드를 유지하는것 만으로도 기술부채가 해소되는 효과를 얻을수 있습니다. 사실, 성능에 의한 기술부채는 프론트엔드 개발자에게 스트레스를 주는 부분이 아닙니다.
대부분은 기존 코드를 파악하고, 사이드 이펙트가 발생하지 않는데 만전을 기울여 한시간동안 코드를 몇줄만 수정하는 일이 비일비재 합니다.
이러한 작업은 개발자에게 스트레스를 유발하기 쉽고, 버그 수정이 또다른 버그를 낳는 위험에 처하게 만듭니다.

항상 스스로에게 하는 다짐으로 ‘정성스럽게 작성하고, 부지런하게 청소하자 !’ 라는 말을 합니다. 특히 자바스크립트는 더러워지기 아주 쉽다고 생각합니다.


회고

계획과 얼마나 일치했는가? 생각해보면 사실 60% 정도 밖에 달성하지 못한것 같습니다. 항상 그렇듯 누구에게나 계획은 있습니다. 시작하기 전 까지는요.

왜 계획을 달성하지 못하였는가 스스로 생각해보았을때 몇가지 이유가 있었습니다.

사실 처음 개발해보는것들이 꽤 많았다고 생각합니다. 저는 지금까지 Vue 를 사용해본적도 없었고, 심화된 캔버스 조작도 처음 해보았습니다.
구현에 필요한 작업이 예상했던것 보다 많이 필요 했던것도 있었고, 생각보다 일찍 끝난거도 많았습니다.
다만 학습에 걸리는 시간은 생각 했던것과 비슷했습니다. 위 그림에서의 고양이 꼬리처럼 세부적인 기능 구현사항이 상당히 많았던것이 계획과 많이 틀어졌던것 같습니다.

이 프로젝트를 진행하면서 느낀점은, 구현 요구사항과 레퍼런스를 좀더 자세히 파악하고, 어떻게 설계하고 작업할지 조금더 깊이 고민해보고 도식화 까지 해보면서 큰 그림을 잡고 들어갔다면 더욱 좋았을것 같습니다.
처음 구현 의도보다 클래스의 역할이 비대해지는등의 문제를 계속해서 겪었습니다.
그렇지만 자바스크립트 어플리케이션 설계 및 구현, 캔버스 및 2차원 그래픽스 프로그래밍 심화학습을 할수 있어 좋았습니다.
특히, 구현간에 생각을 정말 많이 해볼수 있었던 온보딩 프로젝트지 않았나 생각했습니다.

앞으로의 목표로는, 좀더 유연하고 사용성이 좋은 설계를 하는데 도전해보고 싶은것들이 많이 생겨난것 같습니다. 또한 시간이 부족해 많이 알아보지 못한 그래픽스 OOP 에 대해 좀더 알아보고 싶다는 생각을 많이 했습니다.
나아가서 나 뿐만 아닌 다른 개발자 분들의 생산성이 배가될수 있도록 기여하고 싶다는 생각이 들었습니다. 데이터메이커 프론트엔드 개발자 모두가 10x 엔지니어가 될수있도록 많은 기여를 해보고 싶습니다 !


이 글을 마무리하며

개발자의 머릿속, 체감은 항상 난관에 부딪힙니다. 그렇기 때문에 끝내고 나서 정리가 중요한것 같습니다.
클린코드에서 이야기하는 보이 스카우트 정신을 발휘하여 올때보다 떠날때 더 깔끔하게 치우는 습관을 들여야 겠다는 생각을 자주 합니다. 특히 단순한 코드일수록 자꾸 생각나고 다시 찾게 되었던 기억이 납니다.

서비스에서의 플랫폼 - 유저 관계와 마찬가지로 개발자는 코드제공자 - 코드사용자 관계또한 존재합니다. 저는 사용자의 경험을 중요시하는 만큼 다른 개발자의 경험또한 중요시 하며 개발하는 사람이 되고 싶다고 생각합니다.

다시한번, ‘생각은 누구나 할수 있다. 항상 그렇듯 실천이 중요하다’ 라는 문구를 다짐해봅니다.

Get Started Today
with datamaker

데이터메이커 시냅스를 회사에 도입하고 싶으시다면,
아래 의뢰하기 버튼을 눌러주세요!

Synapse 문의하기