오늘은 이전에 강의를 들으며 만들었던 문벅스 애플리케이션을 컴포넌트 단위로 분리해보며 리팩토링 하며 학습한 내용에 대해 공유해보려고 한다.
이 코드는 위 사진에서 확인할 수 있듯이 메뉴를 하나의 TODO LIST 처럼 관리하기 위한 애플리케이션의 실제 동작 관련 코드이다.
하나의 앱에 모든 코드들이 App 컴포넌트 내에 들어가 동작하고 있는 것을 확인할 수 있다.
App 이라는 클래스가 하고 있는 작업과 상태 들은 다음과 같다.
메서드
- addMenu - 입력 창을 누르거나 확인 버튼을 누르면 메뉴를 등록한다.
- removeMenu - 삭제 버튼을 누르면 해당 메뉴를 제거 한다.
- updateMenuCount - 등록을 완료 & 제거 후 총 메뉴 갯수를 업데이트 한다.
- updateMenuName - 수정 버튼을 눌러 메뉴 이름을 수정할 수 있다.
- updateSoldOut - 품절 버튼을 누르면 메뉴가 품절 처리 될 수 있다.
이벤트 핸들러 & 렌더링 관련 메서드
- handle~ - 1-5번의 기능에 대해 이벤트 처리 할 수 있다.
- initEventHandlers - 이벤트 핸들러 들을 관리할 수 있다.
- render - 어떤 특정 작업이 완료되면 변경 된 DOM을 업데이트 하여 브라우저에 그릴 수 있다.
상태
- menu - 앱 내에서 관리되고 있는 메뉴
- curCategory - 현재 관리 중인 메뉴
- init - 앱 실행 시 최초로 실행되는 함수
정리 해보면 앱 이라는 클래스는 menu, curCategory, init이라는 상태를 가지고 있으며 메뉴 입력, 메뉴 등록/수정/삭제, 메뉴 총 갯수 및 품절 상태 업데이트의 기능을 수행하고 있다.
한 클래스에서 모든 기능 들을 절차지향적으로 수행하고 있는 코드라고 볼 수 있다.
이러한 코드는 흔히 좋지 못한 코드라고 하는데 왜 좋지 못한 코드라고 할 수 있을까?
현재는 10가지 미만의 일을 수행하는 200줄 가량 코드의 아주 작은 앱이지만 개발을 진행하며 다양한 기능 들이 변경 및 추가(예시 : 등록 한 메뉴의 총 매출 확인 기능(추가), 각 카테고리(메뉴) 마다 10개의 메뉴만 등록 가능(변경)) 된다면 저 200줄 짜리 코드에 대해서 변경하고 수정하고 삭제해가며 유지 보수 해야할 것이다.
심지어 지금은 200줄 짜리 코드 이지만 200줄이 1000줄이 되고 10000줄이 된다면 그 땐 정말 유지보수 하기 어려워질 것이다.
이렇게 애플리케이션이 변화되며 유지보수가 어려워지게 되면 당연히 코드를 한줄 한줄 해석하는데 더 많은 시간이 소요될 것이며 이는 개발 속도 저하 및 효율성 저하로 이어지게 될 것이며 만약 다른 신입 개발자가 코드를 건드릴려고 하면 더 많은 시간이 소요될 것 이다.
만약 에러가 발생하게 된다면 어떤 부분에서 에러가 발생했는지 원인을 추적하기 어려워 질 것이며 디버깅을 하는데 많은 시간이 소요 될 것이다.
변경 사항이 생길 때 마다 앱을 유닛 테스트, 통합 테스트, E2E 테스트 등을 수행해야 한다면 기능 들이 모두 한 클래스에 종속되어 있을 수도 있기 때문에 심하면 모든 기능 들을 테스트 해봐야 할 수도 있다.
이러한 이유 들로 부터 정의 내린 "좋은 코드"는 다음과 같다.
- 하나의 "책임" 단위로 분리되어진 코드
- 디버깅 하기 쉽고 가독성이 좋은 코드
- 확장성 있고 변경에 유연한 코드
그렇다면 어떻게 App 컴포넌트 내 존재하는 코드들은 단일 책임을 가질 수 있으며 이로 인해 좋은 코드를 가질 수 있을까?
이에 대해 컴포넌트를 떠올려 볼 수 있다.
프로그래밍에 있어 재사용이 가능한 각각의 독립된 모듈
프론트엔드에서의 컴포넌트는 "작업" 단위로 하나의 퍼즐 처럼 분리한다. 그 후 컴포넌트들을 조립하여 하나의 애플리케이션으로 동작하도록 하기 위한 프로그래밍 기법이다.
복잡한 인터렉션을 가진 요즘의 웹 애플리케이션들은 대부분 컴포넌트 단위로 개발하고 있다.
간단한 예시를 참고 해보며 컴포넌트에 대해 이해해보자.
이 TODO 애플리케이션은 다음과 같이 이루어져 있으며 수행하는 일은 다음과 같다.
- 할 일을 입력한다.
- 추가 버튼을 누르면 할 일이 등록된다.
- 체크 박스 상태에 따라 완료 상태가 업데이트 된다.
수행하는 일에 따라 다음과 같이 컴포넌트로 분리해볼 수 있다.
잘게 쪼개어 냈지만 크게 나누면 다음과 같다.
- ToDoInput (할 일을 입력한다.)
- ToDoButton (추가 버튼을 누르면 할 일이 등록된다.)
- ToDoList - ToDoListItem (체크 박스 상태에 따라 완료 상태가 업데이트 된다.)
이렇게 컴포넌트 단위로 앱을 관리하게 된다면 만약 "등록된 TODO에 대해 수정-삭제-완료가 가능해야 한다."와 같은 기능이 추가 되어 ToDoList 컴포넌트에 수정 - 삭제 - 완료 버튼이 추가 되어야 할 경우 ToDoList 컴포넌트만 변경하면 되어 유지보수가 전 보다 편해질 것이다.
이전에 만들었던 문벅스 앱에 대해 컴포넌트로 분리해보면 다음과 같은 구조를 가지고 있었다.
- 빨간색 - Header, Wrapper (위에서 부터)
- 파란색 - NavButton, Title, Count, ToDoForm, ToDoItems (위에서 부터)
- 주황색 - Input, Button (위에서 부터)
이 모든 컴포넌트 들을 MainPage라는 page 단위 컴포넌트 내 포함되도록 하였으며 이 페이지 컴포넌트를 최상단 컴포넌트인 App 컴포넌트에 포함 시켜놓은 형태의 구조를 띄고 있다.
HTML 처럼 뼈대가 될 컴포넌트 클래스는 다음과 같은 구조로 이루어져 있다.
상태
- $target - 자식 노드 들이 추가 될 타겟 노드(부모 컴포넌트)
- props - 자식 컴포넌트에게 주입 할 데이터
- state - 컴포넌트 내에서 관리 되어지는 상태
메서드
- constructor - 생성자 로써, 컴포넌트가 생성되면 target과 props를 초기화 및 초기 상태, 이벤트 핸들러를 설정 후 컴포넌트를 렌더링 하는 메서드
- init - 초기 state를 설정 하는 메서드
- mounted - 부모 컴포넌트가 렌더링 한 이후 자식 컴포넌트를 렌더링 하기 위한 메서드
- template - 컴포넌트가 렌더링 될 마크업(html)을 반환하는 메서드
- render - 컴포넌트를 렌더링 후 mounted를 호출하는 메서드
- setEvent - 이벤트 핸들러를 설정하기 위한 함수
- setState - 상태를 변경하기 위한 메서드
컴포넌트 클래스를 통해 부모 - 자식 컴포넌트를 상속 하여 하나의 앱을 구축하여 쉽게 관리할 수 있다. 전체적으로 React의 Class 컴포넌트와 유사한 구조임을 알 수 있었다.
index.html
index.js
App.js
id가 app인 노드에 MainPage 컴포넌트 들이 모두 들어갈 수 있도록 세팅을 진행했다.
- 컴포넌트 생성자 함수 호출
- constructor 메서드에서 $target, props 초기화한 다음 init 메서드를 통해 state를 초기화 하고 setEvent 메서드로 이벤트 핸들러를 세팅 후 render 호출
- render에서 template 메서드를 호출하여 얻은 html string을 target에 innerHTML로 추가 후 mounted 메서드 호출
- mounted 메서드를 통해 자식 컴포넌트 생성 (최하단 컴포넌트 까지 1 - 4의 과정 반복)
컴포넌트가 렌더링 되기 까지는 위 과정과 동일하다.
MainPage 컴포넌트를 보면 이런 생각이 들 수 있다.
"아까 절차 지향적인 코드랑 다를게 뭐지?"
하지만 현재 코드에서 자세히 보면 template 메서드에선 컴포넌트를 추가하기 위한 wrapper만 있는 것을 확인해볼 수 있다.
또한, 페이지 내 존재하는 기능에 대한 메서드와 상태들을 모두 page 단위 컴포넌트에서 관리하고 있는 것을 확인할 수 있다.
모든 메서드와 상태를 가장 상위 컴포넌트에서 관리 하는 이유는 다음과 같다.
mounted 메서드를 살펴보면 2번째 인자로 state와 메서드 들을 넘겨주고 있다. 즉, 2번째 인자인 props를 통해 자식 컴포넌트가 부모 컴포넌트에게 데이터를 받을 수 있다는 의미이다.
이는 해야 할 일을 자식 컴포넌트에게 위임 한다고 볼 수 있으며 이를 통해 해야 할 일을 알맞게 배분 할 수 있다.
NavButton.js
NavButton의 경우 위 사진의 버튼을 누르게 되면 위 title이 변경되며 해당 카테고리의 메뉴들을 불러오는 역할을 수행 한다.
이런 식으로 원래 App 클래스에서 하던 일을 컴포넌트 단위로 분리함으로써, 관심사를 분리하고 단일 책임에 맞게 일을 수행할 수 있기 때문에 유지 보수 하기 수월해진다.
ToDoInput.js
ToDoButton.js
ToDoInput과 ToDoButton의 경우 모두 메뉴를 추가한다는 공통된 기능을 가지고 있는 컴포넌트 들이다. 만약 각 컴포넌트 마다 addMenu를 추가하게 된다면 코드 길이가 불필요하게 길어지게 될 것이다.
하지만, 부모 컴포넌트에서 props로 메서드를 내려 받음으로써, 코드 길이도 줄어들게 되고 추후에 다른 컴포넌트에서 addMenu를 필요로 한다면 부모 컴포넌트로 부터 props로 받아 사용할 수 있기 때문에 변경에도 유연하게 대처할 수 있는 확장성 있는 구조를 가지게 된다.
주로 React나 Next.js를 통해 프론트엔드 개발을 해오다가 Vanlia JS로 컴포넌트 구현을 해보며 라이브러리나 프레임워크가 제공해주는 기능들이 정말 편리하다는 것을 알게 되었다.
리액트도 결국 라이브러리 이기 때문에 다른 라이브러리 들을 install 하여 일일히 구현한다고 생각해서 번거롭다고 느꼈었는데 hooks를 사용하지 않고 (useState 같은) 컴포넌트 클래스를 정의 후 일일히 직접 구현해보며 리액트가 정말 편리한 UI 라이브러리 임을 체험해 볼 수 있었다.
props나 state가 react에서만 제공하는 하나의 신기술 같은 것이라고 생각했지만 원리를 이해하면 자바스크립트로 충분히 구현 가능하다고 생각했으며 정말 간단한 애플리케이션을 제작한다면 굳이 리액트를 사용하여 번들 사이즈를 늘릴 필요가 없을 수도 있겠다는 생각을 가지게 되었다.
이번 문벅스 앱을 구현해보면서 굳이 데이터가 필요하지 않은 곳에서도 props로 데이터를 내려줘야하는 props drilling 현상이 있었다.
ToDoWrapper.js
이 ToDoWrapper의 경우 props로 데이터를 사용하지 않음에도, 그저 자식 컴포넌트에게 데이터를 내려주는 역할에 불과하고 있다. 즉, 불필요하게 props가 사용된다는 느낌을 받았으며 이런 컴포넌트가 많아진다면 리덕스와 같은 전역 상태 관리가 필요할 수도 있겠다라는 생각이 들었다.
또한, 컴포넌트를 렌더링하는게 react 처럼 그냥 렌더링 되는 것이 아닌 data-component라는 attribute를 가진 노드를 만들어 일일히 렌더링 해야하다보니 불필요한 div 태그가 많이 소모되게 되었다.
하지만 innerHTML을 사용하여 렌더링을 하기 때문에 4개의 컴포넌트를 todoWrapper id를 가진 노드를 타겟으로 잡게 되면 4개 컴포넌트 중 하나의 컴포넌트만 렌더링 되게 된다는 한계점도 가지게 되었다. 이는 단순히 나의 설계 미스 일 수도 있겠다는 생각이 들었고 다른 대안을 추후에 찾아봐야 할 것 같다.
https://github.com/jinyoung234/moonbucks-refactoring