본문으로 바로가기

참고

 

Vanilla Javascript로 웹 컴포넌트 만들기 | 개발자 황준일

Vanilla Javascript로 컴포넌트 만들기 9월에 넥스트 스텝 (opens new window)에서 진행하는 블랙커피 스터디 (opens new window)에 참여했다. 이 포스트는 스터디 기간동안 계속 고민하며 만들었던 컴포넌트를

junilhwang.github.io

기능 추가

현재는 아이템을 추가하는 일만 하고 있지만 좀 더 기능을 추가해서 todo list처럼 작동하게 하고자한다

아이템의 내용을 입력받는 input, 완료/미완료 토글 버튼, 완료/미완료를 기준으로 아이템을 필터하는 버튼

class Items extends Component{
    get filteredItems (){
        const {filterBy, items} = this.state
        return filterBy==='all' ? items : items.filter(({active})=>active===filterBy*1)
    }
    setEvent() {
        this.addEvent('click','.activate',({target})=>{
            const items = [...this.state.items]
            const targetIndex = target.closest('[data-index]').dataset.index*1
            this.setState({
                items: items.map(
                    item=>item.index!==targetIndex ?
                        item :
                        {...item, active: item.active ? 0 : 1}
                )
            })
        })
        this.addEvent('click','.remove',({target})=>{
            const items = [...this.state.items]
            const targetIndex = target.closest('[data-index]').dataset.index*1
            this.setState({items:items.filter(({index})=>index!==targetIndex)})
        })

        this.addEvent('click','.add',(e)=>{
            e.preventDefault()
            const input = this.$target.querySelector('.add-input')
            const nextState = {
                text: input.value ? input.value : `item${this.index}`,
                active: 0,
                index: this.index
            }

            this.setState({items:[...this.state.items,nextState]})
            this.index++
        })
        this.addEvent('click','.toggle',({target})=>{

            const stand = target.closest('[data-stand]').dataset.stand

            this.setState({filterBy: stand})

        })
    }

    setup() {
        this.state = {
            filterBy: 'all',
            items: [
                    {
                        text: 'item1',
                        index: 1,
                        active: 0
                    },
                    {
                        text: 'item2',
                        index: 2,
                        active: 0
                    },
                ]};
        this.index = 3
    }

    template() {
        return `
        <form >
          <input type="text" class="add-input" placeholder="내용을 입력하세요" autofocus>
          <button class="add">추가</button>
      </form>
          <div >
            <button class="toggle" data-stand="all">전체 아이템 보기</button>
            <button class="toggle" data-stand="1">완료된 아이템 보기</button>
            <button class="toggle" data-stand="0">미완료 아이템 보기</button>
          </div>
        <ul>
            ${this.filteredItems.map(({text, active,index})=>
                `
                <li data-index="${index}" >${text}
                    <button class="activate" data-active="false" style="color: ${active ? '#09F' : '#F09'}">${active? '완료':'미완료'}</button>
                    <button class="remove">삭제</button>
                </li> 
                `).join("")}
         </ul>
         `
    }
}

전체적인 틀은 참고한 블로그와 비슷하지만 내가 먼저 짜보고 코드와 비교하다보니 다른 부분이 조금 있다

이제 어엿한 하나의 앱이 되었는데 과연 이게 컴포넌트 기반의 앱일까..?

App에서는 Items 컴포넌트 하나만 불러오고 Items 컴포넌트가 추가, 삭제, 토글 모든 기능을 다 담당하고 있어서 혼자 너무 뚱뚱해져버렸다

컴포넌트 분할

이제 Items 컴포넌트에 붙은 살들을 보기좋게 골고루 퍼지게 해야한다

이 때도 리액트를 떠올리면 어떻게 진행해야할지 잘 보인다

상태값은 상위 컴포넌트에서 하위 컴포넌트로 흘러야한다 -> App에 모든 데이터를 다 넘겨주자

프레젠테이션 컴포넌트는 데이터가 어떻게 처리되는지 모른다 -> 안에서 작동하는 로직들도 다 빼주자

App이 데이터를 가지고 있고 컴포넌트에서 로직을 다 빼버리면 컨테이너는 자연스럽게 App이 하게된다

그렇게 한 번 작성해보자

App.js

class App {
    state = {
        filterBy: 'all',
        items: [
            {
                text: 'item1',
                index: 1,
                active: 0
            },
            {
                text: 'item2',
                index: 2,
                active: 0
            },
        ]
    };
    index = 3
    constructor() {
        this.init()
    }

