Vue 10-2 Pass Props & Emit Events PJT ๊ตฌํ˜„

Seungju Hwangยท2021๋…„ 1์›” 17์ผ
0

Vue

๋ชฉ๋ก ๋ณด๊ธฐ
14/18
post-thumbnail

Intro

ํ•˜๋‚˜์˜ view๋ฅผ ๊ฐ€์ •ํ•˜๊ณ  ๋งŒ๋“  youtube ๊ฒ€์ƒ‰ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


$vue create youtube
$npm i --save axios lodash



๐Ÿ”ต ใ„ฑใ„ฑ

์œ„์— ๊ทธ๋ ค๋†“์€ ๋ ˆ์ด์•„์›ƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐœ๋ฐœ์„ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
SearchBar, VideoList ์ปดํฌ๋„ŒํŠธ (VideoDetail์ปดํฌ๋„ŒํŠธ๋Š” ์ฐจํ›„์—)


๋ฐ์ดํ„ฐ ํ๋ฆ„ :
SearchBar โ†’ App /
App โ†โ†’ VideoList โ†โ†’ VideoListItem /
App โ†’ VideoDetail /

1. SearchBar ์ปดํฌ๋„ŒํŠธ

SearchBar โ†’ App

components/SearchBar.vue

<!--
  components/SearchBar.vue
-->
<template>
  <div>
    <input class="searchBar" type="text" @keypress.enter="searchVideo">
  </div>
</template>

<script>
import axios from 'axios'

const BASE_URL = 'https://www.googleapis.com/youtube/v3/search'
// #1
const API_KEY = process.env.VUE_APP_YOUTUBE_API_KEY

export default {
  name: 'SearchBar',
  data() {
    return {
      userInput:'',
    }
  },
  methods:{
    searchVideo(e) {
      this.userInput = e.target.value

      const config = {
        params: {
          part: 'snippet',
          key: API_KEY,
          q: this.userInput
        }
      }
      axios.get(BASE_URL,config)
      .then(res=>{
        console.log('๋ฐ์ดํ„ฐ ๋ณด๋‚ผ๊ฒŒ!')
        this.$emit('searched-videos',res.data.items)
      })
      .catch(err=>{
        console.log(err)
      })
    }
  }
}
</script>

<style scoped>
  .searchBar {
    width: 100%;
    padding: 0.5rem;
    font-size: 1.25rem;
  }
</style>

#1 : ์ด๊ฑธ ์œ„ํ•ด์„œ .env.loca ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ๋ทฐ์˜ ๋ณ€์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•˜์ž
โญ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ๋ณด๋‚ด๊ธฐ ์œ„ํ•ด์„  this.$emit('searched-videos',res.data.items)
: ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ searched-videos ๋ผ๋Š” ์ด๋ฒคํŠธ๊ฐ€ ์‹คํ–‰๋˜๋ฉด, SearchBar.vue ์ปดํฌ๋„ŒํŠธ์—์„œ res.data.items๋ฅผ ์ƒ์œ„์ปดํฌ๋„ŒํŠธ์—๊ฒŒ ๋ณด๋‚ด์ค„๊ฑฐ์•ผ!!โญ

App.vue

<template>
  <div id="app">
    <header>
      <h1>youtube</h1>
      <!--#3-->
      <SearchBar @searched-videos="searchedVideos"/>
    </header>
    <section>
    </section>
  </div>
</template>

<script>
// #1
import SearchBar from '@/components/SearchBar'

export default {
  name: 'App',
  components: {
  // #2
    SearchBar,
  },
  data() {
    return {
      // #4
      videoList:[],
    }
  },
  methods: {
    // #5
    searchedVideos(videoList) {
      this.videoList = videoList
    }
  }
}
</script>

<style>
header,section {
  width: 80%;
  margin: 0 auto;
  padding: 1rem 0;
}
section {
  display:flex;
}
</style>

#1 : SearchBar ์ปดํฌ๋„ŒํŠธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
#2 : SearchBar ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก
#3 : SearchBar ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

  • SearchBar ์ปดํฌ๋„ŒํŠธ์—์„œ ์ •์˜ํ•œ selected-videos ์ด๋ฒคํŠธ๋ฅผ ์ ๊ณ , ์ด๋Ÿฐ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒ์‹œ searchedVideos ํ•จ์ˆ˜ ์‹คํ–‰!

