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
- 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()
},
]
},
})
- 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
},
]
- 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>
![](https://blog.kakaocdn.net/dn/lZF35/btrQNJ9j6xN/mTqkk6HOcikq9fXtFKwBNK/img.png)
- ArticleItem ์ปดํฌ๋ํธ ์์ฑ
// components/ArticleItem.vue
<template>
<div>
</div>
</template>
<script>
export default {
name: 'ArticleItem',
}
</script>
- 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>
- 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>
- ๊ฒฐ๊ณผ ํ์ธ
Create
- 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
},
]
- 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>
- submit ์ด๋ฒคํธ ๋์ ์ทจ์ํ๊ธฐ
// views/CreateView.vue
<template>
<div>
<h1>๊ฒ์๊ธ ์์ฑ</h1>
<form @submit.prevent="createArticle">
...
</form>
</div>
</template>
โ v-on: {event}.prevent
ํ์ฉํ์ฌ submit ์ด๋ฒคํธ ๋์ ์ทจ์
- 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>
- 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)
}
},
- mutations์์๋ ์ ๋ฌ๋ article ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ๊ฒ์๊ธ ์์ฑ
mutations: {
CREATE_ARTICLE(state, article) {
state.articles.push(article)
state.article_id = state.article_id + 1
}
},
- ๋ค๋ก๊ฐ๊ธฐ ๋งํฌ ์ถ๊ฐ
// views/CreateView.vue
<template>
<div>
<h1>๊ฒ์๊ธ ์์ฑ</h1>
...
<router-link :to="{ name: 'index' }">๋ค๋ก๊ฐ๊ธฐ</router-link>
</div>
</template>
- ๊ฒ์๊ธ ์์ฑ ํ index ํ์ด์ง๋ก ์ด๋ํ๋๋ก ํ๋ ๋ค๋น๊ฒ์ดํฐ ์์ฑ
// views/CreateView.vue
methods: {
createArticle() {
...
this.$store.dispatch('createArticle', payload)
this.$router.push({ name: 'index' })
}
}
- 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
- 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
},
]
- article ์ ์ ๋ฐ state์์ articles ๊ฐ์ ธ์ค๊ธฐ
// views/DetailView.vue
<script>
export default {
name: "DetailView",
data() {
return {
article: null
}
},
computed: {
articles() {
return this.$store.state.articles;
},
},
}
</script>
- 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์ด๋ฏ๋ก ํ๋ณํ์ ํตํด์ ๋น๊ตํด์ค๋ค.
- 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>
- 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
![](https://blog.kakaocdn.net/dn/bjwvKV/btrQOOn9lEJ/Phi0k98gedC3AkZEUrDyi1/img.png)
โ Optional Chaining(?.
) ์์ ํ๊ฐ ๋์์ด undefined๋ null ์ด๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์๊ณ undefined๋ฅผ ๋ฐํํ๋ค.
- 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>
- ๋ก์ปฌ ์๊ฐ์ผ๋ก ๋ณํํด์ฃผ๋ 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>
- DetailView ์ปดํฌ๋ํธ์ ๋ค๋ก๊ฐ๊ธฐ ๋งํฌ ์ถ๊ฐ
// views/DetailView.vue
<template>
<div>
...
<router-link :to="{ name:index }">๋ค๋ก๊ฐ๊ธฐ</router-link>
</div>
</template>
- ๊ฐ ๊ฒ์๊ธ์ ํด๋ฆญํ๋ฉด 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
- 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>
- mutations์์ id์ ํด๋นํ๋ ๊ฒ์๊ธ ์ง์ฐ๊ธฐ
// store/index.js
...
mutations: {
...
DELETE_ARTICLE(state, id) {
state.articles = state.articles.filter((article) => {
return !(article.id === id)
})
}
},
...
- ์ญ์ ํ 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
- 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๋ฒ์งธ ๊ฒ์๊ธ๊ณผ ํผ๋๋ ์ ์๋ค.
- 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>
- ์์ฒญํ ๋ฆฌ์์ค๊ฐ ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ ์๋ 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 |
๋๊ธ