๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
โญ Personal_Study/Vue

Article with Vue

by ํฌ์ŠคํŠธ์‰์ดํฌ 2022. 11. 19.

Articles with Vue

์‚ฌ์ „ ์ค€๋น„

$ vue create articles
$ cd articles
$ vue add vuex
$ vue add router
// App.vue

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

Index

  1. state ์ •์˜
// store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    article_id: 3,
    articles: [
      {
        id: 1,
        title: 'title1',
        content: 'content1',
        createdAt: new Date().getTime()
      },
      {
        id: 2,
        title: 'title2',
        content: 'content2',
        createdAt: new Date().getTime()
      },
    ]
  },
})
  1. IndexView ์ปดํฌ๋„ŒํŠธ ๋ฐ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ
// views/IndexView.vue

<template>
  <div>
    <h1>Articles</h1>
  </div>
</template>

<script>
export default {
  name: 'IndexView'
}
</script>

<style>

</style>
// router/index.js

import IndexView from '../views/IndexView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'IndexView',
    component: IndexView
  },
]  
  1. state ์—์„œ ๋ถˆ๋Ÿฌ์˜จ articles ์ถœ๋ ฅ
// views/IndexView.vue

<template>
  <div>
    <h1>Articles</h1>
    {{ articles }}
  </div>
</template>

<script>
export default {
  name: 'IndexView',
  computed: {
    articles() {
      return this.$store.state.articles
    }
  },
}
</script>
  1. ArticleItem ์ปดํฌ๋„ŒํŠธ ์ž‘์„ฑ
// components/ArticleItem.vue

<template>
  <div>
  </div>
</template>

<script>
export default {
  name: 'ArticleItem',
}
</script>
  1. IndexView ์ปดํฌ๋„ŒํŠธ์—์„œ ArticleItem ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก ๋ฐ‘ props ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
// views/IndexView.vue

<template>
  <div>
    <h1>Articles</h1>
    <ArticleItem 
      v-for="article in articles"
      :key="article.id"
      :article=article
    />
  </div>
</template>

<script>
import ArticleItem from '../components/ArticleItem.vue'

export default {
  name: 'IndexView',
  components: {
    ArticleItem
  },
  computed: {
    articles() {
      return this.$store.state.articles
    }
  },
}
</script>
  1. props ๋ฐ์ดํ„ฐ ์„ ์–ธ ๋ฐ ๊ฒŒ์‹œ๊ธ€ ์ถœ๋ ฅ
// components/ArticleItem.vue

<template>
  <div>
    <p>๊ธ€ ๋ฒˆํ˜ธ: {{ article.id }}</p>
    <p>๊ธ€ ์ œ๋ชฉ: {{ article.title }}</p>
    <hr>
  </div>
</template>

<script>
export default {
  name: 'ArticleItem',
  props: {
    article: Object
  }
}
</script>
  1. ๊ฒฐ๊ณผ ํ™•์ธ

Create

  1. CreateView ์ปดํฌ๋„ŒํŠธ ๋ฐ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ
// views/CreateView.vue

<template>
  <div>
    <h1>๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ</h1>
  </div>
</template>

<script>
export default {
  name: "CreateView"
}
</script>
// router/index.js

const routes = [
  ...
  {
    path: '/create',
    name: 'create',
    component: CreateView
  },
]
  1. Form ์ƒ์„ฑ ๋ฐ ๋ฐ์ดํ„ฐ ์ •์˜
<template>
  <div>
    <h1>๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ</h1>
    <form>
      <label for="title">์ œ๋ชฉ: </label>
      <input id="title" type="text" v-model="title"> 
      <br>
      <label for="content">๋‚ด์šฉ: </label>
      <textarea
        id="content" cols="30" rows="10"
        v-model="content"
      ></textarea>
      <input type="submit">
    </form>
  </div>
</template>

<script>
export default {
  name: "CreateView",
  data() {
    return {
      title: null,
      content: null,
    }
  },
}
</script>
  1. submit ์ด๋ฒคํŠธ ๋™์ž‘ ์ทจ์†Œํ•˜๊ธฐ
// views/CreateView.vue

<template>
  <div>
    <h1>๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ</h1>
    <form @submit.prevent="createArticle">
    ...
    </form>
  </div>
</template>

