재우니의 블로그

 

Vue.js & web api 를 활용한 동적 select box 구현하기 (using cdn) - 2탄

 

 

미리 보기

 

기존 강의 내용을 좀 더 기능을 확장하고자 합니다. 여기에 행추가 및 행삭제를 넣어볼까 합니다.

 

 

 

Vue.js & web api 를 활용한 동적 select box 구현하기 (using cdn)

 

이전 강의를 간략히 설명을 드리면, Vue.js를 활용하여 스텝별로 다른 옵션을 보여주고 선택하는 드롭다운 메뉴를 구현한 코드입니다. 이 코드는 Vue.js의 기본적인 개념인 반응형 데이터, 메소드, 조건부 렌더링(v-show), 이벤트 리스너(v-on:change), 그리고 라이프사이클 훅(mounted)을 활용하고 있습니다. 또한, 각 스텝에서 선택한 옵션에 따라 다음 스텝의 옵션을 동적으로 불러오는 기능을 포함하고 있으며, 이를 위해 Promise 기반의 비동기 처리 방식인 async/await를 사용하여 외부 API에서 데이터를 불러오는 작업을 수행하고 있습니다.

 

 

https://aspdotnet.tistory.com/3168

 

 

행 추가 및 삭제 기능

 

행 추가 및 삭제 기능을 포함하는 코드를 작성하겠습니다. Vue.js에서는 배열을 사용하여 여러 행을 관리할 수 있습니다. 아래 코드는 기존 코드에 행 추가 및 삭제 기능을 포함한 버전입니다: web api 호출은 jquery 를 사용했습니다.

 

 

 

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue.js를 이용한 동적 드롭다운</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>

<body>
    <div id="app">
        <div v-for="(row, index) in rows" :key="index">
            <select name="step1" v-model="row.selectedBreed" v-on:change="fetchSubBreeds(index)">
                <option value="">단계 1 선택</option>
                <option v-for="breed in breeds" :value="breed.id">{{ breed.name }}</option>
            </select>

            <select name="step2" v-model="row.selectedSubBreed" v-on:change="fetchImages(index)">
                <option value="">단계 2 선택</option>
                <option v-for="subBreed in subBreeds[index]" :value="subBreed">{{ subBreed }}</option>
            </select>

            <select name="step3" v-model="row.selectedImage">
                <option value="">단계 3 선택</option>
                <option v-for="image in images[index]" :value="image">{{ image }}</option>
            </select>
            <button @click="removeRow(index)">행 삭제</button>
            <br>
            <img v-if="row.selectedImage" :src="row.selectedImage" alt="dog" width="200" height="200">
            <br>
        </div>
        <button @click="addRow">행 추가</button>
    </div>

    <script>
        new Vue({
            el: '#app',
            data: {
                breeds: [],
                rows: [{
                    selectedBreed: 'terrier',
                    selectedSubBreed: 'american',
                    selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_366.jpg'
                },
                {
                    selectedBreed: 'terrier',
                    selectedSubBreed: 'american',
                    selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10164.jpg'
                },
                {
                    selectedBreed: 'terrier',
                    selectedSubBreed: 'american',
                    selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10164.jpg'
                },
                {
                    selectedBreed: 'terrier',
                    selectedSubBreed: 'american',
                    selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10328.jpg'
                },
                {
                    selectedBreed: 'terrier',
                    selectedSubBreed: 'american',
                    selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10365.jpg'
                }],
                subBreeds: [],
                images: []
            },
            methods: {
                fetchBreeds() {
                    return $.ajax({
                        url: 'https://dog.ceo/api/breeds/list/all',
                        method: 'GET',
                        dataType: 'json'
                    })
                        .done((data) => {
                            this.breeds = Object.keys(data.message).map(breed => ({
                                id: breed,
                                name: breed
                            }));
                        })
                        .fail((jqXHR, textStatus, errorThrown) => {
                            console.error("Failed to fetch breeds", errorThrown);
                        });
                },
                fetchSubBreeds(index) {
                    this.images[index] = []; // 이미지 초기화
                    if (this.rows[index].selectedBreed) {
                        return $.ajax({
                            url: `https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/list`,
                            method: 'GET',
                            dataType: 'json'
                        })
                            .done((data) => {
                                this.$set(this.subBreeds, index, data.message);
                            })
                            .fail((jqXHR, textStatus, errorThrown) => {
                                console.error(`Failed to fetch sub-breeds for breed ${this.rows[index].selectedBreed}`, errorThrown);
                            });
                    } else {
                        this.rows[index].selectedSubBreed = '';
                        this.rows[index].selectedImage = '';
                    }
                },
                fetchImages(index) {
                    this.images[index] = [];
                    if (this.rows[index].selectedBreed) {
                        let url = this.rows[index].selectedSubBreed ?
                            `https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/${this.rows[index].selectedSubBreed}/images/random/10` :
                            `https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/images/random/10`;
                        return $.ajax({
                            url: url,
                            method: 'GET',
                            dataType: 'json'
                        })
                            .done((data) => {
                                this.$set(this.images, index, data.message);
                            })
                            .fail((jqXHR, textStatus, errorThrown) => {
                                console.error(`Failed to fetch images for breed ${this.rows[index].selectedBreed}`, errorThrown);
                            });
                    } else {
                        this.rows[index].selectedImage = '';
                    }
                },
                addRow() {
                    this.rows.push({
                        selectedBreed: '',
                        selectedSubBreed: '',
                        selectedImage: '',
                    });
                    this.subBreeds.push([]);
                    this.images.push([]);
                },
                removeRow(index) {
                    this.$delete(this.rows, index);
                    this.$delete(this.subBreeds, index);
                    this.$delete(this.images, index);
                }
            },
            async mounted() {
                await this.fetchBreeds();
                for (let i = 0; i < this.rows.length; i++) {
                    await this.fetchSubBreeds(i);
                    if (this.rows[i].selectedSubBreed) {
                        await this.fetchImages(i);
                    }
                }
            }
        });
    </script>
