9 리팩토링 1 - 리스트 아이템 컴포넌트 공통화
source: categories/study/vue-beginner-lv3/vue-beginner-lv3_9-00.md
9.1 컴포넌트 공통화 리팩토링 소개
리스트 공통 컴포넌트화 진행 예정
9.2 [실습 안내] 뉴스 리스트 스타일링
src/App.vue
<template>
<div id="app">
<tool-bar></tool-bar>
<transition name="page">
<router-view></router-view>
</transition>
</div>
</template>
<script>
import ToolBar from "./components/ToolBar";
export default {
name: 'App',
components: {
ToolBar,
},
}
</script>
<style>
body {
padding: 0;
margin: 0;
}
a {
color: #34495e;
text-decoration: none;
}
a.router-link-exact-active {
text-decoration: underline;
}
a:hover {
color: #42b883;
text-decoration: underline;
}
/* Router Transition */
.page-enter-active, .page-leave-active {
transition: opacity .5s;
}
.page-enter, .page-leave-to /* .page-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>
src/views/NewsView.vue
<template>
<div>
<ul class="news-list">
<li v-for="(item, index) in fetchedNews" v-bind:key="index" class="post">
<!-- 포인트 영역 -->
<div class="points">
{{item.points}}
</div>
<!-- 기타 정보 영역 -->
<div>
<p class="news-title">
<a v-bind:href="item.url">{{item.title}}</a>
</p>
<small class="link-text">
{{item.time_ago} by
<router-link :to="`/user/${item.user}`" class="link-text">{{item.user}}</router-link>
</small>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapGetters, mapActions} from "vuex";
export default {
name: "NewsView",
computed: {
...mapGetters(['fetchedNews']),
},
methods: {
...mapActions(['FETCH_NEWS']),
},
created() {
this.FETCH_NEWS();
}
}
</script>
<style scoped>
.news-list {
margin: 0;
padding: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
}
.points {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 60px;
color: #42b883;
}
.news-title {
margin: 0;
}
.link-text {
color: #828282;
}
</style>
9.3 [실습] 질문, 구직 리스트 스타일링
src/views/AskView.vue
<template>
<div>
<ul class="news-list">
<li v-for="(item, index) in fetchedAsk" v-bind:key="index" class="post">
<!-- 포인트 영역 -->
<div class="points">
{{item.points}}
</div>
<!-- 기타 정보 영역 -->
<div>
<p class="news-title">
<router-link :to="`/item/${item.id}`">
{{item.title}}
</router-link>
</p>
<small class="link-text">
{{item.time_ago}} by
<router-link :to="`/user/${item.user}`" class="link-text">{{item.user}}</router-link>
</small>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapGetters, mapActions} from 'vuex';
export default {
name: "AskView",
computed: {
// # 3.
...mapGetters(['fetchedAsk']),
// 이름을 바꾸고싶다면
// ...mapGetters({
// ask: 'fetchedAsk',
// })
// # 2.
// ...mapState(['ask'])
// # 1.
// ask() {
// return this.$store.state.ask;
// }
},
methods: {
...mapActions(['FETCH_ASK']),
},
created() {
this.FETCH_ASK();
}
}
</script>
<style scoped>
.news-list {
margin: 0;
padding: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
}
.points {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 60px;
color: #42b883;
}
.news-title {
margin: 0;
}
.link-text {
color: #828282;
}
</style>
src/views/JobsView.vue
<template>
<div>
<ul class="news-list">
<li v-for="(item, index) in fetchedJobs" v-bind:key="index" class="post">
<!-- 포인트 영역 -->
<div class="points">
{{item.points || 0}}
</div>
<!-- 기타 정보 영역 -->
<div>
<p class="news-title">
<a :href="item.url">{{item.title}}</a>
</p>
<small class="link-text">
{{item.time_ago}} by
<a :href="item.url">{{item.domain}}</a>
</small>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapGetters, mapActions} from "vuex";
export default {
name: "JobsView",
computed: {
...mapGetters(['fetchedJobs']),
},
methods: {
...mapActions(['FETCH_JOBS']),
},
created() {
this.FETCH_JOBS();
}
}
</script>
<style scoped>
.news-list {
margin: 0;
padding: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
}
.points {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 60px;
color: #42b883;
}
.news-title {
margin: 0;
}
.link-text {
color: #828282;
}
</style>
9.4 [실습 안내] 공통 컴포넌트 ListItem 제작 및 실습 안내
src/views/NewsView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
export default {
name: "NewsView",
components: {
ListItem,
}
}
</script>
<style scoped>
</style>
src/components/ListItem.vue
<template>
<div>
<ul class="news-list">
<li v-for="(item, index) in fetchedNews" v-bind:key="index" class="post">
<!-- 포인트 영역 -->
<div class="points">
{{item.points}}
</div>
<!-- 기타 정보 영역 -->
<div>
<p class="news-title">
<a v-bind:href="item.url">{{item.title}}</a>
</p>
<small class="link-text">
{{item.time_ago}} by
<router-link :to="`/user/${item.user}`" class="link-text">{{item.user}}</router-link>
</small>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapActions, mapGetters} from "vuex";
export default {
name: "ListItem",
computed: {
...mapGetters(['fetchedNews']),
},
methods: {
...mapActions(['FETCH_NEWS']),
},
created() {
this.FETCH_NEWS();
// this.$store.dispatch('FETCH_NEWS');
// fetchNewsList()
// .then(response => this.users = response.data)
// .catch(error => console.log(error))
}
}
</script>
<style scoped>
.news-list {
margin: 0;
padding: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
}
.points {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 60px;
color: #42b883;
}
.news-title {
margin: 0;
}
.link-text {
color: #828282;
}
</style>
ListItem.vue 공통 컴포넌트화 - 내가푼답 - slot 사용
src/components/ListItem.vue
<template>
<div>
<ul class="news-list">
<li v-for="(item, index) in fetchGetter" v-bind:key="index" class="post">
<!-- 포인트 영역 -->
<div class="points">
{{item.points || 0}}
</div>
<!-- 기타 정보 영역 -->
<div>
<p class="news-title">
<slot name="title" :item="item"></slot>
</p>
<small class="link-text">
{{item.time_ago}} by
<slot name="made" :item="item"></slot>
</small>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "ListItem",
props: ['fetchGetter', 'fetchAction'],
created() {
this.fetchAction();
// this.$store.dispatch('FETCH_NEWS');
// fetchNewsList()
// .then(response => this.users = response.data)
// .catch(error => console.log(error))
}
}
</script>
<style scoped>
.news-list {
margin: 0;
padding: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
}
.points {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 60px;
color: #42b883;
}
.news-title {
margin: 0;
}
.link-text {
color: #828282;
}
</style>
src/views/NewsView.vue
<template>
<div>
<list-item
:fetchAction="FETCH_NEWS"
:fetchGetter="fetchedNews">
<a slot="title" slot-scope="props" :href="props.item.url">{{props.item.title}}</a>
<router-link
slot="made"
slot-scope="props"
:to="`/user/${props.item.user}`"
class="link-text"
>
{{props.item.user}}
</router-link>
</list-item>
</div>
</template>
<script>
import {mapActions, mapGetters} from "vuex";
import ListItem from "../components/ListItem";
export default {
name: "NewsView",
computed: {
...mapGetters(['fetchedNews']),
},
methods: {
...mapActions(['FETCH_NEWS']),
},
components: {
ListItem,
}
}
</script>
<style scoped>
</style>
src/views/AskView.vue
<template>
<div>
<list-item
:fetchGetter="fetchedAsk"
:fetchAction="FETCH_ASK"
>
<router-link
slot="title"
slot-scope="props"
:to="`/item/${props.item.id}`"
>
{{props.item.title}}
</router-link>
<router-link
slot="made"
slot-scope="props"
:to="`/user/${props.item.user}`"
class="link-text"
>
{{props.item.user}}
</router-link>
</list-item>
</div>
</template>
<script>
import {mapGetters, mapActions} from 'vuex';
import ListItem from "../components/ListItem";
export default {
name: "AskView",
computed: {
...mapGetters(['fetchedAsk']),
},
methods: {
...mapActions(['FETCH_ASK']),
},
components: {
ListItem,
}
}
</script>
<style scoped>
</style>
src/views/JobsView.vue
<template>
<div>
<list-item
:fetchGetter="fetchedJobs"
:fetchAction="FETCH_JOBS"
>
<a slot="title" slot-scope="props" :href="props.item.url">{{props.item.title}}</a>
<a slot="made" slot-scope="props" :href="props.item.url">{{props.item.domain}}</a>
</list-item>
</div>
</template>
<script>
import {mapGetters, mapActions} from "vuex";
import ListItem from "../components/ListItem";
export default {
name: "JobsView",
computed: {
...mapGetters(['fetchedJobs']),
},
methods: {
...mapActions(['FETCH_JOBS']),
},
components: {
ListItem,
}
}
</script>
<style scoped>
</style>
9.5 [실습] 공통 컴포넌트 구현(1) - 페이지별 데이터 분기
…skip… 다음 내용~!
9.6 공통 컴포넌트 구현(2) - computed 속성
내 고민.. 내가 푼 답을 보면 나는 AskView.vue
, NewsView.vue
, JobsView.vue
컴포넌트에서 ListItem.vue
컴포넌트로 각각 actions
메소드와 getters
함수들을 props
로 내려주도록 정리했다.
그렇게 전달받은 props
로 ListItem.vue
에서 받아서 실행하고 데이터를 조작하도록 하였다.
그런데 지금 이번 강의에서 강사님은 AskView.vue
, NewsView.vue
, JobsView.vue
컴포넌트를 거치지않고ListItem.vue
에서 모든 actions
와 getters
함수들을 가져다쓰도록 작성했다.
흠.. 이게 어찌보면 코드 파악에는 더 쉬워보이긴 하는데..
경우의 수가 늘어날 수록 else if
문이 늘어나서 코드가 길어지면 지저분해보일거 같은데..
어떤게 더 나은 방법일까?
어렵다. 더 강의 들으면서 생각해봐야겠다.
아 그리고 추가로 현재 강의내용대로하면 지금 AskView.vue
, NewsView.vue
, JobsView.vue
컴포넌트에서 ListItem
컴포넌트의 내용과 100% 일치하지 않은 상태였거든?
그런 미묘한 차이는 어떻게 잡아내는거지?v-if
써서?
근데 그것또한 복잡할거같은데..
여튼 그냥 미세한 차이 무시하고 지금까지는 일괄 처리했네..
아아 이번 강의 마지막 부분에서 이 문제 짚고넘어가시는군. JobsView
에서 미세하게 다른 부분을 과연 어떻게 처리할까?
9.7 공통 컴포넌트 구현(3) - template 속성과 v-if 디렉티브 활용 1
아파 v-if
속성 활용하는거 맞네..
그런데 template
속성은 어떤걸 활용하는걸까?
9.8 공통 컴포넌트 구현(4) - template 속성과 v-if 디렉티브 활용 2
라우터에 name 속성 추가
src/routes/modules/news.js
import NewsView from "../../views/NewsView";
const newsRouter = [
{
// path: url 주소
path: '/news',
// component: url 주소로 갔을 때 표시될 컴포넌트 ( 페이지단위라고 보면됨, 예를 들어서 MainPage 같은 페이지단위 컴포넌트 )
name: 'news',
component: NewsView,
},
]
export default newsRouter
src/routes/modules/jobs.js
import JobsView from "../../views/JobsView";
const jobsRouter = [
{
path: '/jobs',
name: 'jobs',
component: JobsView,
},
]
export default jobsRouter
src/routes/modules/ask.js
import AskView from "../../views/AskView";
const askRouter = [
{
path: '/ask',
name: 'ask',
component: AskView,
},
]
export default askRouter
ListView 컴포넌트, template 태그 활용 v-if 분기 처리
src/components/ListItem.vue
<template>
<div>
<ul class="news-list">
<li v-for="(item, index) in listItems" v-bind:key="index" class="post">
<!-- 포인트 영역 -->
<div class="points">
{{item.points || 0}}
</div>
<!-- 기타 정보 영역 -->
<div>
<!-- 타이틀 영역 -->
<p class="news-title">
<!-- template이라는 가상 태그 활용 -->
<template v-if="item.domain">
<a :href="item.url">
{{item.title}}
</a>
</template>
<template v-else>
<router-link :to="`/item/${item.id}`">
{{item.title}}
</router-link>
</template>
</p>
<small class="link-text">
{{item.time_ago}} by
<router-link
v-if="item.user"
:to="`/user/${item.user}`" class="link-text">{{item.user}}</router-link>
<a :href="item.url" v-else>
{{item.domain}}
</a>
</small>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "ListItem",
computed: {
listItems() {
const name = this.$route.name;
if (name === 'news') {
// this.$store.state 이렇게 state 값이 바로 접근이되지 않는다.
// 내가 뭘 잘못 건드렸나? 아니면 getters로만 state 값에 접근 가능하도록 바뀌었나?
// 아 지금 store/modules에 다 따로 설정해놔서 그렇구나.
// modules 속성 사용 안하고 state, getters, mutations, actions를 다 store/index.js에 선언했으면
// this.$store.state.news로 접근 가능함
return this.$store.getters.fetchedNews;
} else if (name === 'ask') {
return this.$store.getters.fetchedAsk;
} else if (name === 'jobs') {
return this.$store.getters.fetchedJobs;
}
// 강의와는 다르게 기본으로 반환되는 값이 필요하다.
// 없으면 위 if 조건에 맞는게 없을 때엔 아무값도 반환하지 않으니까 그런 경우를 대비하여 에러로 잡는다.
return [];
}
},
created() {
const name = this.$route.name;
if (name === 'news') {
this.$store.dispatch('FETCH_NEWS');
} else if (name === 'ask') {
this.$store.dispatch('FETCH_ASK');
} else if (name === 'jobs') {
this.$store.dispatch('FETCH_JOBS');
}
}
}
</script>
<style scoped>
.news-list {
margin: 0;
padding: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
}
.points {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 60px;
color: #42b883;
}
.news-title {
margin: 0;
}
.link-text {
color: #828282;
}
</style>
페이지 컴포넌트 정리
src/views/AskView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
export default {
name: "AskView",
components: {
ListItem,
}
}
</script>
<style scoped>
</style>
src/views/JobsView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
export default {
name: "JobsView",
components: {
ListItem,
}
}
</script>
<style scoped>
</style>
src/views/NewsView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
export default {
name: "NewsView",
components: {
ListItem,
}
}
</script>
<style scoped>
</style>
흐음.. 강사님의 이런 정리가 더 좋을거 같긴하다.
나처럼 속성 흐름을 여러군데 거쳐서 내려가게하면 흐름 파악이 어려워질테니….vuex
의 store
같은 맥락으로 한 곳에 몰아 정리하는게 파악하기도 쉽고 그런 거 같다.
그래도 더 해봐야겠지만…