#4 : App.vue์—์„œ ์‚ฌ์šฉํ•  ๋น„๋””์˜ค ๋ฆฌ์ŠคํŠธ๋ฅผ ๋‹ด์„ ๋ณ€์ˆ˜
#5 : SearchBar์—์„œ ๋„˜์–ด์˜จ ๋ฐ์ดํ„ฐ๋ฅผ app.vue์— ์ €์žฅํ•˜์ž!

2. VideoList ์ปดํฌ๋„ŒํŠธ

App โ†’ VideoList

components/VideoList.vue

<!--
  components/VideoList.vue
-->
<template>
  <div class="videoList">
    <div
    v-for="video in videoList"
    :key="video.etag"
    class="videoListItem"
    >
      <img width=360 :src="thumbnailUrl(video)" alt="">
      <div>
        <div class="videoListItem__title">{{video.snippet.title | unescape}}</div>
        <div class="videoListItem__channelTitle">{{video.snippet.channelTitle}}</div>
        <div class="videoListItem__description">{{video.snippet.description}}</div>
      </div>
    </div>
  </div>
</template>

<script>
import _ from 'lodash'

export default {
  name:'VideoList',
  props: {
    videoList: Array,
  },
  methods: {
    thumbnailUrl(video) {
      return video.snippet.thumbnails.high.url
    },
  },
  filters: {
    unescape(value) {
      return _.unescape(value)
    }
  },
}
</script>

<style>
.videoList {
  width: 30%;
}
.videoListItem {
  display:flex;
}
.videoListItem__title {
  font-size:1.25rem;
  font-weight:bold;
}
</style>

โญApp์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด์„ 
props: {videoList: Array,} ๋ฅผ ํ†ตํ•ด ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ์ง€ ์ •์˜ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.โญ

App.vue

<template>
  <div id="app">
    <header>
      <h1>youtube</h1>
      <SearchBar @searched-videos="searchedVideos"/>
    </header>
    <section>
    <!-- #3 -->
      <VideoList :videoList="videoList"/>
    </section>
  </div>
</template>

<script>
import SearchBar from '@/components/SearchBar'
// #1
import VideoList from '@/components/VideoList'

export default {
  name: 'App',
  components: {
    SearchBar,
    // #2
    VideoList,
  },
  data() {
    return {
      videoList:[],
    }
  },
  methods: {
    searchedVideos(videoList) {
      this.videoList = videoList
    }
  }
}
</script>

<style>
header,section {
  width: 80%;
  margin: 0 auto;
  padding: 1rem 0;
}
section {
  display:flex;
}
</style>

#1 : VideoList ์ปดํฌ๋„ŒํŠธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
#2 : VideoList ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก
#3 : VideoList ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

  • :videoList๋Š” VideoList ์ปดํฌ๋„ŒํŠธ์—์„œ videoList๋ผ๋Š” ๋ณ€์ˆ˜๋ช…์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ๊ฒƒ์ด๋ผ๋Š” ์˜๋ฏธ ( VideoList์ปดํฌ๋„ŒํŠธ์— ๊ทธ๋ ‡๊ฒŒ ์ •์˜๋ฅผ ํ•ด๋†จ์Œ!)
  • ="videoList๋Š” App.vue์—์„œ ์“ฐ๊ณ  ์žˆ๋Š” videList ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด์ค„ ๊ฒƒ์ด๋ผ๋Š” ์˜๋ฏธ

๊ฒฐ๊ณผ

์•„์ง VideoList โ†’ VideoListItem ์œผ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๊ฒƒ์„ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๊ฑธ ํ•ด๋ณผ๊ฒŒ์š”

3. VideoListItem ์ปดํฌ๋„ŒํŠธ โญ

VideoListItem โ†โ†’ VideoList โ†โ†’ App

components/VideoListItem

<template>
  <!-- #4-1 -->
  <div @click="selectVideo" class="videoListItem">
   <!-- #3-1 -->
    <img :src="thumbnailUrl(video)" alt="">
    <div>
      <!-- #5-1 -->
      <div class="videoListItem__title">{{video.snippet.title | unescape}}</div>
      <div class="videoListItem__channelTitle">{{video.snippet.channelTitle}}</div>
      <div class="videoListItem__description">{{video.snippet.description}}</div>
    </div>
  </div>