</body>

</html>

 

 

이 코드에서는 rows 배열에 각 행의 데이터를 저장하고, subBreeds와 images 배열에 각 행별로 sub Breeds 와 이미지를 저장합니다. 행 추가 버튼을 누르면 addRow 메서드가 호출되어 행이 추가되고, 행 삭제 버튼을 누르면 removeRow 메서드가 호출되어 해당 행이 삭제됩니다. 이 때 splice 메서드를 사용하여 배열에서 특정 인덱스의 요소를 제거합니다. 각 드롭다운 메뉴에서 선택한 옵션이 변경되면 해당 행의 데이터를 업데이트하고, 필요한 경우 다음 step 의 옵션을 불러옵니다.

 

이전 코드와 크게 다른 점은 이제 행추가 기능으로 선택한 값을 아래와 같이 rows: [] 배열로 저장한다는 점입니다.

 

data: {
    breeds: [],
    subBreeds: [[]],
    images: [[]],
    rows: []
}

 

row 에는 각각 아래 json  형태로 저장됩니다.

{
    selectedBreed: 'terrier',
    selectedSubBreed: 'border',
    selectedImage: 'https://images.dog.ceo/breeds/terrier-border/n02093754_115.jpg',
}

 

 

그리고 각 메소드에서 행의 index 를 파라미터로 받아 해당 행의 데이터를 참조하도록 수정하였습니다. this.$set 함수를 사용하여 subBreeds와 images 배열의 특정 인덱스에 값을 설정하였습니다. 이렇게 하면 Vue.js가 배열의 변경을 감지하고 화면을 업데이트할 수 있습니다. 추후 행 삭제를 위해 키값 기준은 index 으로 지정되어 있습니다. (this.$set(this.subBreeds, index, data.message);)

 

fetchSubBreeds(index) {
    this.images[index] = []; // 이미지 초기화
    if (this.rows[index].selectedBreed) {
        return $.ajax({
            url: `https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/list`,
            method: 'GET',
            dataType: 'json'
        })
            .done((data) => {
                this.$set(this.subBreeds, index, data.message);
            })
            .fail((jqXHR, textStatus, errorThrown) => {
                console.error(`Failed to fetch sub-breeds for breed ${this.rows[index].selectedBreed}`, errorThrown);
            });
    } else {
        this.rows[index].selectedSubBreed = '';
        this.rows[index].selectedImage = '';
    }
},

 

 

vue.js 3 에서는 참고로 아래처럼 this.배열data.$set 으로 작성을 합니다.

 

this.subBreeds.$set(this.subBreeds, index, data.message);

 

 

Vue.js 2에서는 Vue.set 또는 this.$set 을 사용해야 합니다. 

this.$set(this.subBreeds, index, data.message);

 

 

그리고 초기값으로 선택된 값들을 셋팅하기 위해서는, mounted 라이프사이클 훅에 추가해야 합니다. 이를 위해 fetchSubBreeds() 메소드를 각 행에 대해 호출하도록 수정하겠습니다.

async mounted() {
    await this.fetchBreeds();
    for (let i = 0; i < this.rows.length; i++) {
        await this.fetchSubBreeds(i);
        if (this.rows[i].selectedSubBreed) {
            await this.fetchImages(i);
        }
    }
}

 

 

행 추가는 addRow() 메소드를 사용하고, 행 삭제는 removeRow(index) 인덱스를 통해 $delete() 함수로 data 배열을 제거합니다. 행 추가의 기본값은 push 함수 내의 json 속성에 기재하면 되며, 2스텝의 subBreads 와 3 스텝의 images 배열은 web api 를 통해 데이터를 받을 예정이므로 지정된 속성이 아닌 [] 으로 초기화 되어 있습니다. 

addRow() {
    this.rows.push({
        selectedBreed: '',
        selectedSubBreed: '',
        selectedImage: '',
    });
    this.subBreeds.push([]);
    this.images.push([]);
},
removeRow(index) {
    this.$delete(this.rows, index);
    this.$delete(this.subBreeds, index);
    this.$delete(this.images, index);
}

 

 

원본 소스

 

 

vuesample1.html
0.01MB