본문 바로가기
IT지식/Web, Server

[Web] 실습 UI 개발로 배워보는 순수 javascript와 VueJS개발

by five-sun 2023. 2. 10.
728x90

- Vue.js란?

Vue.js는 웹프론트엔드 프레임워크 중 하나이다. 

컴포넌트 기반의 SPA(Single Page Applcation)를 구축할 수 있게 해주는 프레임워크이다.

컴포넌트란 웹을 구성하는 로고, 메뉴바, 버튼 등 웹 페이지 내의 다양한 UI요소를 재사용 가능하도록 구조화 한 것이다.

SPA는 단일 페이지 어플리케이션으로 하나의 페이지 안에서 필요한 영역 부분만 로딩 되는 형태를 말한다.

빠른 페이지 변환과 적은 트래핑 양을 지원한다.

 

 

Vue.js를 공부하기 위해서 인프런의 "실습 UI 개발로 배워보는 순수 javascript와 VueJS개발" 강의를 따라하며 학습해보겠다.

 

- 개발환경

  • node.js
  • vscode
  • chrome(최신 버전)
  • lite-server(개발 서버)
  • git(코드 형상 관리)

- 요구사항 분석

검색폼 구현

  • [ ] 검색 상품명 입력 폼이 위치한다. 검색어가 없는 경우이므로 x 버튼을 숨긴다
  • [ ] 검색어를 입력하면 x버튼이 보인다
  • [ ] 엔터를 입력하면 검색 결과가 보인다
  • [ ] x 버튼을 클릭하거나, 검색어를 삭제하면 검색 결과를 삭제한다

 

검색 결과 구현

  • [ ] 검색 결과가 검색폼 아래 위치한다
  • [ ] 검색 결과가 보인다
  • [ ] x버튼을 클릭하면 검색폼이 초기화 되고, 검색 결과가 사라진다

탭 구현

  • [ ] 추천 검색어, 최근 검색어 탭이 검색폼 아래 위치한다
  • [ ] 기본으로 추천 검색어 탭을 선택한다
  • [ ] 각 탭을 클릭하면 탭 아래 내용이 변경된다

 

추천 검색어 구현

  • [ ] 번호, 추천 검색어 목록이 탭 아래 위치한다
  • [ ] 목록에서 검색어를 클릭하면 선택된 검색어로 검색 결과 화면으로 이동
  • [ ] 검색폼에 선택된 추천 검색어 설정

 

최근 검색어 구현

  • [ ] 최근 검색어, 목록이 탭 아래 위치한다
  • [ ] 목록에서 검색어를 클릭하면 선택된 검색어로 검색 결과 화면으로 이동
  • [ ] 검색일자, 버튼 목록이 있다
  • [ ] 목록에서 x 버튼을 클릭하면 선택된 검색어가 목록에서 삭제
  • [ ] 검색시마다 최근 검색어 목록에 추가된다

- MVC 패턴이란?

Model - View - Controller 의 약자이다.

해당 내용을 이전에 정리한 게시물 : https://five-sun.tistory.com/87

 

[Java] Servlet과 JSP 에 대해 '공부' 해보자 + MVC Architecture

Servlet과 JSP를 알아보기 이전에 Web Service의 기본적인 동작 과정을 알아보자. - Web Service의 기본적이 동작 과정 HTML Form -> Servlet -> HTML Page 사용자가 웹페이지 form(HTML Form)을 통해 자신의 정보를 입력

five-sun.tistory.com

 

- 폴더 구조?

강사님께서 어느 정도 짜여진 폴더를 제공해주신다.

강사님의 GitHub을 통해서 clone한 후, master 브랜치는 이미 완성된 프로젝트이기 때문에 git checkout 명령어를 통해서 초기 프로젝트 파일부터 학습을 시작할 수 있었다.

# 기본 제공

1-vanilla

--js

----models

------HistoryModel.js

------KeywordModel.js

------SearchModel.js

----views

------View.js

----app.js

--index.html

--style.css

 

 

- 검색폼 구현

# FormView.js

import View from './View.js'

const tag = '[FormView]'

const FormView = Object.create(View)

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
}

FormView.showResetBtn = function (show = true) {
  this.resetEl.style.display = show ? 'block' : 'none'
}

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

FormView.onKeyup = function (e) {
  const enter = 13
  this.showResetBtn(this.inputEl.value.length)
  if (e.keyCode !== enter) return
  this.emit('@submit', { input: this.inputEl.value })
}

FormView.onClickReset = function() {
    this.emit('@reset')
    this.showResetBtn(false)
}
export default FormView

 

- 검색 결과 구현

#ResultView.js

import View from './View.js'

const tag = '[ResultView]'

const ResultView = Object.create(View)

ResultView.messages = {
  NO_RESULT: '검색 결과가 없습니다'
}

ResultView.setup = function (el) {
  this.init(el)
}

