VueJS 가이드 7 - 인터루드 & 리펙터

VueJS를 통한 애플리케이션 개발에 대해 알아봅니다.

작성자

조덕기

조덕기

DanielCho

본 포스팅은 Matthias HagerVue.js Application Tutorial - Step 7: Interlude & Refactor를 저자의 허락하에 번역한 글입니다. 오탈자, 오역 등이 있다면 연락부탁드립니다.

우리는 첫 번째 목표를 달성했다. 많은 피, 땀, 그리고 상상의 눈물을 쏟아내면서 대부분 안정적인 계정 모듈을 (대부분) 완성했다. 우리가 나중에 추가하거나 변경할 것이라는 점에는 의심의 여지가 없지만 지금 완전히 동작하고 있다. 바깥으로 나가서 신선한 공기를 마실 시간이다.

이 모듈을 코딩하는 동안 몇 가지 거슬리는 점들이 있었다. 사실 필자는 코딩할 때 공책을 책상 위에 올려놓고 메모를 하고 해야할 작업을 써놓는 경향이 있다. 필자는 이 연습을 강력히 권장한다. 이건 TODO 코멘트로 코드를 빽빽이 채우는 것의 아날로그 버전인데 (나는 아직도 이걸 가끔씩 한다!), 차이점은 모든 것이 한 곳에 있고 더 유연하다는 것이다. 필자는 공책을 나눠서 메모 또는 스케치를 하고 복잡한 문제를 해결한다. 스프링노트를 사용하여 항상 평평하게 펼쳐놓는게 좋다.

필자를 괴롭히고 있던 모든 것들은 노트에 기록되어 있다. 보통 이 시점에서 노트의 기록들을 프로젝트 관리 시스템에 수행해야할 작업이나 버그 리포트로 올려놓는다. 필자는 개인적으로 Kanboard를 사용하는데, 매우 강력하고 유연하기 때문이다. 이 툴은 작업들을 잊지않게 해준다. 특히 하나의 항목이 필자를 아쉽게 만들었는데, 이는 바로 create / add, edit / update 및remove / delete를 일관성 없이 썼다는 점이다.

필자는 CRUD 용어를 굳게 지키려고 노력하는데, 여기서는 아쉽게도 실패했다. 이제 당신의 methods를 가장 편한 방식으로 불러도 된다. create 대신에 add를 쓰고 싶다면 그렇게 해도 된다. 문제는 필자는 계속 왔다갔다 한다는 것이다. 액션은 addAccountupdateAccount 이면서 컴포넌트는 CreateEditAccount인 것이다. deleteAccount 액션에서는 removeAccount API 메서드를 쓴다.

(read 역시 동일하다. retrieve는 허용할 수 있지만 데이터를 채울 때는 load를 선호하고 실제로 서버나 데이터베이스에서 가져올 때는 fetch를 선호한다.)

이 시점에서는 사소한 문제로 느껴질 수도 있지만, 3주 후에 이 코딩을 다시 할 때 메서드를 edit으로 했는지 update로 했는지 기억이 나지 않으면 정말 난감해질 수 있다. 더 심각한건 우리는 이제 다음 모듈로 뛰어들기 시작할텐데 이 불일치는 여기서부터 점점 커질 것이라는 점이다. 따라서 리팩터를 만들 좋은 시간이다. 근본적으로 변하는 것이 없으므로 여기에서는 코드를 살펴볼 필요가 없다.

동시에 모든 목록 컴포넌트에서 view를 제거 할 것이다. 이건 필자가 Django에서 가져온 습관인데, 모든 컴포넌트가 어떤 의미에서는 이미 뷰 파일이기 때문에 AccountsListView.vue라고 부를 필요는 없다고 생각한다. 어떻게든 계정 목록과 같은 page 컴포넌트, 탐색바와 같은 partial 컴포넌트 그리고 날짜 선택기 같은 helper 컴포넌트간 구별하는 것이 좋겠지만 이 애플리케이션에서는 큰 문제라고 생각하지 않는다. 어차피 네이밍 스키마가 아닌 프로젝트 구조를 통해 이를 해결할 수 있을 것이다.

저건 해결됐으니 이제 코드에서 리팩토링이 필요하다고 생각되는 다른 부분을 찾아보자. 어떤 코드가 만족스럽지 않다면 결국 나중에 해결해야할 '할 일'들을 쌓아놓거나, 아니면 돌아가서 즉시 수정하자. 지금 보고 바로 이해할 수 없는 내용에는 코멘트를 달거나 리팩토링해야 할 수도 있다. actions.js 파일에서 한 줄이 눈에 띄었다.

