11 리팩토링 3 - Mixin과 하이 오더 컴포넌트
source: categories/study/vue-beginner-lv3/vue-beginner-lv3_9-02.md
11.1 컴포넌트 재활용 방법 및 재활용할 포인트 소개
수정해야될 공통 부분 소개
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>
src/routes/modules/ask.js
import AskView from "../../views/AskView";
const askRouter = [
{
path: '/ask',
name: 'ask',
component: AskView,
},
]
export default askRouter
src/routes/modules/jobs.js
import JobsView from "../../views/JobsView";
const jobsRouter = [
{
path: '/jobs',
name: 'jobs',
component: JobsView,
},
]
export default jobsRouter
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/views/AskView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
export default {
name: "AskView",
components: {
ListItem,
},
created() {
this.$store.dispatch('FETCH_ASK');
}
}
</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,
},
created() {
this.$store.dispatch('FETCH_JOBS');
}
}
</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,
},
created() {
this.$store.dispatch('FETCH_NEWS');
}
}
</script>
<style scoped>
</style>
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 [];
}
},
}
</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>
위에 보면 AskView
, JobsView
, NewsView
는 공통 컴포넌트를 쓰고있다.
이를 정리해보도록 하겠다.
이를 위해 Mixin과 하이오더컴포넌트(HOC)를 배우고 각각 적용해보면서 어떤 부분이 어떻게달라지는지 알아보겠습니다.
11.2 이벤트 버스를 이용한 스피너 컴포넌트 구현
HOC와 믹스인이 왜 좋은지 알려면 HOC와 믹스인을 알아보기 전에, 페이지 로딩바를 적용해야될 것 같다.
스피너는 App.vue
에서 관리하는 것이기 때문에 App.vue
에서 props
로 각 하위 컴포넌트로 내리고,
AskView
, JobsView
, NewsView
에서 event emit
으로 올리고 하는식으로 관리하는 것이 좋다.
하지만 그보다 스피너는 이벤트 버스로 처리하는 것이 훨씬 더 편하다.
이벤트 버스라는 것은 빈 이벤트 객체를 하나 만들어서 그 이벤트 객체를 통해서 컴포넌트간의 데이터를 전달하는 것을 의미한다.
Spinner 동작 bus.$emit(), 이벤트 버스로 제어
src/components/Spinner.vue
<template>
<div class="lds-facebook" v-if="loading">
<div>
</div>
<div>
</div>
<div>
</div>
</div>
</template>
<script>
export default {
name: "Spinner",
props: {
loading: {
type: Boolean,
required: true,
},
},
}
</script>
<style scoped>
.lds-facebook {
display: inline-block;
position: absolute;
width: 64px;
height: 64px;
top: 47%;
left: 47%;
}
.lds-facebook div {
display: inline-block;
position: absolute;
left: 6px;
width: 13px;
background: #42b883;
animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite;
}
.lds-facebook div:nth-child(1) {
left: 6px;
animation-delay: -0.24s;
}
.lds-facebook div:nth-child(2) {
left: 26px;
animation-delay: -0.12s;
}
.lds-facebook div:nth-child(3) {
left: 45px;
animation-delay: 0;
}
@keyframes lds-facebook {
0% {
top: 6px;
height: 51px;
}
50%, 100% {
top: 19px;
height: 26px;
}
}
</style>
src/utils/bus.js
import Vue from "vue";
export default new Vue()
src/views/NewsView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
import bus from "../utils/bus";
export default {
name: "NewsView",
components: {
ListItem,
},
created() {
bus.$emit('start:spinner');
this.$store.dispatch('FETCH_NEWS');
bus.$emit('end:spinner');
},
}
</script>
<style scoped>
</style>
src/App.vue
<template>
<div id="app">
<tool-bar></tool-bar>
<transition name="page">
<router-view></router-view>
</transition>
<spinner :loading="loadingStatus"></spinner>
</div>
</template>
<script>
import ToolBar from "./components/ToolBar";
import Spinner from "./components/Spinner";
import bus from "./utils/bus";
export default {
name: 'App',
components: {
ToolBar,
Spinner,
},
data() {
return {
loadingStatus: false,
}
},
methods: {
startSpinner() {
this.loadingStatus = true;
},
endSpinner() {
this.loadingStatus = false;
},
},
created() {
bus.$on('start:spinner', this.startSpinner);
bus.$on('end:spinner', this.endSpinner);
},
beforeDestroy() {
bus.$off('start:spinner', this.startSpinner);
bus.$off('end:spinner', this.endSpinner);
}
}
</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>
이제 bus.$emit()
가 일어나는 시점에 대해서만 고민하면 됩니다.
11.3 스피너 실행 및 종료 시점 알아보기
src/views/AskView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
import bus from "../utils/bus";
export default {
name: "AskView",
components: {
ListItem,
},
async created() {
bus.$emit('start:spinner');
setTimeout(async () => {
await this.$store.dispatch('FETCH_ASK');
bus.$emit('end:spinner');
}, 3000)
},
// created() {
// bus.$emit('start:spinner');
// setTimeout(() => {
// this.$store.dispatch('FETCH_ASK')
// .then(() => {
// console.log('fetched');
// bus.$emit('end:spinner');
// })
// .catch((error) => {
// console.log(error);
// })
// }, 3000)
// }
}
</script>
<style scoped>
</style>
src/views/JobsView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
import bus from "../utils/bus";
export default {
name: "JobsView",
components: {
ListItem,
},
async created() {
bus.$emit('start:spinner');
setTimeout(async () => {
await this.$store.dispatch('FETCH_JOBS');
bus.$emit('end:spinner');
}, 3000)
},
// created() {
// bus.$emit('start:spinner');
// setTimeout(() => {
// this.$store.dispatch('FETCH_JOBS')
// .then(() => {
// console.log('fetched');
// bus.$emit('end:spinner');
// })
// .catch((error) => {
// console.log(error);
// })
// }, 3000)
// }
}
</script>
<style scoped>
</style>
src/views/NewsView.vue
<template>
<div>
<list-item></list-item>
</div>
</template>
<script>
import ListItem from "../components/ListItem";
import bus from "../utils/bus";
export default {
name: "NewsView",
components: {
ListItem,
},
async created() {
bus.$emit('start:spinner');
setTimeout(async () => {
await this.$store.dispatch('FETCH_NEWS');
bus.$emit('end:spinner');
}, 3000)
},
// created() {
// bus.$emit('start:spinner');
// setTimeout(() => {
// this.$store.dispatch('FETCH_NEWS')
// .then(() => {
// console.log('fetched');
// bus.$emit('end:spinner');
// })
// .catch((error) => {
// console.log(error);
// })
// }, 3000)
// }
}
</script>
<style scoped>
</style>
src/store/modules/FetchNews.js
import {fetchNewsList} from "../../api";
const state = {
news: [],
}
const getters = {
fetchedNews(state) {
return state.news;
},
}
const mutations = {
SET_NEWS(state, news) {
state.news = news;
},
}
const actions = {
async FETCH_NEWS({commit}) {
try {
const {data} = await fetchNewsList();
commit('SET_NEWS', data);
// return data
} catch (error) {
console.log(error);
}
},
// FETCH_NEWS({commit}) {
// fetchNewsList()
// .then(({data}) => {
// commit('SET_NEWS', data);
// // return data;
// })
// .catch(error => console.log(error))
// }
}
export default {
state,
getters,
mutations,
actions,
}
src/store/modules/FetchAsk.js
import {fetchAskList} from "../../api";
const state = {
ask: [],
}
const getters = {
fetchedAsk(state) {
return state.ask;
},
}
const mutations = {
SET_ASK(state, ask) {
state.ask = ask;
},
}
const actions = {
async FETCH_ASK({commit}) {
try {
const {data} = await fetchAskList();
commit('SET_ASK', data);
// return data
} catch (error) {
console.log(error);
}
},
// FETCH_ASK({commit}) {
// fetchNewsList()
// .then(({data}) => {
// commit('SET_ASK', data);
// // return data;
// })
// .catch(error => console.log(error))
// }
}
export default {
state,
getters,
mutations,
actions,
}
src/store/modules/FetchJobs.js
import {fetchJobsList} from "../../api";
const state = {
jobs: [],
}
const getters = {
fetchedJobs(state) {
return state.jobs;
}
}
const mutations = {
SET_JOBS(state, jobs) {
state.jobs = jobs;
},
}
const actions = {
async FETCH_JOBS({commit}) {
try {
const {data} = await fetchJobsList();
commit('SET_JOBS', data);
// return data
} catch (error) {
console.log(error);
}
},
// FETCH_JOBS({commit}) {
// fetchJobsList()
// .then(({data}) => {
// commit('SET_JOBS', data);
// // return data;
// })
// .catch(error => console.log(error))
// }
}
export default {
state,
getters,
mutations,
actions,
}
11.4 하이 오더 컴포넌트(HOC) 소개 및 구현
하이오더 컴포넌트는 컴포넌트의 코드마저 재사용할 수 있는 기술입니다.
하이오더 컴포넌트 정의
뷰의 하이 오더 컴포넌트는 리액트의 하이 오더 컴포넌트에서 기원된 것입니다. (리액트에서 하이 오더 컴포넌트를 가장 많이 사용하고 있다.)
리액트의 하이 오더 컴포넌트 소개 페이지를 보면 아래와 같이 정확한 정의가 나와 있습니다.
A higher-order component (HOC) is an advanced technique in React for reusing component logic.
이 말을 정리해보면 다음과 같습니다.
하이 오더 컴포넌트는 컴포넌트의 로직(코드)을 재사용하기 위한 고급 기술입니다.
<!-- ProductList.vue -->
<template>
<section>
<ul>
<li v-for="product in products">
...
</li>
</ul>
</section>
</template>
<script>
import bus from './bus.js';
export default {
name: 'ProductList',
mounted() {
bus.$emit('off:loading');
},
// ...
}
</script>
<!-- UserList.vue -->
<template>
<div>
<div v-for="product in products">
...
</div>
</div>
</template>
<script>
import bus from './bus.js';
export default {
name: 'UserList',
mounted() {
bus.$emit('off:loading');
},
// ...
}
</script>
위 코드는 ProductList라는 컴포넌트와 UserList 컴포넌트의 로직을 정의한 코드입니다.
두 컴포넌트가 각각 상품과 사용자 정보를 서버에서 받아와 표시해주는 컴포넌트라고 가정했을 때,
공통적으로 들어가는 코드는 다음과 같습니다.
name: '컴포넌트 이름',
mounted() {
bus.$emit('off:loading');
},
name
은 컴포넌트의 이름을 정의해주는 속성이고, mounted()
에서 사용한 이벤트 버스는 서버에서 데이터를 다 받아왔을 때
스피너나 프로그레스 바와 같은 로딩 상태를 완료해주는 코드입니다.
이 두 컴포넌트 이외에도 서버에서 데이터 목록을 받아와 표시해주는 컴포넌트가 있다면 또 비슷한 로직이 반복될 것입니다.
이 때 이 반복되는 코드를 줄여줄 수 있는 패턴이 바로 하이 오더 컴포넌트입니다.
하이 오더 컴포넌트로 반복 코드 줄이기
위에서 반복되는 코드를 줄이기 위해 하이 오더 컴포넌트를 구현해보겠습니다.
// CreateListComponent.js
import bus from './bus.js';
import ListComponent from './ListComponent.vue';
export default function createListComponent(componentName) {
return {
name: componentName,
mounted() {
bus.$emit('off:loading');
},
render(h) {
return h(ListComponent);
}
}
}
위 코드는 CreateListComponent라는 하이 오더 컴포넌트를 구현한 코드입니다.
하이 오더 컴포넌트를 적용할 컴포넌트들의 공통 코드들(mounted, name 등)을 미리 정의했습니다.
그럼 이제 이 하이오더 컴포넌트를 어떻게 사용할까요? 아래를 보겠습니다.
// router.js
import createListComponent from './createListComponent.js';
new VueRouter({
routes: [
{
path: '/products',
component: createListComponent('ProductList');
},
{
path: '/users',
component: createListComponent('UserList');
},
// ...
]
})
위와 같은 방식으로 하이 오더 컴포넌트를 임포트 하고, 각 컴포넌트의 이름만 정의를 해주면 컴포넌트의 기본 공용 로직인 mounted()
, name
를 가지고 컴포넌트가 생성됩니다.
따라서, 컴포넌트마다 불 필요하게 반복되는 코드를 정의하지 않아도 됩니다.
실습 - 하이오더 컴포넌트 구현하기
src/routes/modules/news.js
import NewsView from "../../views/NewsView";
import CreateListView from "../../views/CreateListView";
const newsRouter = [
{
// path: url 주소
path: '/news',
// component: url 주소로 갔을 때 표시될 컴포넌트 ( 페이지단위라고 보면됨, 예를 들어서 MainPage 같은 페이지단위 컴포넌트 )
name: 'news',
// component: NewsView,
component: CreateListView('NewsView'),
},
]
export default newsRouter
src/routes/modules/ask.js
import AskView from "../../views/AskView";
import CreateListView from "../../views/CreateListView";
const askRouter = [
{
path: '/ask',
name: 'ask',
// component: AskView,
component: CreateListView('AskView'),
},
]
export default askRouter
src/routes/modules/jobs.js
import JobsView from "../../views/JobsView";
import CreateListView from "../../views/CreateListView";
const jobsRouter = [
{
path: '/jobs',
name: 'jobs',
// component: JobsView,
component: CreateListView('JobsView'),
},
]
export default jobsRouter
src/views/CreateListView.js
import ListView from "./ListView";
export default function CreateListView(name) {
return {
// 재사용할 인스턴스(컴포넌트) 옵션들이 들어갈 자리
// 아래와 같은 속성들을 넣을 수 있다
// el: '',
// data: '',
// components: {
//
// },
// created() {
//
// },
// name 프로퍼티를 받아서 컴포넌트 이름을 정의해줍니다.
name,
// 내부적으로 탬플릿을 컴파일해줄 때 render 함수를 사용합니다.
render(createElement) {
return createElement(ListView);
}
}
}
src/views/ListView.vue
<template>
<div>
list
</div>
</template>
<script>
export default {
name: "ListView"
}
</script>
<style scoped>
</style>
11.5 하이 오더 컴포넌트에서 사용할 ListView 컴포넌트 구현
조금 많이 정리됨…
깃 커밋기록 참고…
풀리지않는 의문
로딩 스피너시 일부로 3초뒤에 데이터 요청하라고 setTimeout
을 활용했다.
그런데 스피너 생성되고 없어지고는 잘 되는데, 문제는 3초 사이동안 이전 데이터가 그냥 화면에 뿌려져서 보인다.
그러다가 3초뒤에 setTimeout
끝나고 진짜 데이터 요청을 보내서 받아오면 그때 또 바뀐다.
흐음.. 실습이긴하지만 이 부분도 완벽하게 동작하도록 하고싶은데.. 방법이 없나?
11.6 하이 오더 컴포넌트가 적용된 앱 구조 설명 및 호름 정리
다음 시간엔 위에서 HOC로 정리한 것을 Mixins로 정리해보겠습니다.
그러면서 HOC와 Mixins의 차이점에 대해 보도록 하겠습니다.
우선 다음을 잘 기억해주십시오.
HOC를 사용했을 시, 위와 같이 JobsVIew
라는(이 이름은 계속 바뀝니다. name
인자값으로 설정되게 해놨기 때문) 하이오더 컴포넌트가 생성되게 됩니다.
그 아래에 render(ListView)
랜더 함수로 생성한 ListView
컴포넌트가 들어가구요.
이를 잘 기억하시길 바랍니다.
11.7 Mixin의 개요와 활용처 그리고 HOC와의 차이점
컴포넌트 재활용 방법 2번째, Mixin에 대해 알아보도록 하겠습니다.
Mixins
믹스인(Mixins)은 여러 컴포넌트 간에 공통으로 사용하고 있는 로직, 기능들을 재사용하는 방법입니다.
믹스인에 정의할 수 있는 로직은 data, methods, created 등과 같은 컴포넌트 옵션입니다.
믹스인 코드 형식
믹스인 문법은 아래와 같습니다.
var HelloMixins = {
// 컴포넌트 옵션 (data, methods, created 등)
}
new Vue({
mixins: [HelloMixins],
})
위와 같이 믹스인을 주입할 컴포넌트에 mixins
속성을 선언하고 배열 []
안에 주입할 믹스인들을 추가합니다.
믹스인 사용 예시
그럼 실제로 있을 법한 믹스인 코드 예시를 보겠습니다.
웹 애플리케이션을 구현할 때 많이 사용되는 다이얼로그(모달 혹은 팝업 창)의 열기, 닫기 로직을 믹스인에 정의했습니다.
var DialogMixin = {
data() {
return {
dialog: false,
}
},
methods: {
showDialog() {
this.dialog = true;
},
closeDialog() {
this.dialog = false;
},
}
}
DialogMixin
에는 다디얼로그의 표시 상태를 나타내는 dialog
데이터와 다이얼로그를 열거나 닫는 메서드 showDialog()
, closeDialog()
가 정의되어 있습니다.
이제 이 믹스인을 컴포넌트에 어떻게 주입할까요?
아래 코드를 보겠습니다.
<!-- LoginForm.vue -->
<script>
import {DialogMixin} from './mixins.js';
export default {
// ...
mixins: [DialogMixin],
methods: {
submitForm() {
axios.post('login', {
id: this.id,
pw: this.pw,
})
.then(() => this.closeDialog())
.catch(error => new Error(error));
}
}
}
</script>
믹스인을 사용할 수준이 되면 보통은 싱글 파일 컴포넌트 체계에서 ES6를 능숙하게 사용하고 있을겁니다.
위의 코드는 ES6의 모듈화 문법을 이용해 믹스인을 다른 파일에서 가져와서 주입하는 코드입니다.
submitForm()
메서드에서 HTTP POST
요청을 보내고나면 this.closeDialog()
로 메서드를 호출하는데,
이 메서드는 믹스인에 의해 주입된 메서드입니다.
따라서 LoginForm 컴포넌트에 없더라도 사용할 수 있습니다.
- 위와 같이 정리하면 상당히 깔끔하게 코드가 정리가되고,
- 두번째로는 부차적인 요소들.. 예를 들어서
spinner
돌아가는 것들.. 또는 팝업 열고 닫는 것들은Mixins
로 뺐을 때 굉장히 효율이 좋습니다.
그렇게 했을 때, 컴포넌트의 로직들이 굉장히 단순해지면서CRUD
관련된 비즈니스 로직만 남게됩니다.
경우에 따라서는CRUD
또한 공통 로직을 빼서 관리할 수도 있습니다.
HOC는 JobsView
, NewsView
, AskView
같이 페이지 컴포넌트의 공통화에 사용을 했다.
각 3개의 컴포넌트에서 하던 역할을 ListView
컴포넌르로 빼내어 ListItem
컴포넌트를 등록하고,CreateListView
라는 하이오더 컴포넌트에서 name
, created
같은 로직들을 가져가고, render
함수로 ListView
컴포넌트를 랜더링 함으로써,
훨씬 더 깔끔하고 보기좋은 컴포넌트 정리를 할 수 있었다.
다만 하이오더 컴포넌트의 단점은
위와 같이 컴포넌트 레벨이 깊어진다는 단점이 있습니다.
중간에 ListView
라는 컴포넌트가 생겨버립니다.
만약 NewsView
에서 재활용하던 로직들이 ListView
에서 어떤식으로 변형이된다던지, 아니면 여기서 추가적인 로직이 생겨서 통신하는 일이 생기면,
그때부터 레벨이 깊어지기 때문에 통신하기 까다롭게됩니다.
(중간 컴포넌트인 ListView
에 공통으로 빼서 너헝야될 로직이 있다면, 통신이 어려워진다는 뜻인가? 근데 어짜피 store
사용하잖아? 흐음 무슨 경우에 까다로워지는걸까?)
여튼 하이오더 컴포넌트를 많이쓰면 쓸수록 컴포넌트의 깊이가 깊어지면서
자연스럽게 컴포넌트 통신에 있어서 불편한 현상들이 발생합니다.
이제 그 부분에 있어서 Mixins
라는 재활용 로직를 사용해볼겁니다.
11.8 [실습 안내] Mixin 적용 후 HOC 구조와 비교
믹스인
으로하면 컴포넌트 레벨이 hoc
보다 하나 적다.
11.9 [실습] Mixin 실습 및 컴포넌트 재활용 방법에 대한 리뷰
이전 시간에 이미 다 적용해봤다.
믹스인과 HOC 사이의 차이점에 대해 충분히 생각해보고, 어떨 때 어느거를 사용하면 좋을지 궁리해봐야겠다. 흠..
다음 챕터는 라우팅쪽 관련해서 언제 데이터를 불러오면 좋을지, 그거에 대한 UX를 같이 고민해보도록 하겠습니다.