해리의 데브로그

실습 UI 개발을 통해 배워보는 JS & Vue JS (6) - 최근 검색어 구현 (JS)

|

본 강의는 Inflearn의 김정환 개발자 님의 강의(실습 UI 개발로 배워보는 순수 javascript 와 VueJS 개발)를 듣고 배운 내용을 정리한 포스팅 입니다.

최근 검색어 구현 (1), (2)

최근 검색어, 목록이 탭 아래 위치한다 [ ] 목록에서 검색어를 클릭하면 선택된 검색어로 검색 결과 화면으로 이동

1) index.html

  • div 태그 추가 (id="search-history")
<div id="search-keyword"></div>
<div id="search-history"></div>
<div id="search-result"></div>

2) HistroyView.js & MainController.js

  • KeywordView를 복사하여 구현
  • HistoryView와 MainController를 연결
    • KeywordView를 연결했던 것과 동일한 방식으로 연결
    • onClickHistory 함수 구현. search함수에 keyword를 넘김
    • selectedTab의 default값을 “최근 검색어” 로 변경
// HistoryView
import KeywordView from './KeywordView.js'

const tag = '[HistoryView]'

const HistoryView = Object.create(KeywordView)

export default HistoryView

// MainController
import HistoryView from '../views/HistoryView.js'


export default {
    init() {
        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))
        
        this.selctedTab = '최근 검색어'
    },
    
    
    onClickKeyword(keyword) {
        this.search(keyword)
    },

    onClickHistory(keyword) {
        this.search(keyword)
    },
    

3) MainController.js (2)

  • 위의 코드대로 서버를 실행시키면 아래 함수에 따라 debugger로 입력된 breakpoint에서 멈추게된다.
  • 분기문 if 조건에서 작성한 코드처럼, 유사한 코드를 ‘최근 검색어’ 에 대해 작성할 수 있음. (this.fetchSearchHistory())
  • HistoryModel 내 list 함수를 호출함(promise 객체를 반환함). 결과를 data에 저장하여 HistoryView.render 함수로 넘김. HistoryView에는 render 함수가 따로 존재하지 않지만 KeywordView를 복사하였으므로, 사실 KeywordView의 render 함수라고 봐도 무방하다.
    renderView() {
        console.log(tag, 'renderView()')
        TabView.setActiveTab(this.selctedTab)

        if (this.selctedTab === '추천 검색어') {
            this.fetchSearchKeyword()
        } else {
            // debugger
            this.fetchSearchHistory()
        }

        ResultView.hide()
    },


    fetchSearchHistory() {
        HistoryModel.list().then(data => {
            HistoryView.render(data)
        })
    },

최근 검색어 구현 (3)

[ ] 검색일자, x 버튼을 구현

1) 추가 코드

몇 파일에서 추가된 코드를 발견함

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

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

2) HistoryView.js

KeywordView.js의 render 함수를 살펴보면 getKeywordHtml 함수를 통해 리스트를 보여주는데 이부분은 HistoryView.js와는 포맷이 조금 다름. => getKeywordHtml 함수를 HistoryView.js에서 오버라이딩하여 재 구현

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>'
}
 

최근 검색어 구현 (4)

[ ] 목록에서 x 버튼을 클릭하면 선택된 검색어가 목록에서 삭제

1) 추가 코드

MainController.js 에서 추가된 코드를 발견함

    renderView() {
        console.log(tag, 'renderView()')
        TabView.setActiveTab(this.selctedTab)

        if (this.selctedTab === '추천 검색어') {
            this.fetchSearchKeyword()
            HistoryView.hide() // 추가된 코드
        } else {
            this.fetchSearchHistory()
            KeywordView.hide()  // 추가된 코드
        }

        ResultView.hide()
    },

2) HistoryView.js

  • x 버튼을 클릭이벤트와 바인딩 함 => bindRemoveBtn 함수 정의
  • 해당 함수는 버튼을 모두 찾아서 클릭 이벤트를 달아주는 기능을 함.
  • 앞서 작성했던 코드와 동일한 방식으로 코드 작성
    • e.stopPropagation() 을 통해 클릭의 이벤트 전파를 막음
    • x 버튼이 클릭되었다는 것을 MainController에게 알려주기 위한 함수 onRemove 정의
    • 어떤 값을 지울건지. 지울려는 키워드를 보냄(키워드는 btn 위에 있는 li 엘레멘트의 data 어트리뷰트에 있음)
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})
}

3) MainController.js & KeywordView.js

bindRemoveBtn을 어느시점에서 호출하느냐가 또다른 관건임.

  • HistoryView를 render하는 부분을 찾음 => fetchSearchHistory 에서 render 함. 이부분에 코드 추가
  • render 함수가 호출되고 나면 데이터를 기반으로 DOM이 생성됨. 그 이후 이벤트를 바인딩 할 수 있기 때문에 체이닝을 이용하여 bindRemoveBtn() 을 만듦.
  • 체이닝을 할려면 render 함수가 this를 return 하고 있어야함. render은 HistoryView에 있는 것이 아니라 이 HistoryView가 복사해온 KeywordView에 있음. KeywordView의 render 함수에 this를 return 하도록 하자.
// MainController.js
fetchSearchHistory() {
    HistoryModel.list().then(data => {
        HistoryView.render(data).bindRemoveBtn()
    })
},
    

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

4) MainController.js

  • 추가로 remove 함수를 발산하도록 컨트롤러 초기화 부분에 코드 삽입
  • onRemoveHistory 함수에 keyword를 넘김 & 함수 구현
  • 실제로 데이터를 삭제하는 것은 controller가 아니라 model이 데이터를 관리함.
  • HistoryModel 내 미리 구현해 놓은 remove 함수에 삭제할 keyword를 넘겨줌.
  • 이후 다시 renderView를 호출해줌
