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 속성

Note

내 고민.. 내가 푼 답을 보면 나는 AskView.vue, NewsView.vue, JobsView.vue 컴포넌트에서 ListItem.vue 컴포넌트로 각각 actions 메소드와 getters 함수들을 props로 내려주도록 정리했다.
그렇게 전달받은 propsListItem.vue에서 받아서 실행하고 데이터를 조작하도록 하였다.

그런데 지금 이번 강의에서 강사님은 AskView.vue, NewsView.vue, JobsView.vue 컴포넌트를 거치지않고
ListItem.vue에서 모든 actionsgetters 함수들을 가져다쓰도록 작성했다.
흠.. 이게 어찌보면 코드 파악에는 더 쉬워보이긴 하는데..
경우의 수가 늘어날 수록 else if문이 늘어나서 코드가 길어지면 지저분해보일거 같은데..

어떤게 더 나은 방법일까?
어렵다. 더 강의 들으면서 생각해봐야겠다.

아 그리고 추가로 현재 강의내용대로하면 지금 AskView.vue, NewsView.vue, JobsView.vue 컴포넌트에서 ListItem 컴포넌트의 내용과 100% 일치하지 않은 상태였거든?
그런 미묘한 차이는 어떻게 잡아내는거지?
v-if써서?
근데 그것또한 복잡할거같은데..
여튼 그냥 미세한 차이 무시하고 지금까지는 일괄 처리했네..

아아 이번 강의 마지막 부분에서 이 문제 짚고넘어가시는군. JobsView에서 미세하게 다른 부분을 과연 어떻게 처리할까?

9.7 공통 컴포넌트 구현(3) - template 속성과 v-if 디렉티브 활용 1

Note

아파 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>


Note

흐음.. 강사님의 이런 정리가 더 좋을거 같긴하다.
나처럼 속성 흐름을 여러군데 거쳐서 내려가게하면 흐름 파악이 어려워질테니….
vuexstore 같은 맥락으로 한 곳에 몰아 정리하는게 파악하기도 쉽고 그런 거 같다.
그래도 더 해봐야겠지만…