Vuetify의 소소한 문제들

들어가며

VuetifyVue 개발에 아주 유용한 style library이다. Vue를 전제로 개발되었기 때문에 호환성도 무척 높을 뿐만 아니라 유저들이 원하는 대부분의 기능들이 구현되어 있다. 원하는 예제를 잘 골라 copy-and-paste만 잘 해도 그럴싸한 외형을 만들 수 있다.

그러나 기능을 깊이 사용하려면 Vuetify 뿐만 아니라 Vue까지 잘 이해하고 있어야 한다. 게다가 공식문서가 매우 충실한 편임에도 불구하고 사용자가 알기 어려운 문제들도 있다.

여기에서는 개발 중에 만난 몇가지 문제들과 찾아낸 해결책을 기록해 둔다.

v-autocomplete

먼저 input에서 자동완성을 지원해 주는 v-autocomplete directive이다. 테그 등을 입력할 때 매우 유용하다. 그러나 기능이 세세하게 구비되어 있는 만큼 API를 잘 이용하기까지는 시행착오가 불가피 했다.

items가 객체인 경우

자동완성은 전체 items에서는 입력값에 부합되는 subset items를 보여주는 기능이다. items가 아래와 같이 객체라면 어떻게 해야 할까? input에 텍스트로 표시되어야 하는 object 값의 key를 item-text로 알려주고(“Set property of items’s text value”), 실제로 입력되는 값에 해당하는 object 값의 key를 item-value로 지정해 주어야 한다. 하지만 이렇게만 하면, 입력 결과가 item-value로 지정한 값, 즉 여기에서는 string 값을 가지게 된다. 입력 결과를 object로 하기 위해서는 return-object directive를 반드시 넣어 주어야 한다.

<script>
export default {
    data(){
        return people: [
                    { name: 'Sandra Adams', group: 'Group 1' },
                    { name: 'Ali Connors', group: 'Group 1' },
                    { name: 'Trevor Hansen', group: 'Group 1' },
                    { name: 'Tucker Smith', group: 'Group 1' },
                ]
    }
    /* ... */
}
</script>
<v-autocomplete
    :items="people"
    item-text="name"
    item-value="name"
    v-model="friends"
    return-object
></v-autocomplete>

tag를 선택한 뒤에 사용자 입력값 지우기

v-autocomplete에서 여러개의 tag를 선택할 수 있는 multiple directive를 사용하면, tag를 선택한 뒤에도 사용자가 입력한 텍스트가 그대로 살아있게 된다. 따라서 다음과 같이 input event를 이용하여 search-input 값을 초기화 시켜줄 필요가 있다. ref

<v-autocomplete
    :items="people"
    item-text="name"
    item-value="name"
    v-model="friends"
    return-object
    multiple
    :search-input.sync="userInput"
    @input="userInput=null"
></v-autocomplete>

tag를 선택한 뒤에 menu slot 닫기

v-autocomplete에서 여러개의 tag를 선택할 수 있는 multiple directive를 사용하면, tag를 선택한 뒤에도 menu slot이 사라지지 않고 열려 있는 채로 다음 입력을 기다리게 된다. 데스크톱 환경에서는 문제가 없지만, 모바일 기기에서는 slot이 화면을 가득 채우기 때문에 입력이 불편해 질 수 있다. 따라서 하나의 값을 입력한 뒤에 slot을 닫을 필요가 생긴다.

slot을 닫기 위해서는 아래와 같은 menu-props directive를 넣어 주면 된다.

<v-autocomplete
    :items="people"
    item-text="name"
    item-value="name"
    v-model="friends"
    return-object
    multiple
    menu-props="{'closeOnContentClick': true}"
></v-autocomplete>

이렇게 하면 이론적으로 잘 동작해야 하지만, 기기에 따라 브라우저에 따라 잘 동작하지 않는 경우도 있었다. 그래서 실재 개발에서는 편법을 이용하였다. menu는 focus 되면 열리고 blur 되면 닫히게 되어 있다. 따라서 아래와 같이 입력 후에 blur 처리를 해 주고 조금 뒤에 focus를 주면 더 자연스럽게 이 문제를 해결할 수 있다.