export default {
    init() {
        HistoryView.setup(document.querySelector('#search-history'))
            .on('@click', e => this.onClickHistory(e.detail.keyword))
            .on('@remove', e => this.onRemoveHistory(e.detail.keyword))

최근 검색어 구현 checkout

현재까지 작성한 코드를 기반으로 서버를 실행시키면, TabView의 기본 값은 최근 검색어로 되어있음. 이때 추천 검색어 탭을 클릭하면 설정한 breakpoint에 따라 정지되는데 그 로직은 다음과 같음.

  • TabView.setup 함수에 따라 onChangeTab 함수가 실행됨.
  • onChangeTab 함수에는 debugger가 설정되어있음.

이 코드를 다시한번 다듬어 보도록 하자.

export default {
    // 컨트롤러 초기화 부분
    init() {
        TabView.setup(document.querySelector('#tabs'))
            .on('@change', e => this.onChangeTab(e.detail.tabName))
    },
    
    onChangeTab(tabName) {
        debugger
    },
  • onChangeTab 함수에는 tabName 이 인자로 들어옴 => this.selectedTabtabName을 설정해줌
  • 이후 다시 renderView를 호출함
  • 마지막으로 탭구현의 2번 명세 (기본적으로 추천 검색어 탭을 선택한다)에 따라, this.selectedTab의 기본 값을 this.selectedTab = '추천 검색어' 으로 다시 변경하도록 하자.

최근 검색어 구현 (5)

[ ] 검색시마다 최근 검색어 목록에 추가된다

MainController.js

  • 검색을 하게 되면 MainController의 특정 함수로 수렴하게 되어 있음. 컨트롤러 초기화 부분에 따라, 어떠한 view에서 이벤트가 발생하는 경로를 잘 살펴보면 search 함수로 도달하게 되는 것을 알 수 있음. search 함수에서 검색한 이력을 추가하면 됨.
    search(query) {
        FormView.setValue(query)
        HistoryModel.add(query)
        SearchModel.list(query).then(data => {
            this.onSearchResult(data)
        })
    },

실습 UI 개발을 통해 배워보는 JS & Vue JS (5) - 추천 검색어 구현 (JS)

|

본 강의는 Inflearn의 김정환 개발자 님의 강의(실습 UI 개발로 배워보는 순수 javascript 와 VueJS 개발)를 듣고 배운 내용을 정리한 포스팅 입니다.

추천 검색어 구현 (1)

번호, 추천 검색어 목록이 탭 아래 위치한다.

1) index.html

  • 추천 검색어 목록을 보여줄 코드 구현
  • div 엘리먼트 내 search-keyword id 값 적용
  • 실제 데이터는 서버에서 갖고오고, 그 데이터를 기반으로 DOM을 만들어내기 때문에 1개의 div만 만듦.
  • search-keyword 를 이용하여 KeywordView.js 생성
<div class="container">
    <ul id="tabs" class="tabs">
        <li>추천 검색어</li>
        <li>최근 검색어</li>
    </ul>

    <div id="search-keyword"></div>
    <div id="search-result"></div>
</div>

2) KeywordView.js

  • 다른 View와 동일하게 View를 가져와 복사해서 사용 & 디버깅을 위한 태그 생성
  • setup 함수 & 데이터를 받아서 뿌려줄 render 함수 생성
  • 엘레멘트의 innerHTML에 데이터를 넣음. 2가지의 경우 고려
    • 데이터가 있는 경우(data.length): getKeywordHtml 함수를 통해서 html 문자열을 받아옴
    • 그렇지 않은 경우, “추천 검색어가 업습니다” 문자열 출력
    • this.show() 메서드를 이용하여 실제로 출력
  • getKeywordHtml 함수 정의. 우선은 간단히 debugger 만 입력
  • MainController.js에서 사용 할 수 있도록 모듈을 export 해 줌
import View from './View.js'

const tag = '[KeywordView]'

const KeywordView = Object.create(View)

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

KeywordView.render = function (data = []) {
    this.el.innerHTML = data.length ? this.getKeywordsHtml(data) : '추천 검색어가 없습니다'
    this.show()
}

KeywordView.getKeywordsHtml = function (data) {
    debugger
}

export default KeywordView 

3) MainController.js

  • KeywordView.js 를 import 해오고 controller를 초기화하는 코드에 삽입
  • search-keyword 엘레멘트 (id값) 로 setup 설정
import KeywordView from '../views/KeywordView.js'