    get filteredItems (){
        const {filterBy, items} = this.state
        return filterBy==='all' ? items : items.filter(({active})=>active===filterBy*1)
    }

    init(){
        const $target = document.querySelector('#app')
        const itemInput = new ItemInput($target,{})
        const itemFilters = new ItemFilters($target,{})
        const items = new Items($target,this.state)
    }
}

export default App

Items.js

import Component from "../lib/Component.js";
class Items extends Component{

    template() {
        const {items} = this.props
        return `
        <ul>
            ${items.map(({text, active,index})=>
                `
                <li data-index="${index}" >${text}
                    <button class="activate" data-active="false" style="color: ${active ? '#09F' : '#F09'}">${active? '완료':'미완료'}</button>
                    <button class="remove">삭제</button>
                </li> 
                `).join("")}
         </ul>
         `
    }
}
export default Items

Iteminput.js

import Component from "../lib/Component.js";

class ItemInput extends Component{
    template(){
        return `
    <form >
        <input type="text" class="add-input" placeholder="내용을 입력하세요" autofocus>
        <button class="add">추가</button>
    </form>`
    }
}

export default ItemInput

ItemFilters.js

import Component from "../lib/Component.js";
class ItemFilters extends Component{
    template(){
        return `
    <div >
        <button class="toggle" data-stand="all">전체 아이템 보기</button>
        <button class="toggle" data-stand="1">완료된 아이템 보기</button>
        <button class="toggle" data-stand="0">미완료 아이템 보기</button>
    </div>
`
    }
}

export default ItemFilters

Component.js

class Component {
    constructor ($target,props) {
        ...
           this.props = props
        ...

    render () {
        const elem = document.createElement('div')
        elem.classList.add(`${this.constructor.name}`)
        elem.innerHTML = this.template()
        this.$target.appendChild(elem)
    }
    ...

export default Component

원문에서는 App이 컴포넌트 클래스를 상속받으면서 화면을 구성하고 있는데 이부분이 잘 이해가 되지 않아서 App을 컴포넌트로 작성하지 않았다

그에 맞게 Component.js도 살짝 수정해주었고 이벤트를 제외하고는 데이터와 로직은 App에 주고 각 컴포넌트는 기능을 기준으로 분리했다

구현하다보니 setState를 불러올 때마다 App 컴포넌트 전체가 아닌 items에 해당하는 영역만 렌더링되게할 수 있을 것 같아서 그렇게 구현했다

내가 커스텀 하자마자 일관성이 깨지고 더러워지는 코드를 보고 경악했지만..

    constructor() {
        super(...arguments)
        const $target = document.querySelector('#app')
        this.itemInput = new ItemInput($target, {
            addItem : this.addItem
        })
        this.itemFilters = new ItemFilters($target,{
            filterChange : this.filterItem
        })
        this.items = new Items($target, {
            items : this.state.items,
            filterBy : this.state.filterBy,
            removeItem : this.removeItem,
            toggleItem : this.toggleItem,
        })
    }



    //this를 이용하기 위해서 애로우 펑션 사용
    addItem = (value) =>{
        const items = [...this.state.items]
        const newItem = {text: `${value ? value : 'item'+this.index}`, index : this.index, active: 0}

        this.setState('items', {items: [...items, newItem]})

        this.index++
    }
    toggleItem = (targetIndex) =>{
        const items = [...this.state.items]
        this.setState('items', {
            items: items.map(item => item.index !== targetIndex ?
                item :
                {...item, active: item.active ? 0 : 1})
        })
    }

    removeItem = (targetIndex) =>{
        const items = [...this.state.items]
        this.setState('items', {items: items.filter(item => item.index !== targetIndex)})
    }
    filterItem = (stand)=>{
        this.setState('items', {filterBy: stand})
    }

    // App컴포넌트 전체가 렌더링되는게 싫어서 변하는 부분만 다시 렌더링 시켜주기
    setState(component,nextState){
        this.state = {...this.state,...nextState}
        this[component].setState(this.state)
    }

https://github.com/ludacirs/vanilla-js/tree/main/simple-component

'JavaScript' 카테고리의 다른 글

local/session Storage  (0) 2021.05.22
fetch API  (0) 2021.05.21
바닐라 자바스크립트로 컴포넌트 만들기 -1  (0) 2021.05.17
IntersectionObserve API, 무한 스크롤  (0) 2021.05.15
IntersectionObserve API, 레이지 로딩  (0) 2021.05.13