โœ” v-on: {event}.prevent ํ™œ์šฉํ•˜์—ฌ submit ์ด๋ฒคํŠธ ๋™์ž‘ ์ทจ์†Œ

  1. actions์— ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ธธ ๋•Œ ํ•˜๋‚˜์˜ object๋กœ ๋งŒ๋“ค์–ด์„œ ์ „๋‹ฌ
// views/CreateView.vue

<script>
export default {
  name: "CreateView",
  data() {
    return {
      title: null,
      content: null,
    }
  },
  methods: {
    createArticle() {
      const title = this.title
      const content = this.content
      const payload = {
        title, content
      }
      this.$store.dispatch('createArticle', payload)
    }
  }
}
</script>
  1. actions์—์„œ ๋„˜์–ด์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•˜์—ฌ article ์ƒ์„ฑ ํ›„ mutations ํ˜ธ์ถœ
// store/index.js

actions: {
    createArticle(context, payload) {
      const article = {
        id: context.state.article_id,
        title: payload.title,
        content: payload.content,
        createdAt: new Date().getTime()
      }
      context.commit('CREATE_ARTICLE', article)
    }
  },
  1. mutations์—์„œ๋Š” ์ „๋‹ฌ๋œ article ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
  mutations: {
    CREATE_ARTICLE(state, article) {
      state.articles.push(article)
      state.article_id = state.article_id + 1
    }
  },
  1. ๋’ค๋กœ๊ฐ€๊ธฐ ๋งํฌ ์ถ”๊ฐ€
// views/CreateView.vue

<template>
  <div>
    <h1>๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ</h1>
    ...
    <router-link :to="{ name: 'index' }">๋’ค๋กœ๊ฐ€๊ธฐ</router-link>
  </div>
</template>
  1. ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ํ›„ index ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋„๋ก ํ•˜๋Š” ๋„ค๋น„๊ฒŒ์ดํ„ฐ ์ž‘์„ฑ
// views/CreateView.vue

  methods: {
    createArticle() {
      ...
      this.$store.dispatch('createArticle', payload)
      this.$router.push({ name: 'index' })
    }
  }
  1. IndexView ์ปดํฌ๋„ŒํŠธ์— ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋งํฌ ์ถ”๊ฐ€
// views/IndexView.vue

<template>
  <div>
    <h1>Articles</h1>
    <router-link :to="{name:'create'}">๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ</router-link>
    <ArticleItem 
      v-for="article in articles"
      :key="article.id"
      :article=article
    />
  </div>
</template>

Detail

  1. DetailView ์ปดํฌ๋„ŒํŠธ ๋ฐ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ
// views/DetailView.vue

<template>
  <div>
    <h1>Detail</h1>
  </div>
</template>

<script>
export default {
  name: "DetailView",
};
</script>
// router/index.js
import DetailView from '../views/DetailView.vue'

...

const routes = [
  ...
  {
    path: '/:id',
    name: 'detail',
    component: DetailView
  },
]
  1. article ์ •์˜ ๋ฐ state์—์„œ articles ๊ฐ€์ ธ์˜ค๊ธฐ
// views/DetailView.vue

<script>
export default {
  name: "DetailView",
  data() {
    return {
      article: null
      }
  },
  computed: {
    articles() {
      return this.$store.state.articles;
    },
  },
}
</script>
  1. articles์—์„œ ๋™์ ์ธ์ž๋ฅผ ํ†ตํ•ด ๋ฐ›์€ id์— ํ•ด๋‹นํ•˜๋Š” article ๊ฐ€์ ธ์˜ค๊ธฐ
// views/DetailView.vue

<script>
  ...
  methods: {
    getArticleById() {
      const id = this.$route.params.id;
      for (const article of this.articles) {
        // url์€ ๋ฌธ์ž๋กœ ๋“ค์–ด์˜ค๋ฏ€๋กœ ํ˜•๋ณ€ํ™˜ํ•ด์„œ ๋น„๊ต
        if (article.id === Number(id)) {
          this.article = article;
          break;
        }
      }
    },
  },
</script>

โœ” ๋™์  ์ธ์ž๋ฅผ ํ†ตํ•ด ๋ฐ›์€ id๋Š” str์ด๋ฏ€๋กœ ํ˜•๋ณ€ํ™˜์„ ํ†ตํ•ด์„œ ๋น„๊ตํ•ด์ค€๋‹ค.

  1. article ์ถœ๋ ฅ