export default {
    // 컨트롤러 초기화 부분
    init() {
		// 중략
        KeywordView.setup(document.querySelector('#search-keyword'))
  • search-keyword에 render하는 코드를 작성. MainController.js 내 renderView가 그 역할을 담당하고있음.
  • controller가 가지고 있는 데이터 중 selectedTab이라는 게 있음. (default값: “추천 검색어”)
  • selectedTab 값이 어떤 것이냐에 따라서 “추천 검색어” 또는 “최근 검색어”를 출력하는 로직을 짤 수 있음.
    • “추천 검색어” 인 경우 KeywordView.js에 render 함수를 호출함. (현재까지의 코드로는 “추천 검색어가 없습니다” 가 출력됨)

	renderView() {
        console.log(tag, 'renderView()')
        TabView.setActiveTab(this.selctedTab)
        ResultView.hide()

        if (this.selctedTab=== '추천 검색어') {
            KeywordView.render()
        } else {

        }
    },

4) MainController.js (2)

  • models/KeywordModel.js 를 통해 추천 검색어를 가져 옴. 기 파일 내list 함수를 이용해 데이터를 수신
  • controller에서 KeywordModel을 가져온 후 renderView 함수 내에서 KeywordModel의 list를 호출 함.
  • 호출한 값은 promise 객체를 반환하므로 .then 함수를 통해 데이터를 얻어 올 수 있음.
  • 받아온 데이터를 render 함수에 그대로 넘김
  • 이후 서버를 실행시키면, KeywordView.getKeywordsHtml 함수가 실행되어 debugger로 지정한 곳에 멈춤. 이는 keywordView.render 함수를 통해 data.length가 있기 때문에 getKeywordsHtml이 호출된 것.
  • 나머지 우리가 해야할 일은 getKeywordHtml을 구현하는 것
import KeywordModel from '../models/KeywordModel.js'

renderView() {
    console.log(tag, 'renderView()')
    TabView.setActiveTab(this.selctedTab)
    ResultView.hide()

    if (this.selctedTab=== '추천 검색어') {
        KeywordModel.list().then(data => {
            KeywordView.render(data)
        })
    } else {

    }
},

5) KeywordView.js

  • 받은 데이터를 reduce함수를 통해 HTML을 생성
  • 초기 값으로 <ul> 입력. style.css를 통해 이미 정의된 css를 적용하기 위해 list 클래스 추가
  • reduce 함수를 사용하는 매커니즘은 이전 포스팅과 동일
  • 추가로, number를 넣기 위해 reduce의 3번째 인자로 index를 삽입
KeywordView.getKeywordsHtml = function (data) {
    return data.reduce((html, item, index) => {
        html += `<li>
        <span class="number">${index + 1} </span>
        ${item.keyword}
        </li>` 
        return html        
    }, '<ul class="list">') + '</ul>'

6) MainController.js

  • MainController.js 내 renderView() 함수에서, 추천검색어를 선택한 경우 모델에서 데이터를 갖고와 KeyworView.render함수에 뿌려주는 코드는 view를 rendering하는 역할이기 때문에 모델에서 데이터를 갖고오는 부분은 별도로 따로 떼 낼 수 도 있음.
  • 코드를 떼네어, fetchSearchKeyword 라는 함수에게 위임
    renderView() {
        console.log(tag, 'renderView()')
        TabView.setActiveTab(this.selctedTab)

        if (this.selctedTab=== '추천 검색어') {
            this.fetchSearchKeyword()
        } else {

        }

        ResultView.hide()

    },

    fetchSearchKeyword() {
        KeywordModel.list().then(data => {
            KeywordView.render(data)
        })
    },

추천 검색어 구현 (2)

목록에서 검색어를 클릭하면 선택된 검색어로 검색 결과 화면으로 이동

추천 검색어에 있는 리스트를 클릭하면 클릭 이벤트를 수신하도록 하자

1) KeywordView.js (1)

  • bind이벤트(bindClickEvent)를 setup에서 호출 & 함수 구현
  • li 엘레멘트를 찾은 후, 클릭 이벤트를 하나씩 바인딩 하는 코드 작성
  • 유사 배열이므로 Array.from을 통해 array로 만든 후, forEach를 통해 array의 요소에 하나씩 접근
  • li 엘레멘트에 클릭 이벤트 바인딩한 후 onClickKeyword 함수로 이벤트를 전달 해줌.
KeywordView.bindClickEvent = function() {
    Array.from(this.el.querySelectorAll('li')).forEach(li => {
        li.addEventListener('click', e => this.onClickKeyword(e))
    })
}

2) Keyword.View.js (2)

  • onClickKeyword 함수 구현
  • 데이터를 클릭했을 때 이벤트는 발생. 이 때 어떤 추천 검색어가 클릭되었는지를 알아야함. 그렇게 하기 위해서는 이벤트(e) 에 데이터를 심어서 보내줘야함.
  • getKeywordHtml 함수 내 reduce 메서드를 통해 html에 쌓아올리는 코드에서 data 변수를 추가
  • data-keyword에 실제 출력한 키워드(item.keyword)를 바인딩 함. 이 데이터를 갖고와 활용
  • keyword를 추출 (keyworde.currentTarget.dataset에 저장되어있음)
  • 클릭했을 때 검색 결과 페이지로 넘어가는 역할은 KeywordView의 역할은 아님. 이는 mainController가 다룰 예정이므로, MainController에게 어떤 값이 클릭되었다는 것을 통지만 함(위임) => emit 함수 사용
  • 해당 이벤트를 controller가 수신할 수 있도록 setup 함수에서 this를 return 시킴
KeywordView.setup = function(el) {
    this.init(el)
    this.bindClickEvent()

    return this
}

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.onClickKeyword = function(e) {
    const {keyword} = e.currentTarget.dataset
    this.emit('@click', {keyword}) 
}

3) MainController.js

  • 해당 이벤트를 MainController에서 수신 할 수 있도록 KeywordView.setup 함수를 체이닝으로 수신
  • onClickKeyword 함수를 호출하며, 이벤트의 키워드 값을 전달함
  • keywordView에서 전달한 값이 제대로 들어오는지 확인하기 위해 breakpoint 설정
export default {
    init() {
        // 중략
        KeywordView.setup(document.querySelector('#search-keyword'))
            .on('@click', e => this.onClickKeyword(e.detail.keyword))
    },
	
    // 중략

    onClickKeyword(keyword) {
        debugger
    }
}

4) keywordView.js

  • setup에서 bind이벤트를 발생 시키기 않고, DOM이 만들어 진 후에 이벤트를 발생시켜야함.

  • render 함수에서 innerHTML을 추가한 뒤에 bind 이벤트를 발생시키도록 수정

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

