[JS] JavaScript MVC 구현해보기

본 내용은 인프런 강의 실습 UI 개발로 배워보는 순수 javascript와 VueJS 개발 후기다.
순수 자바스크립트를 이용해서 목록 출력, 목록 검색, 최근 검색을 작게 구현 해보았다.

개발환경

VS code, node, lite-server(npm 설치 모듈)
링크를 통해서 다운로드를 해도 되고 homebrew, npm install을 통해 설치해도 된다.

폴더구조와 기능

크게 controllers, model, views 폴더로 나뉘어 있다.

app.js

Dom이 로드되는 시점에 MainController.js의 init 함수 호출을 담당한다.
DOMContentLoaded 더 알아보기

import MainController from './controllers/MainController.js'

//DOMContentLoaded
//브라우저가 HTML을 전부 읽고 DOM 트리를 완성하는 즉시 한다.
//이미지 파일이나 스타일시트 등의 기타 자원은 기다리지 않는다.
document.addEventListener('DOMContentLoaded', () => {
  MainController.init()
})

MainController.js

View, Model 영역의 파일들을 모두 import하여 사용한다.
각각 파일은 default export 되어있기 때문에 불러오는 클래스에 중괄호를 사용하지 않고 불러올 수 있다.

default export 의 특징
가져오기 할 때 개발자가 원하는 대로 이름을 지정할 수 있다. 하지만 실무에선 같은 걸 가져오는데도 이름이 달라 혼란의 여지가 생길 수 있기 때문에 파일 이름과 동일한 이름을 사용하도록 팀원끼리 내부 규칙을 정할 수 있다.

import FormView from '../views/FormView.js'
import ResultView from '../views/ResultView.js'
import TabView from '../views/TabView.js'
import KeywordView from '../views/KeywordView.js'
import HistoryView from '../views/HistoryView.js'

import SearchModel from '../models/SearchModel.js'
import KeywordModel from '../models/KeywordModel.js'
import HistoryModel from '../models/HistoryModel.js'

init 메서드를 통해 import된 모듈 중에서 상태가 변한 View를 감지한다.
모든 모듈(View.js)들은 setup이라는 메서드를 가지고 있고 특정 element를 통해 이벤트를 감지 받은 후 각각의 모듈에서 화면 변화에 맞는 메서드를 호출한다.
이후 컨트롤러에 태그(@submit, @reset, @change 등등)를 반환해 관련한 메서드를 호출 시킨다.

//MainController init 함수
  init() {
    FormView.setup(document.querySelector('form'))
    	//on은 메서드 체이닝
      .on('@submit', e => this.onSubmit(e.detail.input))
      .on('@reset', e => this.onResetForm())
    
    TabView.setup(document.querySelector('#tabs'))
      .on('@change', e => this.onChangeTab(e.detail.tabName))
    
    KeywordView.setup(document.querySelector('#search-keyword'))
      .on('@click', e => this.onClickKeyword(e.detail.keyword))
    
    HistoryView.setup(document.querySelector('#search-history'))
      .on('@click', e => this.onClickHistory(e.detail.keyword))
      .on('@remove', e => this.onRemoveHistory(e.detail.keyword))
    ResultView.setup(document.querySelector('#search-result'))
    this.selectedTab = '추천 검색어'
    this.renderView()
  },
  //검색할 단어를 매개변수로 받아 search 
  search(query) {
    FormView.setValue(query)
    SearchModel.list(query).then(data => {
      this.onSearchResult(data)
    })
  },
  //... 메서드 생략
    

View

View.js
공통으로 사용할 메서드가 정의되어 있다.
각각의 View.js에서 이 모듈을 import하여 새 객체를 만들어 사용한다.

const tag = '[View]'

export default {
  init(el) {
    if (!el) throw el
    this.el = el
    return this
  },

  on(event, handler) {
    this.el.addEventListener(event, handler)
    return this
  },

  emit(event, data) {
    const evt = new CustomEvent(event, { detail: data })
    this.el.dispatchEvent(evt)
    return this
  },

  hide() {
    this.el.style.display = 'none'
    return this
  },

  show() {
    this.el.style.display = ''
    return this
  }
}

다른 모듈들은 단순히 import를 하여 사용하였으나 View.js을 사용할 때는 import 후 Object.create를 하여 객체를 만들어 사용하였다.
그 이유는 객체의 상속을 통해 부모 객체의 기능을 물려받아 새로운 기능을 추가하여 사용하기 위함이라고 한다.
자바스크립트에서 상속과 프로퍼티, 인스턴스의 개념을 더 이해해야 할 필요가 보인다.