// views/DetailView.vue

<template>
  <div>
    <h1>Detail</h1>
    <p>๊ธ€ ๋ฒˆํ˜ธ: {{ article.id }}</p>
    <p>์ œ๋ชฉ: {{ article.title }}</p>
    <p>๋‚ด์šฉ: {{ article.content }}</p>
    <p>์ž‘์„ฑ์‹œ๊ฐ„: {{ article.createdAt }}</p>
  </div>
</template>
  1. created lifecycle hook์„ ํ†ตํ•ด ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์„ ๋•Œ article์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ
// views/DetailView.vue

<script>
...
  methods: {
    getArticleById() {
      const id = this.$route.params.id;
      for (const article of this.articles) {
        // url์€ ๋ฌธ์ž๋กœ ๋“ค์–ด์˜ค๋ฏ€๋กœ ํ˜•๋ณ€ํ™˜ํ•ด์„œ ๋น„๊ต
        if (article.id === Number(id)) {
          this.article = article;
          break;
        }
      }
    },
  },
  created() {
    this.getArticleById(this.$route.params.id)
  },
</script>

[์ฐธ๊ณ ] Optional Chaining

โœ” Optional Chaining(?.) ์•ž์˜ ํ‰๊ฐ€ ๋Œ€์ƒ์ด undefined๋‚˜ null ์ด๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ณ  undefined๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

  1. optional chaining(?.)์„ ํ†ตํ•ด article ๊ฐ์ฒด๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์ถœ๋ ฅ๋˜๋„๋ก ์ˆ˜์ •
// views/DetailView.vue

<template>
  <div>
    <h1>Detail</h1>
    <p>๊ธ€ ๋ฒˆํ˜ธ: {{ article?.id }}</p>
    <p>์ œ๋ชฉ: {{ article.title }}</p>
    <p>๋‚ด์šฉ: {{ article.content }}</p>
    <p>์ž‘์„ฑ์‹œ๊ฐ„: {{ article.createdAt }}</p>
  </div>
</template>
  1. ๋กœ์ปฌ ์‹œ๊ฐ„์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” computed ๊ฐ’ ์ž‘์„ฑ ๋ฐ ์ถœ๋ ฅ
// views/DetailView.vue

<script>
export default {
  ...
  computed: {
    ...
    articleCreatedAt() {
      const article = this.article
      const createdAt = new Date(article?.createdAt).toLocaleString()
      return createdAt
    }
  },
}
// views/DetailView.vue

<template>
  <div>
    <h1>Detail</h1>
    <p>๊ธ€ ๋ฒˆํ˜ธ: {{ article?.id }}</p>
    <p>์ œ๋ชฉ: {{ article.title }}</p>
    <p>๋‚ด์šฉ: {{ article.content }}</p>
    <!-- <p>์ž‘์„ฑ์‹œ๊ฐ„: {{ article.createdAt }}</p> -->
    <p>์ž‘์„ฑ์‹œ๊ฐ„: {{ articleCreatedAt }}</p>
  </div>
</template>
  1. DetailView ์ปดํฌ๋„ŒํŠธ์— ๋’ค๋กœ๊ฐ€๊ธฐ ๋งํฌ ์ถ”๊ฐ€
// views/DetailView.vue

<template>
  <div>
  ...
    <router-link :to="{ name:index }">๋’ค๋กœ๊ฐ€๊ธฐ</router-link>
  </div>
</template>
  1. ๊ฐ ๊ฒŒ์‹œ๊ธ€์„ ํด๋ฆญํ•˜๋ฉด detail ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋„๋ก ArticleItem์— ์ด๋ฒคํŠธ ์ถ”๊ฐ€
// components/ArticleItem.vue

<template>
  <div @click="goDetail(article.id)">
    <p>๊ธ€ ๋ฒˆํ˜ธ: {{ article.id }}</p>
    <p>๊ธ€ ์ œ๋ชฉ: {{ article.title }}</p>
    <hr />
  </div>
</template>

<script>
export default {
  name: "ArticleItem",
  props: {
    article: Object,
  },
  methods: {
    goDetail(id) {
      this.$router.push({ name:'detail', params:{id} })
    }
  },
};
</script>