5) MainController.js

  • OnClickKeyword 함수 구현. 이 함수는 키워드가 클릭을 했을 때 실행 됨.
  • 실제 검색을 해야함. 검색하는 기능은 미리 구현해놓은 search 함수 활용
onClickKeyword(keyword) {
    this.search(keyword)
},

6) MainController.js

  • 여기까지 코드로 서버를 실행시키면 탭과 검색결과가 동시에 뜸
  • onClickKeyword 함수가 실행되면서 들어오는 keyword를 search 함수에 넘겨줌
  • search 함수는 받은 값을 가지고 searchModel에서 조회를 한 후, 거기서 받은 데이터를 onSearchResult로 넘김.
  • onSearchResult에서는 resultView를 그리는 역할을 함. 여기서 그리기 전에, 기존에 있던 TabView와 KeywordView를 숨겨줄 필요가 있음. => TabView.hide() & ` KeywordView.hide()` 삽입!
    search(query) {
        console.log(tag, 'search()', query)
        // search API
        SearchModel.list(query).then(data => {
            this.onSearchResult(data)
        })
    },
    

    onSearchResult(data) {
        TabView.hide()
        KeywordView.hide()
        ResultView.render(data)
    },


    onClickKeyword(keyword) {
        this.search(keyword)
    },
}

추천 검색어 구현 (3)

검색폼에 선택된 추천 검색어 설정

1) MainController.js

  • onClickKeyword 함수는 받은 키워드를 search 함수로 넘겨줌.
  • search 함수에 FormView를 셋팅하는 로직을 추가 => setValue 함수 호출
    search(query) {
        FormView.setValue(query)
        SearchModel.list(query).then(data => {
            this.onSearchResult(data)
        })
    },
        
    onClickKeyword(keyword) {
        this.search(keyword)
    },

