섹션 10. 리팩토링 1 - 리스트 아이템 컴포넌트 공통화

Cho Dragoo·2021년 6월 24일
0

vue.js 인프런 학습을 위한 강의와 책

이 스리즈에서는 인프런 강좌
"Vue.js 완벽 가이드 - 실습과 리팩토링으로 배우는 실전 개념" 을 수강하고 복습을 위한 기록을 하고 있습니다. 자세한 강의내용이 궁금하신 분은 https://www.inflearn.com/course/vue-js/dashboard 의 링크를 참고하세요.

[실습 안내] 뉴스 리스트 스타일링

src / view / NewsView.vue

<template>
  <div>
    <ul class="news-list">
      <li v-for="item in fetchedNews" v-bind:key="item.id" class="post">
        <!-- 포인트영역 -->
        <div class="points">
          {{ item.points }}
        </div>
        <!-- 기타 정보 영역 -->
        <a :href="item.url">
          {{ item.title }}
        </a>
        <small>
          {{ item.time_ago }} by
          <router-link v-bind:to="`/user/${item.user}`">{{
            item.user
          }}</router-link>
        </small>
      </li>
    </ul>
    <!-- <p v-for="item in fetchedNews" v-bind:key="item.id">
      <a :href="item.url">
        {{ item.title }}
      </a>
      <small>
        {{ item.time_ago }} by
        <router-link v-bind:to="`/user/${item.user}`">{{
          item.user
        }}</router-link>
      </small>
    </p> -->
  </div>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters(["fetchedNews"]),
  },
  created() {
    this.$store.dispatch("FETCH_NEWS_LIST");
  },
};
</script>

<style scoped>
.news-list {
  margin: 0;
  padding: 0;
}

.post {
  list-style: none;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #eee;
}

.points {
  width: 80px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #42b883;
}
</style>

컴포넌트의 개발사고에 익숙해지기 위한 작업을 시작한다.

우선NewsView.vue파일에서 스타일링 작업을 해주고 이코드를 기준으로 나머지 view /들을 리펙토링 해본다.

스타일링 할 곳에 class네임을 주고 <style scoped>영역에서 코드를 작성한다.

개발자도구의 element.style {}안에서의 스타일링 테스트는 유용하다.

이렇게 스타일링 되었으면 성공이다. 브라우저 영역을 줄일 시에 포인트를 출력하는 영역이 들쑥날쑥 한 건 정상적인 현상인 것 같고. 포인트가 출력 되지 않는 공간도 가끔 나오는데 API쪽에서 누락했을 확률이 높은 것 같다고 봐야 한다.

src / app.vue

<style>
body {
  padding: 0;
  margin: 0;
}

/* new zone */
a {
  color: #34495e;
  text-decoration: none;
}

a:hover {
  color: #42b883;
  text-decoration: underline;
}

a.router-link-exact-active {
  text-decoration: underline;
}
/* new zone */

/* Router Transaction */
.page-enter-active,
.page-leave-active {
  transition: opacity 0.5s;
}
.page-enter, .page-leave-to /* .page-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>

app.vue

파일의 <style>로 가서 /* new zone */처럼 작성한다.

파란 텍스트를 일정한 색깔로 통일하고 평소엔 언더라인이 없다가 '마우스호버'나 '헤더영역'의 링크 클릭 시에 언더라인 생성하는 코드다.

이후 저장시에 잘 성공됬다는 걸 확인 할 수있다.

[실습] 질문, 구직 리스트 스타일링

NewsView.vue에서 <template>작업과 스타일링 작업 했었던걸