<template>
    <div>
        <v-autocomplete
            ref="autoinput"
            :items="people"
            item-text="name"
            item-value="name"
            v-model="friends"
            return-object
            multiple
            @input="inputChanged"
        ></v-autocomplete>
        <!-- ... -->
    </div>
    <template>
        <script>
            export default {
                // ...
                methods: {
                    inputChanged() {
                        //↓ For clear v-menu slot
                        this.$refs.autoinput.blur()
                        setTimeout(() => {
                            this.$refs.autoinput.focus()
                        }, 500)
                    },
                },
            }
        </script></template
    ></template
>

사용자 입력이 완료되기를 기다리기

v-autocomplete는 사용자가 키보드로 입력을 할 때마다 items을 필터링한다. 한글의 경우 , 학ㄱ 등을 넣어도 필터링이 되는데, 대부분의 경우 불필요한 로딩이다. 더군다나 items를 remote server에서 가져온다고 했을 때는 네트워크 로딩까지 겹치게 된다.

이런 경우에는 사용자가 입력을 마칠 때 까지 기다렸다가 필터링 하는 편이 유리히다. 0.5초 정도 입력이 없는 것을 입력이 완료되었다고 본다면 다음과 같이 하면 된다. ref

<template>
    <div>
        <v-autocomplete
            :items="people"
            item-text="name"
            item-value="name"
            v-model="friends"
            return-object
            multiple
            @input="inputChanged"
            :search-input.sync="userInput"
        ></v-autocomplete>
        <!-- ... -->
    </div>
</template>

<script>
    export default {
        data() {
            return {
                people: [
                    /* ... */
                ],
                userInput: null,
            }
        },
        props: ["itemData"],
        watch: {
            userInput(val) {
                if (!val) {
                    return
                }
                this.fetchEntriesDebounced()
            },
        },
        methods: {
            fetchEntriesDebounced() {
                // cancel pending call
                clearTimeout(this._timerId)

                // delay new call 500ms
                this._timerId = setTimeout(() => {
                    // maybe : this.fetch_data()
                    this.people = this.itemData ? this.itemData : []
                }, 500)
            },
        },
    }
</script>

v-lazy

한 페이지에 표시해야 할 데이터가 많은 경우, 현재 화면에 나타낼 결과만 보여주는 것이 효과적이다. lazy 전략이다. Vuetify에서는 이를 위해 v-lazy directive를 제공한다. 하지만 사용 방법은 조금 어렵다. ref

card를 화면에 뿌려주기 위해 v-lazy를 사용한다고 하였을 때,

  1. v-sheetv-responsivev-lazy를 감싸야 한다.
  2. 대략적인 card의 높이를 알고 v-sheetv-responsivemin-height attribute를 통해 알려주어야 한다.
  3. 각 객체 (여기서는 card)에 isActive를 설정해 주어한다. 물론 객체에 isActive가 있어야 하는 것은 아니다. v-lazy에서 참조용으로 선언만 해 주면 된다.
<template>
    <v-row class="fill-height overflow-y-auto" dense>
        <v-col
            cols="12"
            lg="4"
            md="6"
            v-for="(item, idx) in items"
            :key="idx"
            no-gutters
        >
            <v-sheet min-height="100" class="fill-height" color="transparent">
                <v-lazy
                    class="fill-height"
                    v-model="item.isActivate"
                    :options="{threshold: .5}"
                >
                    <v-card class="fill-height" rounded hover>
                        <div class="d-flex flex-no-wrap justify-space-between">
                            <v-card-title></v-card-title>
                            <v-card-subtitle
                                ></v-card-subtitle
                            >
                            <v-card-text></v-card-text>
                        </div>
                    </v-card>
                </v-lazy>
            </v-sheet>
        </v-col>
    </v-row>
</template>

<script>
    export default {
        data() {
            return {
                items: [
                    { title: "title1", subtitle: "subtitle1", text: "text1" },
                    { title: "title2", subtitle: "subtitle2", text: "text2" },
                    { title: "title3", subtitle: "subtitle3", text: "text3" },
                    /* ... */
                    // item must not have "isActivate" key & value
                ],
            }
        },
    }
</script>
... ... ... ...
Back