2) FormView.js (1)

  • FormView.js 내 setValue 함수 구현
  • inputEl의` value값에 받아온 value를 저장시킴
FormView.setValue = function(value = '') {
    this.inputEl.value = value
}

3) FormView.js (2)

  • 추가로 검색폼에 선택된 추천 검색어를 뜨게했을 때, 삭제가능한 x버튼도 뜨도록 코드를 작성할 수 있음.
  • 이부분은 FormView.js 내 showResetBtn 함수를 통해 구현되어있음.
  • showResetBtn 값을 true로 주거나, inputEl.value.length을 boolean 값으로 해석하도록 넘겨 줘도 됨.

FormView.setValue = function(value = '') {
    this.inputEl.value = value
    // this.showResetBtn(true)
    this.showResetBtn(this.inputEl.value.length)

4) MainController.js

  • 검색폼에 검색어를 삭제했을 경우, TabView가 사라짐. 이 View를 복구하는 코드를 작성할 수 있음.
  • 검색 폼에서 x버튼을 누르면 MainController.js 를 통해 @reset 이벤트가 발생함. 그러면 controller에서는 onResetForm() 함수가 실행됨 onResetForm 은 단순히 ResultView를 감쳐주기만 하고 있음.
  • 이부분을 수정하여, controller가 view를 그리는 renderView()를 호출하도록 함.
export default {
    init() {

        FormView.setup(document.querySelector('form'))
            .on('@submit', e => this.onSubmit(e.detail.input))
            .on('@reset', e=> this.onResetForm())
    },
    
    onResetForm(input) {
        console.log(tag, 'onResetForm()')
        // ResultView.hide()
        this.renderView()
    },   

※ Error 발생 case

이번 챕터를 따라가다가 두 차례 에러가 발생한 이력이 있어 메모를 남김. 두 차례 모두 this.show를 입력하지 않아 발생한 에러였음. 해당 강의는 git을 통해 각 강의별로 branch를 불러와 진행하는데, 자세히 코드를 살펴보면 이전 강의에서 언급하지 않았던 코드가 일부 수정되거나 추가된 경우를 종종 보았다.

  • 검색어를 입력하고 엔터를 쳤을 때 결과가 나오지 않은 경우
    • ResultView.js 내 render 함수에서 this.show()삽입
ResultView.render = function(data = []) {
    console.log(tag, 'render()', data)
    this.el.innerHTML = data.length ? this.getSearchResultsHtml(data) : '검색 결과가 없습니다'
    this.show()
}
  • 검색어를 삭제한 후, TabView가 뜨지 않고 추천검색어 목록만 뜨는 경우
    • TabVivew.js 내 setActiveTab 함수에서 this.show() 삽입
TabView.setActiveTab = function(tabName){
    Array.from(this.el.querySelectorAll('li')).forEach(li => {
        li.className = li.innerHTML == tabName ? 'active' : ''
    })
    this.show()
}

실습 UI 개발을 통해 배워보는 JS & Vue JS (4) - 탭 구현 (JS)

|

본 강의는 Inflearn의 김정환 개발자 님의 강의(실습 UI 개발로 배워보는 순수 javascript 와 VueJS 개발)를 듣고 배운 내용을 정리한 포스팅 입니다.

탭 구현 (1)

추천 검색어, 최근 검색어 탭이 검색폼 아래 위치한다.

1) index.html

  • 검색 폼과 검색 결과 사이에 탭 구현
  • id와 class 값에 tabs 입력. tabs 클래스에 대한 스타일은 style.css에 정의되어있음.
<div class="container">
    <ul id="tabs" class="tabs">
        <li>추천 검색어</li>
        <li>최근 검색어</li>
    </ul>
    <div id="search-result"></div>
</div>
  ul.tabs {
    display: flex;
  }
  .tabs li {
    display: inline-block;
    width: 50%;
    padding: 15px;
    text-align: center;
    box-sizing: border-box;
    border-bottom: 1px solid #ccc; 
    background-color: #eee;
    color: #999;
  }

탭 구현 (2)

기본으로 추천 검색어 탭을 선택한다.

style.css에는 tabs 클래스 내 li 태그 내 active 클래스 속성에 대한 스타일이 정의 되어있음. 따라서, active 클래스를 추천 검색어 탭에 적용시키면 됨.

1) TabView.js

  • 다른 View들과 마찬가지로, View를 import 하여 객체를 복사. 디버깅을 위해 Tag 정의
  • TabViewsetup이라는 함수를 만들어 엘레멘트를 주입받아 init함수에 전달함.
  • 탭에 active 클래스를 추가 하기 위해 setActiveTab 이라는 함수 정의. tabName이라는 인자를 받음
    • li 태그를 찾아서 array로 만든 후, forEach 반복문을 돌림.
    • 이때, 처음 맞는 li만 반환하는 것이 아니라 전체 데이터를 반환하기 위해, querySelectorAll() 메서드를 사용해야함.
    • liclassName에다가 active 문자열을 추가해주면 됨.
    • 이때 innerHTMLTabName이 같은 경우에만 active 클래스가 추가되도록 삼항 연산자 사용
import View from './View.js'

const Tag = '[TabView]'

const TabView = Object.create(View)

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

//active tab을 셋팅하는 함수
TabView.setActiveTab = function(tabName){
    Array.from(this.el.querySelectorAll('li')).forEach(li => {
        li.className = li.innerHTML == tabName ? 'active' : ''
    })
}

// MainController.js 에서 사용할 수 있게 export 시킴.
export default TabView

2) MainController.js

  • MainController.js 에서 TabView.js를 호출
  • 다름 view와 마찬가지로, init 함수에서 TabView를 setup 함.
  • 추천검색어를 선택하도록 만들어야됨. 탭은 추천 검색어 또는 최근 검색어를 선택할 수 있음. 그러므로 controller에서 어떤 탭을 선택했는지 내부적으로 가지고 있으면 좋음.
  • controller에 selectedTab이라는 변수를 정의하고 default로 추천 검색어를 입력함.
  • 이후, TabView의 setActiveTab 함수를 호출함. 이때 controller가 가지고있는 selectedTab을 인자로 넘김
import FormView from '../views/FormView.js'
import ResultView from '../views/ResultView.js'
import TabView from '../views/TabView.js' // TabView 추가

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

const tag = '[MainController]'

export default {
    init() {
        FormView.setup(document.querySelector('form'))
            .on('@submit', e => this.onSubmit(e.detail.input))
            .on('@reset', e=> this.onResetForm())
        
        TabView.setup(document.querySelector('#tabs')) // TabView에 대한 setup 설정

        ResultView.setup(document.querySelector('#search-result'))  
        
        this.selectedTab = '추천 검색어'
        TabView.setActiveTab(this.selectedTab)
    },

3) MainController.js

  • controller에는 이미 3개의 view를 갖고 있음. 이 view들을 한번에 그려줄 함수를 추가로 정의해보자.
  • renderView라는 함수 정의. 한번만 renderView라는 함수를 호출하면 controller가 갖고있는 view들을 다 그릴 수 있도록 renderView쪽으로 역할을 위임시킴.
export default {
    init() {
 
		// 중간의 코드는 중략        
        this.selctedTab = '추천 검색어'
        this.renderView()
    },
    
    // 중략
    
    renderView() {
        console.log(tag, 'renderView()')
        TabView.setActiveTab(this.selctedTab)
        ResultView.hide()
    },

탭 구현 (3)

각 탭을 클릭하면 탭 아래 내용이 변경된다.

1) TabView.js

  • TabView.setup 내에서 bindClick 함수 호출 (클릭 이벤트에 대하여 bind 시키는 기능) & bindClick 함수 정의
  • 탭들은 li 엘레멘트로 되어있으므로 li 전체(querySelectorAll('li'))를 arrary로 만든 후 반복문을 돌림.
  • li에 대해 click event를 listener 하도록 코드 구현 & eventListener는 onClick() 으로 넘겨줌. 이때 li.innerHTML (즉 , TabName을 인자로 넘겨줌)
  • onClick 함수에는 TabName이 들어옴. setActiveTab의 인자로 TabName을 넘김.
  • 또한 Tab이 change되었다는것을 Main Controller에게 알려줘야함. TabView는 Tab만 관리하며, 그 아래의 내용은 TabView에서는 신경쓰지 않는 내용임. 따라서 Tab이 변경된 사실만 controller에게 전달하면 됨.
  • controller에서 setup 함수를 실행한다음 체이닝을 이용하여 on 이라는 함수를 실행할 예정이므로 TabView.setup에서 this를 return 시켜 줄 것.
TabView.setup = function(el) {
    this.init(el)
    this.bindClick()
    return this
}


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

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

2) MainController.js

  • @change 이벤트에 대해서 수신하고 있어야함. 이벤트가 들어왔을 때, controller에 onChangeTab이라는 함수를 실행하게 함.(이벤트의 tabName을 전달함 e.detail.tabName )
  • 동작유무를 확인하기 위해 debugger 만 찍어봄. 최종적으로 탭을 누르면 breakpoint에 도달함을 알 수 있음.
export default {

    init() {

        TabView.setup(document.querySelector('#tabs'))
            .on('@change', e => this.onChangeTab(e.detail.tabName))

    },
    
    onChangeTab(tabName) {
        debugger
    }

2019년 7월 1주차 TIL

|

2019-07-01

  • 자바스크립트를 이용하여 Django 인스타 프로젝트를 통해 만든 좋아요 기능을 동적으로 재구현해보았다.

  • JS 코드를 실제 Django 프로젝트에 적용한 첫번째 실습으로, 어떠한 접근방식으로 코드를 짜야하는지에 대한 전반적인 흐름을 알 수 있었음.

  • queryselector를 이용하여 엘레멘트에 접근한 후, 이벤트를 생성하여 axios를 통해 Django url 으로 요청을 보내 응답을 받는 방식으로 진행되었음.

  • 이때 JS에서 응답을 받을 수있도록 views.py의 return 값을 JsonResponse을 이용하여 Json의 형태로 반환해줌.

2019-07-02

  • 어제에 이어, Django 내에서 JS를 통하여 댓글을 생성하고 보여주는 기능으 동적으로 구현해보았음.

  • 전반적인 매커니즘은 어제와 유사했으나, 댓글 생성시 생기는 데이터를 어떻게 보내는지에 대한 코드가 추가되었음. 이때,FormData 라는 것을 이용하여 Form 필드와 그 값을 나타내는 일련의 key/value 쌍을 생성하여 요청을 보냈음.

  • 또한 views.py의 함수 반환값인 JsonResponse를 오브젝트의형태로 저장하는데, 필요한 정보((댓글 내용, id, 댓글 생성자 등)를 key/value의 형태로 저장하여 넘겨서 댓글 리스트를 동적으로 보낼 때 활용하였음.

  • 응답으로 받은 정보를 통해 DOM 내 id값을 고유하게 부여하여, 어떠한 포스트에 대한 댓글인지를 확인하고, queryselector를 이용하여 댓글을 동적으로 리스트에 나타냄.

2019-07-03

  • Vue.js에 대한 본격적인 복습 & 공부를 시작하였음.
  • Vue.js는 템플릿 구문을 통해 선언적으로 DOM에 데이터를 렌더링하는 것부터 시작하였음. el , data , methods 등의 options들이 존재.
  • 반복문과 조건문의 방식은 다른 언어와 유사했는데, 딕셔너리를 반복문으로 돌리는 경우가 조금 특이했다.
    • 딕셔너리에서 반복문을 돌릴경우, 기본적으로 value값이 출력됨. key값을 출력하기 위해서는 Object.keys() 사용
  • Vue.js는 Django의 MVC pattern과 유사한 MVVM이라는 디자인 패턴을 갖고 있음. M(model)과 V(View)는 동일한데, Vue.js가 View와 Model 사이를 데이터바인딩을 통해 controller의 역할을 한다고 하여 VM이라는 명칭을 사용하게 됨.

  • inflearn를 통해 추천 받은 Vue.js 강의가 몇개 있는데, 그 중 하나를 선정해서 추가적으로 공부를 해봐야겠다.

2019-07-04

  • Vue.js를 통해 To-do 앱을 구현(이벤트 리스너 구현)하고, Cat API를 통해 요청을 보내고 응답을 받는 시간을 가져보았음.

  • v-on: 설정된 DOM 이벤트를 통해 JS 코드를 실행시키게 이벤트 리스너를 구현할 수 있음.

  • v-bind : HTML 어트리뷰트(속성)에는 인터폴레이션 값을 직접 넣지 못하는데 이때 사용하는 것이 v-bind 임.

  • 특히, JS 내 this라는 개념에 대해서 조금 깊게 공부를 해보았음. 일반적인 언어에서의 this는 인스턴스의 호출한 대상의 현재 객체를 뜻하는 것인데 JS는 조금 다르게 this가 동작하였음. (특히 함수가 어떻게 호출 되었는지에 따라 동적으로 동작) 1) 기본적인 함수 선언 후 this 출력 시, 전역 객체(window)가 호출됨. 2) Vue 인스턴스내 메서드를 선언하고 내부에서 this를 출력할 경우, 오브젝트의 메서드이므로 오브젝트가 호출됨. 3) arrow function에서의 this는 호출과 위치와 상관없이 상위 스코프 this를 가리킴.
  • methods에서 함수를 정의할 때 함수 내부에서 callback 함수로 또다른 함수를 호출하는 경우가 있는데 이때 this를 호출하면 전역 객체(window)가 호출되는 것에 의구심이 들었다. 각종 정보를 찾아보고 최종적으로 스스로 정리한 내용으로는, this는 항상 상위 객체를 호출하는 것으로 결정을 지었다.

  • 기본적으로 함수 선언할 때 this를 출력할 경우, 함수의 상위 객체는 당연히 전역 객체인 window이며, Vue 인스턴스 메서드에서 this를 호출할 때는 당연히 상위 객체는 Vue 인스턴스이므로 모든 로직이 맞아 떨어진다. 그런데 함수 내부에서 callback 함수를 통해 또다른 함수를 호출하고 그 내부에서 this를 호출하는 경우에는 앞의 함수와 바인딩이 되지 않아 상위 객체를 전역 객체(window)로 인식된다. (함수 내 함수를 호출하는 경우는 앞의 함수와의 연결고리 가 끊어진다고 보면 될듯하다), 따라서 함수 내부에서 상위 스코프 this를 가키리기 이위해 arrow function을 사용한다.

2019-07-05

  • 어제에 이어 Vue.js를 통해 To-do 앱을 확장해보고, Vue의 속성들을 살펴보았다.
  • v-model 은 DOM에서 입력한 값과 Vue 인스턴스 내 data의 값을 동기화 할 때 사용하는 디렉티브이다. (양방향 데이터 바인딩 생성)
  • Vue에는 methods와 유사한 역할을 하는 computed가 있는데 그 둘의 차이는 캐싱의 유무이다.
  • 그 외 어떠한 값이 변경되면 함수가 실행될 때 사용하는 watch도 있음.
  • v-text vs v-htmlv-if vs v-show의 차이점을 살펴 보았음.
  • inflearn에서 김정환 님의 실습 UI 개발로 배워보는 순수 javascript 와 VueJs 개발 이라는 강의를 구매하였다. 실습 위주로 진행되는데 무엇보다도 동일한 기능을 JS 와 Vue.js 둘다 구현하여 어떠한 차이점이 있는지를 알 수 있다는 것이, 매우 흥미롭게 들렸다.

2019-07-07

  • 주말 동안 꾸준히 inflearn에서 김정환 님의 실습 UI 개발로 배워보는 순수 javascript 와 VueJs 개발 강의를 듣고 있다.
  • 지금까지 자바스크립트를 배운 방식은 HTML 파일을 만들어 script 태그 내에서 코드를 작성하거나 별도의 js 파일을 만들어 link로 연결하는 간단한 방식으로 배웠는데, JS를 MVC 패턴에 따라 용도를 분류하고, 코드를 작성하는 방식이 매우 생소했다.
  • view 파일을 만들어 복사하거나 다른 js에서 사용을 위해 export 시키는 방법 등, 알지 못했던 새로운 방식을 많이 알 수 있었다. 또한 기본적인 작동방법만 알았던 JS의 배열 조작 메서드 중 하나인 reduce 를 이런방식으로 활용할 수 있다는 점이 매우 놀라웠다.
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>'
}
  • 무엇보다도 총 4시간 짜리 강의를 약 13시간동안 들었음에도 불구하고, 이제 전체강의의 반밖에 듣지 못하였다. (JS로 기능 구현까지 강의 완료). 강의를 들으면서 코드를 따라 작성하고, Markdown을 꼼꼼히 작성하는 방식으로 공부를 하고 있는데, 이럴 때면 공부하는 방식에 대해 많은 회의감이 드는건 어쩔수 없는 것 같다. 강의만을 몇차례 완독하는 방식이 오히려 공부 체득에는 더 효율적일 것이라고 생각은 들지만, 마크다운을 통해 나만의 언어로 내용을 정리하는게 나중에 복습을 할 때나, 관련 내용을 찾아볼 때 더 낫다고 판단하기에 별 다른 방도 없이 열심히 강의를 들을 수 밖에 없을 것 같다.

  • 다음주도 화이팅이다!

실습 UI 개발을 통해 배워보는 JS & Vue JS (3) - 검색결과 구현 (JS)

|

본 강의는 Inflearn의 김정환 개발자 님의 강의(실습 UI 개발로 배워보는 순수 javascript 와 VueJS 개발)를 듣고 배운 내용을 정리한 포스팅 입니다.

검색 결과 구현 (1)

검색 결과가 검색폼 아래 위치한다

1) index.html

  • FormView와 다르게 검색결과는 미리 데이터가 미리 정해져있지 않음. 검색 데이터를 서버에 요청하고 서버가 준 검색데이터를 받아서 동적으로 보여줘야함.
  • 검색 결과를 위한 ResultView가 mount 될 수 있는 엘레멘트만 넣도록 하면 됨.
  • 기본 틀 생성 후, ResultView.js 생성
<div class="container">
    <div id="search-result"></div>
</div>

2) ResultView.js

  • index.html > search-result 라는 엘레멘트에 무언가를 출력하도록 구현
  • FormView와 마찬가지로 View.js를 이용하여 코드 작성
  • 디버깅을 위한 태그 정의 및 View 객체를 이용하여 복사
  • Setup 메서드 정의
    • 엘레멘트를 주입받아 내부 속성으로 엘레멘트를 갖고 있음 (그 역할은 init 메서드가 수행)
import View from '.View.js'

// 디버깅을 위한 태그 정의
const tag = '[ResultView]'

// View 객체를 이용하여 복사
const ResultView = Object.create(View)


// Setup 메서드 정의
// 엘레멘트를 주입받아 내부 속성으로 엘레멘트를 갖고 있음.
// (그 역할은 init 메서드가 수행)
ResultView.setup = function(el) {
    this.init(el)
}

3) MainController.js

  • ResultView를 MainController에서도 사용할 수 있게 코드 추가
  • 컨트롤러를 초기화하는 init 메서드에서 ResultView를 setup
import FormView from '../views/FormView.js'
import ResultView from '../viewsResultView.js'

const tag = '[MainController]'

export default {
    // 컨트롤러 초기화 부분
    init() {
        FormView.setup(document.querySelector('form'))
            .on('@submit', e => this.onSubmit(e.detail.input))
            .on('@reset', e=> this.onResetForm())
        
        ResultView.setup(document.querySelector('#serach-reesult'))  
    },

4) ResultView.js

  • 실제 검색결과가 그려지기 위해 ResultView에 render 함수 정의
  • render 함수는, 서버로부터 받아온 검색결과 데이터를 동적으로 그려주는 역할을 함.
    • data는 컬렉션으로 받으며, 없는 경우는 빈배열로 기본값 설정
    • 엘레멘트의 innerHTML을 통해 데이터를 받도록 하자, 2가지 경우가 존재(데이터가 있는/없는 경우)
    • 배열의 길이가 있는 경우 => 실제 그 데이터로 그리도록 함.(그리는 역할은 getSearchResultsHtml 이라는 함수로 뽑을 예정)
    • 데이터가 없는 경우, 문구 삽입
  • getSearchResultsHtml 는 데이터를 받아 debugger를 걸어 멈추도록 하자
  • ResultView를 컨트롤러에서 사용할 수 있도록 모듈을 export
//HTML DOM을 만들어내는 함수
ResultView.render = function(data = []) {
    console.log(tag, 'render()', data)
    this.el.innerHTML = data.length ? this.getSearchResultsHtml(data) : '검색 결과가 없습니다'
}

ResultView.getSearchResultsHtml = funtion(data) {
    debugger
}

export default ResultView;

5) MainController.js

HTML DOM을 만들어내는 render 함수를 MainController가 호출해야됨. 그시점에대해서 생각해보도록 하자.

  • Form에서 입력이 발생 했을 때, @submit 이벤트 발생
  • @submit 이벤트를 처리하는 것이 onSubmit 함수
  • onSubmit 함수는 입력데이터를 받아서 검색 요청을 하게 될 것임
  • 검색요청하는 메서드 searchonsubmit 함수 내에서 호출
  • Search 함수는 search API를 백엔드로 호출. 그 결과를 받아 onSearchResult가 처리. 뭔가 데이터를 받아 넘겨주도록 할 것임.
  • onSearchResult 함수는 서버로 부터 받은 데이터를 ResultView.js > render 메서드로 넘김
import FormView from '../views/FormView.js'
import ResultView from '../viewsResultView.js'

const tag = '[MainController]'

export default {
    init() {
        FormView.setup(document.querySelector('form'))
            .on('@submit', e => this.onSubmit(e.detail.input))
            .on('@reset', e=> this.onResetForm())

        ResultView.setup(document.querySelector('#serach-reesult'))  
    },

    // step 2
    search(query) {
        console.log(tag, 'search()', query)
        // search API
        this.onSearchResult([])

    },

    onSubmit(input) {
        console.log(tag, 'onSubmit()', input)
        // Step 1
        this.search(input)
    },

    onResetForm(input) {
        console.log(tag, 'onResetForm()')
    },

    // step 3
    onSearchResult(data) {
        ResultView.render(data)
    },
}

6) 검색 결과 프로세스 Summary

  1. FormView에서 @submit 이벤트가 발생하였을 때(엔터키가 눌러졌을 때), onSumit 함수가 구동
  2. 검색 요청을 위해 search 함수 실행
  3. search 함수는 search API를 통해 데이터를 얻어옴.
  4. 그 데이터를 받아서 onSearchResult를 실행함
  5. onSearchResult는 데이터를 받아 ResultView.js 의 render 함수로 넘겨줌
  6. render 함수는 검색 결과를 그림

7) 실제 API 호출하기

  • models/SearchModel.js 내 list 함수를 이용하여 데이터를 갖고 오도록 하자.
  • MainController에서 SearchModel import
  • 검색하는 search 함수 내에서 SearchModel의 list를 호출함. 이때 검색어를 넘겨줄 것.
    • 이 list는 Promise를 반환하기 때문에 then 함수를 쓸 수 있음. then 함수의 검색결과로 data가 넘어오고, 그 data에다가 onSearchResult의 data로 넘겨줌.
import SearchModel from '../models/SearchModel.js'

export default {
    /// 중략 
    search(query) {
        console.log(tag, 'search()', query)
        SearchModel.list(query).then(data => {
            this.onSearchResult(data)
        })

    },

위의 코드대로 서버를 실행시켜 검색어를 입력한 후 검색을 하면 ResultView.getSearchResultHtml 에서 멈춤. 이말은 ResultView에서 render를 그릴 때, data의 length값이 없었기 때문에, “검색 결과가 없습니다” 라는 결과가 떴지만, 이번에는 data의 length가 있기 때문에, getSearchResultHtml 가 실행되었고, debugger로 인해 break point로 멈춘 것을 확인 할 수 있음.

검색 결과 구현 (2)

검색 결과가 보인다.

실제 검색 결과를 보여주기 위해서 ResultView.js 내 getSearchResultsHtml 함수를 구현해보도록 하자.

1) ResultView.js

  • innerHTML의 값으로 들어가므로, HTML 문자열만 return 해주면 됨. 데이터는 collection 이므로, reduce 함수를 이용
  • 초기값으로 '<ul>' 을 넘겨주고, 함수가 끝난 후 닫는 ul 태그를 달아줌.
  • reduce 콜백함수의 첫번째 인자로 들어가는 누적값에다가 li 태그 및 값을 만들어주면 됨.
  • li를 만들어내는 getSearchItemHtml 함수를 만들고 item 데이터를 넘김.
  • getSearchItemHtml
    • li 태그로 이루어진 문자열을 반환함
    • 검색 결과에는 상품 이미지와 상품 제목이 들어가므로, 이미지 태그와 p 태그를 이용하여 이미지와 제목을 출력
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>`
}

검색 결과 구현 (3) - 실습

x버튼을 클릭하면 검색폼이 초기화 되고, 검색 결과가 사라진다

1) MainController.js

  • 입력한 검색어를 초기화 시켜주는 onResetForm 함수를 활용하여 검색결과를 없앨 수 있음.
  • ResultView 함수의 hide 메서드 사용
  • ResultView의 hide는 ResultView가 복사한 View 함수 내 정의되어있는 메서드임.
    onResetForm(input) {
        console.log(tag, 'onResetForm()')
        ResultView.hide()
    },

2) 나의 코드

2-1) MainController.js
  • OnResetFrom 함수에서 ResultView 내 reset이라는 함수를 호출시킴
    onResetForm(input) {``
        console.log(tag, 'onResetForm()')
        ResultView.reset()
    },
2-2) ResultView.js
  • 엘레멘트의 innerHTML의 값을 초기화시킴
ResultView.reset = function(){
    console.log(tag, 'reset()')
    this.el.innerHTML = ''

}