Object.create(객체)
상속을 통해 부모 객체의 기능을 물려받고, 본인만의 새로운 기능을 추가할 수 있다. 특이한 점은 Object.create을 통해 생성할 경우 생성자 코드를 실행하지 않는다. 생성자를 호출할 경우는 Object.create 보다 New를 통하여 객체를 호출 해주는 것이 낫다.
Object.create의 매개변수

Object.create(prototypeObect,propertyOberct)
  • 첫번째 매개변수 : 프로토타입
  • 두번째 매개변수 : 생성할 객체의 프로퍼티 키와 디스크립터 객체 전달

예시) FormView.js

아래 소스를 보면 Object.create를 사용해 View의 기능을 물려받아 FormView라는 객체를 생성해주었다.

import View from './View.js'

const tag = '[FormView]'

//Object.create 사용
const FormView = Object.create(View)

setup 함수에서 기본 상태와 상태 변화를 감지했을 경우의 필요한 함수를 정의한다.

FormView.setup = function (el) {
  this.init(el)
  this.inputEl = el.querySelector('[type=text]')
  this.resetEl = el.querySelector('[type=reset')
  this.showResetBtn(false)
  this.bindEvents()
  return this
}

이후 UI에 관련된 함수와 데이터의 상태 변화 함수를 정의한다.
데이터의 상태 변화 함수는 emit 메서드로 태그를 전달하여 컨트롤러에게 상태 변화를 알린다.

FormView.bindEvents = function() {
  this.on('submit', e => e.preventDefault())
  this.resetEl.addEventListener('click', e => this.onClickReset())
}

FormView.onClickReset = function() {
  //컨트롤러에서 @reset일 때 함수 호출
  this.emit('@reset')
  this.showResetBtn(false)
}

Models

각 model에서는 필요한 데이터와 처리를 담당하였다.
컨트롤러에서 모듈로 import 한 후 뷰에서 데이터 처리시 태그를 보내고 컨트롤러에서 태그를 받아 관련 모델 메서드를 호출한다.
예시) HistoryModel.js

export default {
  data: [
    { keyword: '검색기록2', date: '12.03' },
    { keyword: '검색기록1', date: '12.02'},
    { keyword: '검색기록0', date: '12.01' },
  ],

  list() {
    return Promise.resolve(this.data)
  },
  
  add(keyword = '') {
    keyword = keyword.trim()
    if (!keyword) return 
    if (this.data.some(item => item.keyword === keyword)) {
      this.remove(keyword)
    }

    const date = '12.31'
    this.data = [{keyword, date}, ...this.data]
  },
  
  remove(keyword) {
    this.data = this.data.filter(item => item.keyword !== keyword)
  }
}

큰 흐름

  1. app.js에서 MainController.js init 메서드 호출한다.
  2. MainController.js에는 view, model 모듈들이 import 되어있고 관련 요소의 상태 변화를 감지하여 그 모듈로 상태 변화를 전달한다.
  3. 각 모듈(Views 아래의 view.js 파일들)들은 setup 함수를 통해 기본적으로 실행하는 함수들이 있고 크게 UI 함수와 데이터 처리 함수가 있다.
  4. 데이터 처리 함수의 경우 MainController.js에게 태그를 전달한다.
  5. MainController.js에서 전달된 태그에 맞는 함수를 실행하고 Model에서 데이터와 관련된 처리 후 MainController.js에게 값을 전달하고 View에 반영한다.

구현 화면

추천 검색어, 최근 검색어, 검색, 기록 삭제, 검색 결과 리스트

느낀점

데이터 연결 없이 객체 통해서 바로 뿌려볼 수 있어서 편했고, 각각 파일들의 역할이 명확해서 모듈로 만들어 사용할 때의 이점을 알 수 있었다.
자바에서 import 해서 사용하는 것과 비슷해 보이는데 자바스크립트에서는 import, export 방식 구현이 더 다양해 보인다.
자바에서도 클래스 자체를 import하는 것에 별로 생각을 안해봤는데 다른 방식이 있는지 찾아봐야겠다.
또 ES6, 프로퍼티, 속성에 대해서 공부를 좀 더 해야겠다.

깃 구현 주소 바로가기

좋은 웹페이지 즐겨찾기