Object.keys(res).forEach((key)=> {accounts[res[key].id] = res [key]; });

API에서 반환된 데이터를 저장소에 필요한 포맷으로 변환하는 무해한 한 줄 코드이다. 좀 엉망이긴 하지만, 내가 본 (또는 쓴) 최악의 코드는 아니다. 몇 주 후에 이 줄을 보면 즉시 무엇을 하는지 알 수 있을까? 아마 추론을 할 수 있겠지만 목적은 즉시 명백하지 않을 것이다. 여기에 코멘트를 약간 달 수 있다. 그러나 데이터베이스에서 불러오는 모든 유형의 데이터에 대해 비슷한 작업을 수행해야 할 것이다. 그냥 이걸 어딘가에 유틸리티 기능으로 바꾸는 게 좋겠다.

이제 문제는 이게 API에 속할까 아니면 액션에 속할까이다. 작업이 데이터를 저장하기 전에 예상치 못한 방식으로 데이터를 조작해야 할까? API가 항상 필요한 포맷으로 데이터를 제공해야 할까?

이러한 질문은 실제 기능보다는 프로그래밍 원리의 문제이다. 실제로 이 상황에서 이 코드가 어디에 속해 있는지는 중요하지 않다.하지만 애플리케이션이 복잡해지고 한 줄의 코드가 길어지고, 포맷 문제로 인해 간헐적으로 데이터가 손실되는 치명적인 버그를 찾아내고, API 코드에 대해 수 백 번의 테스트를 실행 했는데도 알아내지 못하고 있는데 끝내 문제는 애플리케이션이 Vuex 작업의 데이터를 변경하고, 버그가 실제로는 거기에 있었음을 알기 전에 시간을 엄청나게 낭비할 것이다! 이 사소해 보이던 문제가 꽤나 심각한 결과를 초래한 것이다.

많이 왔다 갔다 했지만, 많은 모듈을 사용하기 때문에 결국 프로세싱 코드를 src/utils.js 파일로 옮기기로 결정했다. 더 많은 글로벌 API 펑션을 작성하게 되면 src/api.js 파일을 만들겠지만 지금은 필요하지 않다. 그런 다음 실제 변환을 API 코드로 옮겼다. 우리가 만드는 API 레이어는 사용 가능한 데이터를 반환해야하므로 그곳이 가장 좋은 장소라고 느꼈다.

src/app/accounts/api.js

import localforage from 'localforage';
import { processAPIData } from '../../utils';

const ACCOUNT_NAMESPACE = 'ACCOUNT-';

export const fetchAccounts = () => {
  return localforage.startsWith(ACCOUNT_NAMESPACE).then((res) => {
    return processAPIData(res);
  });
};

...

src/utils.js

...

export const processAPIData = function (data) {
  /*
  Converts the data formatted for IndexedDB / API into the format
  our application uses.
   */
  let res = {};
  Object.keys(data).forEach((key) => { res[data[key].id] = data[key]; });
  return res;
};

src/app/accounts/vuex/actions.js

...

export const loadAccounts = (state) => {
  // loads accounts only if they are not already loaded
  if (!state.accounts || Object.keys(state.accounts).length === 0) {
    return fetchAccounts().then((res) => {
      state.commit('LOAD_ACCOUNTS', res);
    });
  }
};

마지막으로, 작지만 위험한 버그를 발견했다. 계정을 편집하면 잔액이 사라진다! 이래서 코드에 맞는 테스트 세트를 만들어야한다. 문제를 바로 발견했을 것이다. 코드를 보면 CreateUpdateAccount.vue에서 업데이트할 때 계정 개체의 일부만 로드하기로 결정했기 때문에 저장 코드가 변경되는 필드만 업데이트하지 않고, 전체 개체를 덮어씌우는 것이다. 이것은 필자가 그 당시에 더 생각해 봤어야했던 것이었다. 이 경우 컴포넌트에 있는 모든 객체 데이터를 로드하는 것으로 전환하고 사용자에게는 그들이 업데이트해야 할 필드만 보여줘야 한다.

// src/app/accounts/components/CreateUpdateAccount.vue

...

if (selectedAccount) {
  this.editing = true;
  this.selectedAccount = Object.assign({}, selectedAccount); // we copy to a new object so we aren't editing the real object in the Vuex store
}
...

GIT : 324fbe9

GIT : 23182d1

Tags : vue javascript 

comments powered by Disqus