ResultView.render = function (data = []) {
  console.log(tag, 'render()', data)
  this.el.innerHTML = data.length ? this.getSearchResultsHtml(data) : this.messages.NO_RESULT
  this.show()
}

ResultView.getSearchResultsHtml = function (data) {
  return data.reduce((html, item) => {
    html += this.getSearchItemHtml(item)
    return html
  }, '<ul>') + '</ul>'
}

ResultView.getSearchItemHtml = function (item) {
  return `<li>
    <img src="${item.image}" />
    <p>${item.name}</p>
  </li>`
}

export default ResultView

# SearchModel.js

const data = [
  {
    id: 1,
    name: "비건 샐러드",
    image:
      "https://images.unsplash.com/photo-1512621776951-a57141f2eefd?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80",
  },
  {
    id: 2,
    name: "수제 햄버거",
    image:
      "https://images.unsplash.com/photo-1550317138-10000687a72b?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80",
  },
];

export default {
  list(query) {
    return new Promise(res => {
      setTimeout(()=> {
        res(data)
      }, 200);
    })
  }
}

 

- 탭 구현

# TabView.js

import View from './View.js'

const tag = '[TabView]'

const TabView = Object.create(View)

TabView.tabNames = {
  recommand: '추천 검색어',
  recent: '최근 검색어',
}

TabView.setup = function (el) {
  this.init(el)
  this.bindClick()
  return this
}

TabView.setActiveTab = function (tabName) {
  Array.from(this.el.children).forEach(li => {
    li.className = li.innerHTML === tabName ? 'active' : ''
  })
  this.show()
}

TabView.bindClick = function () {
  Array.from(this.el.children).forEach(li => {
    li.addEventListener('click', e => this.onClick(li.innerHTML))
  })
}

TabView.onClick = function (tabName) {
  this.setActiveTab(tabName)
  this.emit('@change', { tabName })
}

export default TabView

 

- 추천 검색어 구현

# KeywordView.js

import View from './View.js'

const tag = '[KeywordView]'

const KeywordView = Object.create(View)

KeywordView.messages = {
  NO_KEYWORDS: '추천 검색어가 없습니다'
}

KeywordView.setup = function (el) {
  this.init(el)
  return this
}

KeywordView.render = function (data = []) {
  this.el.innerHTML = data.length ? this.getKeywordsHtml(data) : this.messages.NO_KEYWORDS
  this.bindClickEvent()
  this.show()
}

KeywordView.getKeywordsHtml = function (data) {
  return data.reduce((html, item, index) => {
    html += `<li data-keyword="${item.keyword}"><span class="number">${index + 1}</span>${item.keyword}</li>`
    return html
  }, '<ul class="list">') + "</ul>"
}

KeywordView.bindClickEvent = function () {
  Array.from(this.el.querySelectorAll('li')).forEach(li => {
    li.addEventListener('click', e => this.onClickKeyword(e))
  })
}

KeywordView.onClickKeyword = function (e) {
  const { keyword } = e.currentTarget.dataset
  this.emit('@click', { keyword })
}

export default KeywordView

# KeywordModel.js

export default {
  data: [
    {keyword: '이탈리아'}, 
    {keyword: '세프의요리'}, 
    {keyword: '제철'}, 
    {keyword: '홈파티'}
  ],

  list() {
    return new Promise(res => {
      setTimeout(() => {
        res(this.data)
      }, 200)
    })
  }
}

- 최근 검색어 구현

# HistoryView.js

import KeywordView from './KeywordView.js'

const tag = '[HistoryView]'

const HistoryView = Object.create(KeywordView)

HistoryView.messages.NO_KEYWORDS = '검색 이력이 없습니다'

HistoryView.getKeywordsHtml = function (data) {
  return data.reduce((html, item) => {
    html += `<li data-keyword="${item.keyword}">
      ${item.keyword} 
      <span class="date">${item.date}</span>
      <button class="btn-remove"></button>
      </li>`
    return html
  }, '<ul class="list">') + "</ul>"
}

HistoryView.bindRemoveBtn = function () {
  Array.from(this.el.querySelectorAll('button.btn-remove')).forEach(btn => {
    btn.addEventListener('click', e => {
      e.stopPropagation()
      this.onRemove(btn.parentElement.dataset.keyword)
    })
  })
}

HistoryView.onRemove = function (keyword) {
  this.emit('@remove', { keyword })
}

export default HistoryView

# 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)
  }
}

 

Vanilla Javascript를 사용해 Vue.js개발을 배워보기 위한 토대를 만들어보았다.

 

다음을 넘어가기 이전에 MVVM에 대해서 살펴보았다.

 

-MVVM이란?

구조상으로는 Model <-> View-Mdel <->View 로 이루고 있다.

View-Model은 Model보다 View에 적합한 형태를 이루고 있다.

하나의 View에 하나의 View-Model이 일대일로 매칭을 이루고 있다.

 

 

728x90