</template>

<script>
import _ from 'lodash'

export default {
  // #1 
  name: 'VideoListItem',
  // #2
  props: {
    video:Object,
  },
  methods: {
    // #3
    thumbnailUrl(video) {
      return video.snippet.thumbnails.high.url
    },
    // #4
    selectVideo(){
      this.$emit('select-video',this.video)
    }
  },
  // #5
  filters: {
    unescape(v){
      return _.unescape(v)
    }
  }
}
</script>

<style scoped>
.videoListItem {
  /* display:flex; */
}
.videoListItem__title {
  font-size:1.25rem;
  font-weight:bold;
}
.videoListItem > img {
  width:100%;
}
</style>

#1 : ์ปดํฌ๋„ŒํŠธ ์ด๋ฆ„ ์ •์˜
#2 : VideoList์—์„œ ๋ฐ›์•„์˜ค๋Š” ๋ฐ์ดํ„ฐ props ์ •์˜
#3 : ์ธ๋„ค์ผurl ๋ฐ›์•„์˜ค๋Š” ํ•จ์ˆ˜ ์ •์˜ ๋ฐ ์‚ฌ์šฉ
#4 : ๋น„๋””์˜ค ์„ ํƒ์‹œ ๊ทธ ๋น„๋””์˜ค๋ฅผ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ(๋ถ€๋ชจ)๋กœ ๋ณด๋‚ด์ฃผ๊ธฐ! select-video๋ผ๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค!
#5 : ํ•„ํ„ฐ๋ฅผ ์ •์˜ํ•  ์ˆ˜๋„ ์žˆ๋„ค์š”!! ๋ณด๊ธฐ๋ถˆํŽธํ•œ๋‹จ์–ด๋Š” ์—†์• ์ฃผ๋Š” ๊ธฐ๋Šฅ! โญ

components/VideoList ์ˆ˜์ •

<template>
  <div class="videoList">
    <!-- #3 -->
    <VideoListItem
      v-for="video in videoList"
      :key="video.etag"
      :video="video"
      @select-video="selectVideo"
    />
  </div>
</template>

<script>
// #1
import VideoListItem from '@/components/VideoListItem'

export default {
  name:'VideoList',
  components: {
    // #2
    VideoListItem
  },
  props: {
    videoList: Array,
  },
  methods: {
    // #4
    selectVideo(video) {
      this.$emit('select-video',video)
    }
  },
}
</script>

<style>
.videoList {
  width: 30%;
}
</style>

#1 : VideoListItem ์ปดํฌ๋„ŒํŠธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
#2 : VideoListItem ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก
#3 : VideoListItem ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ
#4 : VideoListItem์—์„œ select-video ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ(๋ถ€๋ชจ)์—๊ฒŒ video ๋ณด๋‚ด์ฃผ๊ธฐ

App.vue

<template>
  <div id="app">
    <header>
      <h1>youtube</h1>
      <SearchBar @searched-videos="searchedVideos"/>
    </header>
    <section>
      <!-- #1 -->
      <VideoList :videoList="videoList" @select-video="selectVideo"/>
    </section>
  </div>
</template>

<script>
import SearchBar from '@/components/SearchBar'
import VideoList from '@/components/VideoList'


export default {
  name: 'App',
  components: {
    SearchBar,
    VideoList,
  },
  data() {
    return {
      videoList:[],
      // #1-1
      selectedVideo: {},
      isVideoSelected:false,
    }
  },
  methods: {
    searchedVideos(videoList) {
      this.videoList = videoList
    },
    // #1-2
    selectVideo(video){
      console.log('select๋น„๋””์˜ค ํ˜ธ์ถœ!')
      console.log(video)
      this.selectedVideo = video
      this.isVideoSelected = true
    },
  }
}
</script>

<style>
header,section {
  width: 80%;
  margin: 0 auto;
  padding: 1rem 0;
}
section {
  display:flex;
}
</style>