Delete

  1. DetailView ์ปดํฌ๋„ŒํŠธ์— ์‚ญ์ œ ๋ฒ„ํŠผ ๋งŒ๋“ค๊ณ , mutations ํ˜ธ์ถœ
// views.DetailView.vue

<template>
  <div>
  ...
    <button @click="deleteArticle">์‚ญ์ œ</button>
    <br />
    <router-link :to="{ name: index }">๋’ค๋กœ๊ฐ€๊ธฐ</router-link>
  </div>
</template>

<script>
export default {
  ...
  methods: {
    ...
    deleteArticle() {
      this.$store.commit('DELETE_ARTICLE', this.article.id)
    }
  },
  ...
};
</script>
  1. mutations์—์„œ id์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€ ์ง€์šฐ๊ธฐ
// store/index.js

...
  mutations: {
    ...
    DELETE_ARTICLE(state, id) {
      state.articles = state.articles.filter((article) => {
        return !(article.id === id)
      })
    }
  },
...
  1. ์‚ญ์ œ ํ›„ index ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋„๋ก ๋„ค๋น„๊ฒŒ์ด์…˜ ์ž‘์„ฑ
// views/DetailView.vue

<script>
export default {
  ...
  methods: {
    ...
    deleteArticle() {
      this.$store.commit('DELETE_ARTICLE', this.article.id)
      this.$router.push({ name:'index' })
    }
  },
  ...
};
</script>

404 Not Found

  1. NotFound404 ์ปดํฌ๋„ŒํŠธ ๋ฐ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ
// views/NotFound404.vue

<template>
  <div>
    <h1>404 Not Found</h1>
  </div>
</template>

<script>
export default {
  name: 'NotFound404'
}
</script>
// router/index.js

const routes = [
  ...
  {
    path: '/404-not-found',
    name: 'NotFound404',
    component: NotFound404
  },
  {
    path: '/:id',
    name: 'detail',
    component: DetailView
  },
]

โœ” Detail์— ๋Œ€ํ•œ router๋ณด๋‹ค ์œ„์— ๋“ฑ๋กํ•ด์ค˜์•ผ ํ•œ๋‹ค.
โœ” /404๋กœ ๋“ฑ๋กํ•˜๋ฉด 404๋ฒˆ์งธ ๊ฒŒ์‹œ๊ธ€๊ณผ ํ˜ผ๋™๋  ์ˆ˜ ์žˆ๋‹ค.

  1. DetailView ์ปดํฌ๋„ŒํŠธ์— id์— ํ•ด๋‹นํ•˜๋Š” article์ด ์—†์œผ๋ฉด 404 ํŽ˜์ด์ง€๋กœ ์ด๋™
// views/DetailView.vue

<script>
export default {
  ...
  methods: {
    getArticleById() {
      const id = this.$route.params.id;
      for (const article of this.articles) {
        // url์€ ๋ฌธ์ž๋กœ ๋“ค์–ด์˜ค๋ฏ€๋กœ ํ˜•๋ณ€ํ™˜ํ•ด์„œ ๋น„๊ต
        if (article.id === Number(id)) {
          this.article = article;
          break;
        }
        if (!this.article) {
          this.$router.push({ name:'NotFound404' })
        }
      }
    },
    ...
  },
  ...
}
</script>
  1. ์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์—†๋Š” id๊ฐ€ ์•„๋‹Œ ์ „ํ˜€ ๋‹ค๋ฅธ ์š”์ฒญ์— ๋Œ€๋น„ํ•˜์—ฌ 404page๋กœ redirect ์‹œํ‚ค๊ธฐ
// router/index.js

const routes = [
  ...
  {
    path: '*',
    redirect: { name: 'NotFound404' }
  }
]

โœ” ์ตœํ•˜๋‹จ์— ์ž‘์„ฑ
โœ” $router.push์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ name์„ ์ด์šฉํ•˜์—ฌ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋‹ค.

'โญ Personal_Study > Vue' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

Cross - Origin Resource Sharing (CORS)  (0) 2022.11.20
Vue with DRF: Server & Client  (0) 2022.11.20
Navigation Guard  (0) 2022.11.19
Vue Router  (0) 2022.11.18
UX & UI  (0) 2022.11.18

๋Œ“๊ธ€