AskView.vue 로 거의 그대로 재활용이 가능하다. 다른 사이트로 연결 되는게 아닌 router-link로 item/${[item.id](http://item.id/)}에 진입 해야 되는 점이 다르다.

<template>
  <div>
    <ul class="news-list">
      <li v-for="item in fetchedAsk" v-bind:key="item.id" class="post">
        <!-- 포인트영역 -->
        <div class="points">
          {{ item.points }}
          <!-- 공통으로 쓰이는 item 덕분에 리팩토링이 용이해졌다. -->
        </div>
        <!-- 기타 정보 영역 -->
        <div>
          <p class="news-title">
            <router-link v-bind:to="`item/${item.id}`"> <!-- 주의 -->
              {{ item.title }}                           <!-- 주의 -->
            </router-link>                               <!-- 주의 -->
          </p>
          <small class="link-text">
            {{ item.time_ago }} by
            <router-link v-bind:to="`/user/${item.user}`" class="link-text">{{
              item.user
            }}</router-link>
          </small>
        </div>
      </li>
    </ul>

    <!-- <p v-for="item in fetchedAsk" v-bind:key="item.id">
      <router-link v-bind:to="`item/${item.id}`"> 지워지는 구간
        {{ item.title }}
      </router-link>
      <small>{{ item.time_ago }} by {{ item.user }}</small>
    </p> -->
  </div>
</template>

특히 주목할 것은 이 작업을 했을 때 mapGetters에서 각종 텍스트 데이터가 담긴 배열 fetchedAsk<template>코드에 반드시 적용하는 것과 v-for="item in fetchedAsk"에서 보듯이 모든 views폴더 내의 파일에서 쓰이는 item 덕분에 재사용하기 편해졌다는 걸 알게된다.

<style scoped>
.news-list {
  margin: 0;
  padding: 0;
}

.post {
  list-style: none;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #eee;
}

.points {
  width: 80px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #42b883;
}

.news-title {
  margin: 0;
}

.link-text {
  color: #828282;
}
</style>

class 네임이 같아졌으므로 NewsView.vue 의 스타일링 그대로 쓸 수있다.



JobsView.vue 파일의 <template>역시 비슷한 방법으로 수정하면 된다.

<template>
  <div>
    <ul class="news-list">
      <li v-for="job in fetchedJobs" v-bind:key="job.id" class="post">
        <!-- 포인트영역 -->
        <div class="points">
          {{ job.points || 0 }}
        </div>
        <!-- 기타 정보 영역 -->
        <div>
          <p class="news-title">
            <a :href="job.url">
              {{ job.title }}
            </a>
          </p>
          <small class="link-text">
            {{ job.time_ago }} by
            <!-- <router-link v-bind:to="`/user/${job.url}`" class="link-text">{{
              {{job.domain}}
            }}</router-link> -->
            <a :href="job.url">
              {{ job.domain }}
            </a>
          </small>
        </div>
      </li>
    </ul>
    <!-- 
    <p v-for="job in fetchedJobs" v-bind:key="job.id">
      <a :href="job.url">
        {{ job.domain }}
      </a>
      <small>{{ job.time_ago }} , {{ job.domain }}</small>
    </p> -->
  </div>
</template>

다만 데이터 정보가 AskView.vueNewsView.vue하고는 차이나 나있다. 그래서 job.user가 아닌 job.url이며 a테그 :href로 사이트 이동하게 된다.

포인트 데이터도 비어있으므로 {{ job.points || 0 }}처럼 포인트 값이 null이면 0이 나오게 한다. 덕분에 타이틀간의 문단간격이 유지된다.

css 스타일링은 역시 그대로 이용하게 된다.



[실습 안내] 공통 컴포넌트 ListItem 제작 및 실습 안내

그 동안 작업을 해보았다면 알 둣이 AskView.vue , NewsView.vue, JobsView.vue 파일들은 많이 유사하다는걸 알 수있다.

그래서 이 반복되는 패턴을 담당하는 공통 컴포넌트 ListView.vue 파일을 생성한다.

components / ListView.vue

<template>
  <div>
    <ul class="news-list">
      <li v-for="item in fetchedNews" v-bind:key="item.id" class="post">
        <!-- 포인트영역 -->
        <div class="points">
          {{ item.points }}
        </div>
        <!-- 기타 정보 영역 -->
        <div>
          <p class="news-title">
            <a :href="item.url">
              {{ item.title }}
            </a>
          </p>
          <small class="link-text">
            by
            <router-link v-bind:to="`/user/${item.user}`" class="link-text">{{
              item.user
            }}</router-link>
          </small>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters(["fetchedNews"]),
  },
  created() {
    this.$store.dispatch("FETCH_NEWS_LIST");
  },
};
</script>

<style scoped>
.news-list {
  margin: 0;
  padding: 0;
}

.post {
  list-style: none;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #eee;
}

.points {
  width: 80px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #42b883;
}

.news-title {
  margin: 0;
}

.link-text {
  color: #828282;
}
</style>

NewsView.vue에 있던 <template>, <script>, <style scoped> 영역의 코드를 ListView.vue에 몽땅 복붙해 가져온다.



src / view / NewsView.vue

<template>
  <div>
    <list-item></list-item>
  </div>
</template>

<script>
import ListItem from "../components/ListItem.vue";

export default {
  components: {
    ListItem,
  },
};
</script>

그리고 NewsView.vue파일의 내용은 대부분 지우고