#1 : VideoList์ปดํฌ๋„ŒํŠธ์—์„œ select-video ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ selectVideo ํ•จ์ˆ˜ ์‹คํ–‰๋œ๋‹ค.
selectedVideo๋Š” ์„ ํƒ๋œ ๋น„๋””์˜ค๊ฐ์ฒด ์ €์žฅํ•  ๋ณ€์ˆ˜ / isVideoSelected๋Š” ์„ ํƒ๋œ ๋น„๋””์˜ค๊ฐ€ ์žˆ๋Š” ์ง€ ์—ฌ๋ถ€

4. VideoDetail ์ปดํฌ๋„ŒํŠธ

App โ†’ VideoDetail

components/VideoDetail

<template>
  <div class="videoDetail">
    <div class="videoContainer">
      <!-- #2 -->
      <iframe 
        :src="videoUrl" 
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen
      >
      </iframe>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VideoDetail',
  // #1
  props: {
    video:Object,
  },
  // #2
  computed: {
    videoUrl() {
      const videoId = this.video.id.videoId
      const url = `https://www.youtube.com/embed/${videoId}`
      return url
    }
  }
}
</script>

<style>
.videoDetail {
  width: 70%;
  padding-right: 1rem;
}
.videoContainer {
  position: relative;
  padding-top: 56.25%;
}
.videoContainer > iframe {
  position: absolute;
  top:0;
  width:100%;
  height: 100%;
}
</style>

#1 : ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ›์•„์˜ฌ ๋ฐ์ดํ„ฐ props:{video:Object}
#2 : ๋‚˜๋Š” videourl๋งŒ ํ•„์š”ํ•˜๋‹ˆ๊นŒ ๊ทธ๊ฑธ ๊ณ„์‚ฐํ•ด์„œ ์ œ 2์˜ ๋ฐ์ดํ„ฐ๋กœ ์“ฐ์ž! computed:{videoUrl(){...}}

App.vue

<template>
  <div id="app">
    <header>
      <h1>youtube</h1>
      <SearchBar @searched-videos="searchedVideos"/>
    </header>
    <section>
      <!-- #3 -->
      <VideoDetail v-if="isVideoSelected" :video="selectedVideo"/>
      <VideoList :videoList="videoList" @select-video="selectVideo"/>
    </section>
  </div>
</template>

<script>
import SearchBar from '@/components/SearchBar'
import VideoList from '@/components/VideoList'
// #1
import VideoDetail from '@/components/VideoDetail'


export default {
  name: 'App',
  components: {
    SearchBar,
    VideoList,
    // #2
    VideoDetail,
  },
  data() {
    return {
      videoList:[],
      selectedVideo: {},
      isVideoSelected:false,
    }
  },
  methods: {
    searchedVideos(videoList) {
      this.videoList = videoList
    },
    selectVideo(video){
      console.log('select๋น„๋””์˜ค ํ˜ธ์ถœ!')
      console.log(video)
      this.selectedVideo = video
      this.isVideoSelected = true
    },
  }
}
</script>

<style>
header,section {
  width: 80%;
  margin: 0 auto;
  padding: 1rem 0;
}
section {
  display:flex;
}
</style>

#1 : VideoDetail์ปดํฌ๋„ŒํŠธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
#2 : VideoDetail ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋กํ•˜๊ธฐ
#3 : VideoDetail ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ ์ด๋•Œ ์ด ์ปดํฌ๋„ŒํŠธ๋Š” isVideoSelected๊ฐ€ ์ฐธ์ผ ๋•Œ๋งŒ ์‚ฌ์šฉํ•จ. ๊ทธ๋ฆฌ๊ณ  VideoDetail์ปดํฌ๋„ŒํŠธ๋กœ selectedVideo๋ฅผ ๋ณด๋‚ด์ค„๊ฑฐ์•ผ!

๊ฒฐ๊ณผ

  • ๋น„๋””์˜ค ์„ ํƒ์‹œ

  • vue devtools (google extension)

profile
๊ธฐ๋กํ•˜๋Š” ์Šต๊ด€์€ ์‰ฝ๊ฒŒ ๋ฌด๋„ˆ์ง€์ง€ ์•Š์•„์š”.

0๊ฐœ์˜ ๋Œ“๊ธ€