<template>에는 <list-item></list-item> 추가 (VScode 자동완성의 오타를 조심)

<script>영역에는 ListItemimport해서 components: {ListItem}export default 한다.



재대로 했다면 vue 개발자 도구에서 하위에 이 생성된걸 볼 수있다.

하지만 이대로는 나머지 파일들은 props 차이 때문에 그대로 적용을 하지는 못한다.



[실습] 공통 컴포넌트 구현(1) - 페이지별 데이터 분기

AskView.vue

<template>
  <div>
    <list-item></list-item>
    <!-- <ul class="news-list">
      <li v-for="item in fetchedAsk" v-bind:key="item.id" class="post">
        <div class="points">
          {{ item.points }}
        </div>
        <div>
          <p class="news-title">
            <router-link v-bind:to="`item/${item.id}`">
              {{ item.title }}
            </router-link>
          </p>
          <small class="link-text">
            {{ item.time_ago }} by
            <router-link v-bind:to="`/user/${item.user}`" class="link-text">{{
              item.user
            }}</router-link>
          </small>
        </div>
      </li>
    </ul> -->
  </div>
</template>

<script>
// import { mapGetters } from "vuex";
import ListItem from "../components/ListItem.vue";

export default {
  // computed: {
  //   ...mapGetters(["fetchedAsk"]),
  // },
  // created() {
  //   this.$store.dispatch("FETCH_ASK_LIST");
  // },
  components: {
    ListItem,
  },
};
</script>

이대로 하면 News, Ask페이지의 내용이 똑같이 나오는 오류가 나온다.

ListView.vue

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters(["fetchedNews"]),
  },
  created() {
    // this.$store.dispatch("FETCH_NEWS_LIST"); 
    console.log(this.$route);
  },
};
</script>

console.log(this.$route) 로 개발자 도구 확인

name부분이 정의되지 않다는걸 알 수있다. 이제 name을 정의해서 ListView.vue

파일을 시작으로 분기처리 작업을 할 것이다.

route/index.js 에서 name을 정의해 추가

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters(["fetchedNews"]),
  },
  created() {
    // this.$store.dispatch("FETCH_NEWS_LIST"); // 여기서 분기처리가 필요하다.
    console.log(this.$route.path === "/news");
///////////////////////////
    const name = this.$route.name;
    if (name == "news") {
      this.$store.dispatch("FETCH_NEWS_LIST");
    } else if (name == "ask") {
      this.$store.dispatch("FETCH_ASK_LIST");
    } else if (name == "jobs") {
      this.$store.dispatch("FETCH_JOBS_LIST");
    }
////////////////////////////
  },
};
</script>

if문으로 각각 파일로 보낼 분기점을 추가

데이터 자체는 잘 전달 됬지만 <template>에 텍스트로 붙여지지 않았는지 Ask페이지는 News와 동일한 내용으로 보여진다. 추가 작업이 필요한 상황이다.



공통 컴포넌트 구현(2) - computed 속성

components / ListView.vue

<template>
  <div>
    <ul class="news-list">
      <li v-for="item in listItems" v-bind:key="item.id" class="post"> <!-- listItems 추가-->
        <!-- 포인트영역 -->
        <div class="points">
          {{ item.points }}
        </div>
        <!-- 기타 정보 영역 -->
        <div>
          <p class="news-title">
            <a :href="item.url">
              {{ item.title }}
            </a>
          </p>
          <small class="link-text">
            by
            <router-link v-bind:to="`/user/${item.user}`" class="link-text">{{
              item.user
            }}</router-link>
          </small>
        </div>
      </li>
    </ul>
  </div>
</template>

<template>

영역에도 분기를 해야한다. 그래서 우선은 텍스트 데이터를 뿌려주는 영역인 v-for="item in listItems" 로 변경 후

<script>
export default {
  created() {
    const name = this.$route.name;
    if (name === "news") {
      this.$store.dispatch("FETCH_NEWS_LIST");
    } else if (name === "ask") {
      this.$store.dispatch("FETCH_ASK_LIST");
    } else if (name === "jobs") {
      this.$store.dispatch("FETCH_JOBS_LIST");
    }
  },
//////////////////////////////
  computed: {
    // eslint-disable-next-line vue/return-in-computed-property
    listItems() {
      const name = this.$route.name;
      if (name === "news") {
        return this.$store.state.news;
      } else if (name === "ask") {
        return this.$store.state.ask;
      } else if (name === "jobs") {
        return this.$store.state.jobs;
      }
    },
  },
};
/////////////////////////////////////
</script>

listItems() 분기처리 위한 if문을 computed 속성에 추가

다만 왜인지 강의내용과는 달리 이쪽에서 eslint의 오류가 일어났고 VScode에서 제공하는 예외처리 주석으로 임시 해결해 진행했다.

JobsView.vue

<template>
  <div>
    <list-item></list-item>
    <!-- <ul class="news-list">
      <li
        v-for="job in this.$store.state.jobs"
        v-bind:key="job.id"
        class="post"
      >
        
        <div class="points">
          {{ job.points || 0 }}
        </div>
        
        <div>
          <p class="news-title">
            <a :href="job.url">
              {{ job.title }}
            </a>
          </p>
          <small class="link-text">
            {{ job.time_ago }} by
            <a :href="job.url">
              {{ job.domain }}
            </a>
          </small>
        </div>
      </li>
    </ul> -->
  </div>
</template>

<script>
import ListItem from "../components/ListItem.vue";

export default {
  components: {
    ListItem,
  },
};
</script>

JobsView.vue 파일도 NewsView.vue처럼 <list-item></list-item>로 대체해서 ListView.vue로 보내버린다.

현재 jobs페이지에 포인트를 표시하는 노드가 비워져 있다. 본래 jobs페이지를 담당하는 JobsView.vue
파일이 분기처리위해 간략해지면서 없어진 것이다.

<template>
  <div>
    <ul class="news-list">
      <li v-for="item in listItems" v-bind:key="item.id" class="post">
        <!-- 포인트영역 -->
        <div class="points">
          {{ item.points || 0 }} <!-- JobsView.vue에 했던 것 처럼 다시 추가 -->
        </div>
        <!-- 기타 정보 영역 -->
        <div>
          <!-- 타이틀 영역 -->
          <p class="news-title">
            <template v-if="item.domain">
              <a v-bind:href="item.url">
                {{ item.title }}
              </a>
            </template>
            <template v-else>

JobsView.vue에 했던 것 처럼 다시 추가

item.points || 0item.points 의 변수값이 null 처럼 비어있으면 숫자'0'이 적용되는 순수 js의 문법이다.



재대로 입력했다면 해당 페이지에 다시 0 포인트가 돌아오게 된다.




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

jobs페이지에서는 유저정보가 아닌 도메인정보이기 때문에 관련된 노드가 표시되지 않았다

이걸 고쳐야한다.

Ask 페이지도 제목을 클릭하면 관련 질문 페이지가 나오지 않는다.

Ask 페이지는 다른 페이지와는 달리 item 페이지의 특정유저id를 보여주는 주소로 이동하기 때문이다.

따라서 Ask 페이지도 적용한다는 것은 공통 컴포넌트 ListView.vue의 해당 노드에도 if문으로 분기 시켜야 한다는 것이다.




ListView.vue

<template>
  <div>
    <ul class="news-list">
      <li v-for="item in listItems" v-bind:key="item.id" class="post">
        <!-- 포인트영역 -->
        <div class="points">
          {{ item.points || 0 }}
        </div>
        <!-- 기타 정보 영역 -->
        <div>
          <!-- 타이틀 영역 -->
          <p class="news-title">

            <template v-if="item.domain">   <!-- 첫번째 분기법 -->
              <a v-bind:href="item.url">
                {{ item.title }}
              </a>
            </template>
            <template v-else>
              <router-link v-bind:to="`item/${item.id}`">
                {{ item.title }}
              </router-link>
            </template>

          </p>
          <small class="link-text">
            {{ item.time_ago }} by

            <router-link                       <!-- 두번째 분기법 -->
              v-if="item.user"
              v-bind: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>

그 모든 문제점을 고친 파일내용을 보자. 분기하는 방법은 2가지로 보인다.

한가지는 <template>테그를 새로 생성해 v-if="item.domain"을 작성하고 <template>테그 내부에 활용했던 해당 노드를 넣는 것

item.domain일 때 <template>내부의 내용인 item.url로 링크 연결 되며 그게 아니면 <template v-else>테그의 내용대로 item.id의 링크가 적용된다는 뜻이 된다.

두번째는 특정테그를 만들어 감싸지 않고 해당노드 내부에 그대로 v-if문을 작성하는 것이다.

개인적으로 이 방법이 마음에 들었으며 가독성을 좀 더 높일 필요가 있다면 첫번째 방법이 더 좋을지도 모른다.

profile
어떤 문제든 파악 할 수 있으며 해결책을 찾을 수 있는 개발능력을 꿈꾸고 있습니다.

0개의 댓글