혼자 적어보는 노트

프로그래머스 데브코스 TIL - Day 39 본문

스터디

프로그래머스 데브코스 TIL - Day 39

jinist 2022. 5. 13. 02:19

 

✅ 오늘의 학습

📌 Vue (6)

 

- 플러그인

- 믹스인

- Teleport

- Provide / Injext

- Store

- Vuex

 

 


 

플러그인 사용

 

app.config.globalProperties에 원하는 플러그인의 이름을 붙여주면
컴포넌트 내부에서 this로 접근할 수 있다.

 

[plugins/fetch.js]

export default {
  install(app, option) {
    // 첫번째 인자는 app 객체, 두 번째 인자는 option
    app.config.globalProperties.$fetch = (url, opts) => {
      return fetch(url, opts).then(res => res.json());
    };
  }
};

 

[main.js]

const app = createApp(App);
app.use(fetchPlugin);
app.mount('#app');

 

[컴포넌트]

  created(){
    this.init();
  },
  methods:{
    async init(){
      const res = await this.$fetch('https://jsonplaceholder.typicode.com/todos/1');
      console.log(res);
    }

 

 

 

mixin

 

공식문서: 믹스인

재사용 가능한 기능을 미리 정의해두고 컴포넌트에서 불러와서 쓸 수 있는 기능.

 

같은 이름의 훅 함수는 병합되어 호출되며, 값을 요구하는 옵션의 객체에 중복된 키가 있다면

컴포넌트 쪽의 옵션이 우선순위를 갖게된다.

 

[mixins/sample.js]

export default {
  data(){
    return {
      count:1,
      msg: 'Hi~'
    };
  }
};

 

[App.vue]

<template>
  <h1>
    {{ msg }} /* HI!!! * 컴포넌트에 선언한 옵션이 출력 됨 */
    {{ count }} /* 1 */
  </h1>
</template>

<script>
import sampleMixin from './mixins/sample';

export default {
  mixins: [sampleMixin],
  data() {
    return {
      msg: 'HI!!!',
    };
  },

};
</script>

 

 

폴더 안의 컴포넌트 전부 불러오기

 

App 컴포넌트 내부에서 많은 컴포넌트들을 등록해야 할 때

일일히 import를 해주어야 하는 불편함이 있다

 

import TextField from '~/components/fields/TextField';
import SimpleRadio from '~/components/fields/SimpleRadio';

export default {
  
  components:{
    TextField,
    SimpleRadio
  },
 
 ...

 

💡 해결 방법!

 

컴포넌트들이 담긴 폴더에 index.js를 만들어준다.

export { default as TextField } from './TextField';
export { default as SimpleRadio } from './SimpleRadio';

 

다시 App.vue로 돌아와서 작성한다.

import * as FieldComponents from '~/components/fields/index.js';

export default {
  components:{
    ...FieldComponents
  },

 

 

Teleport를 활용한 Modal 만들기

 

모달 핸들링 구조

- 모달의 open/close를 결정하는 데이터는 모달 밖의 컴포넌트에 있다.

- 모달 안에서는 받은 값에 따라 open/close를 한다

- 모달 안의 이벤트를 통해 받은 값을 false로 바꿔주어 close를 한다.

 

[App.vue]

<template>
  <Modal
    v-model="isShow"
    width="300px">
    <template #activator>
      <button>클릭 시 Modal Open!</button>
    </template>
    <h3>모달에 나타날 내용</h3>
  </Modal>
</template>

export default {
  data() {
    return {
      isShow: false
    };
  },
};
</script>

#activator를 통해 노출시킬 버튼의 영역을 지정하고

아래는 모달 클릭 시 나타나는 내용을 작성한다.

 

[Modal.vue]

<template>
  <div @click="onModal">
    <slot name="activator"></slot>
  </div>
  <teleport to="body"> <!-- 바디태그 내부로 순간이동 -->
    <template v-if="modelValue">
      <div
        class="modal"
        @click="offModal">
        <div
          :style="{width: `${parseInt(width, 10)}px`}"
          class="modal__inner"
          @click.stop>
          <button
            v-if="closeable"
            class="close"
            @click="offModal">
            x
          </button>
          <slot></slot>
        </div>
      </div>
    </template>
  </teleport>
</template>

teleport안에 작성한 내용은 지정한 body태그 내부로 순간이동 하게 된다.

즉, Modal의 선언은 컴포넌트 내부에 했지만 버튼만 그 자리에 있고

Modal 내용은 body 하단에 위치하게 된다.

 

 

[Modal.vue 의 script부분]

<script>
export default {
  props:{
    width: {
      type: [String, Number],
      default: 400
    },
    closeable:{
      type:Boolean,
      default: false 
    },
    modelValue:{
      type:Boolean,
      default: false,
    }
  },
  watch: {
    modelValue(newValue){
      if(newValue){
        window.addEventListener('keyup', this.keyupHandler);
      } else{
        window.removeEventListener('keyup', this.keyupHandler);
      }
    }
  },
  methods:{
    keyupHandler(event){
      if(event.key === 'Escape'){
        this.offModal();
      }
    },
    onModal(){
      this.$emit('update:modelValue', true);
    },
    offModal(){
      this.$emit('update:modelValue', false);
    }
  }
};
</script>

 

보통 컴포넌트에 등록된 이벤트는 컴포넌트가 언마운트 될 때 제거가 되지만 

 

window에 등록한 이벤트는 직접 제거를 해주어야 한다.

 

$emit을 통해 상위로 값을 전달해준다. 나는 이 $emit 방식이 아직도 좀 헷갈린다.

물론 간단한 예제기 때문에 보면 알겠는데 좀 다른 방식으로 응용을 한다면

뚝딱 만들어내진 못할 것 같은 느낌..? 몇번 더 만들어 보면서 익혀야 될 듯 하다.

 

모달 전역 선언

 

[main.js]

import Modal from '~/components/Modal';

const app = createApp(App);
app.component('Modal', Modal);
app.mount('#app');

모달 같은 경우는 같은 형태를 여러 곳에서 사용할 수 있기 때문에

전역으로 등록해 놓으면 import 없이 사용할 수 있다,

 

 

Provide / inject

부모컴포넌트의 데이터를 뎁스가 깊은 하위 컴포넌트에 전달해야 할 때
props를 통해 일일이 전달하지 않고 한번에 전달할 수 있는 기능.

* 전달하는 데이터에 반응성이 제공되지 않기 때문에 따로 처리를 해야한다.

 

1. 부모 컴포넌트에서 provide 옵션 사용

전달할 데이터를 vue의 computed를 import해서 계산된 값으로 만들어서 사용한다

=> 그냥 사용 시 전달된 데이터는 반응성을 가지지 않는다.

<script>
import Parent from '~/components/Parent';
import { computed } from 'vue';
export default {
  
  components:{
    Parent,
  },
  provide(){
    return{ 
      msg: computed(()=> this.msg)
    };
  },
  data(){
    return{
      msg: '전달할 데이터'
    };
  }

};
</script>

 

2. 전달 받을 값을 사용할 하위 컴포넌트에서 injext 옵션 사용

- computed로 계산되어 전달된 값을 사용할 경우 .value로 접근해서 사용해주어야 한다.

<template>
  <h2>Child! / {{ msg.value }}</h2>
</template>


<script>
export default {
  inject: ['msg']
};
</script>

 

 

store

store에서 공유된 데이터를 직접 변경해도 데이터가 변경된다.
하지만 직접 여러 곳에서 변경을 하게 된다면 이후 프로젝트의 규모가 커졌을 때
데이터를 추적하기가 어려워지기 때문에 함수를 통해서만 state의 변경을 해야한다.

 

[store.js]

import { reactive } from 'vue';

export const state = reactive({
    msg: 'Hello Vue?!',
    count: 1

});

export const getters = {
  reversedMsg(){
    return state.msg.split('').reverse().join('');
  }
};

export const mutations = {
  increaseCount() {
    state.count += 1;
  },
  decreaseCount() {
    state.count -= 1;
  },
  updateMsg(newMsg) {
    state.msg = newMsg;
  }
};


export const actions = {
  async fetchTodo(){
    const todo = await fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json());
    mutations.updateMsg(todo.title);
    console.log(todo);
  }
};

함수를 통해 state를 변경하고 반응성을 적용하기 위해 vue의 reactive를 사용한다.

getters에 함수를 등록하여 변환된 값을 전달해 줄 수 있고

mutations를 사용하여 state를 변경하는 함수를 등록 할 수 있고

actions를 통해 비동기 함수를 구분하여 변경할 수 있다.

 

컴포넌트에서는 아래와 같이 store의 데이터를 사용할 수 있다.

<template>
  <h1>Hello.vue</h1>
  <div>{{ reversedMsg }}</div>
  <div @click="increaseCount">
    {{ count }}
  </div>
  <button @click="fetchTodo">
    Get Todo
  </button>
</template>


<script>
import { state, getters, mutations, actions } from '~/store';
export default {
  data() {
    return state;
  },
  computed:{
    reversedMsg: getters.reversedMsg
  },
  methods:{
    increaseCount: mutations.increaseCount,
    fetchTodo: actions.fetchTodo
  },
};
</script>

 

 

 

Vuex

: Vue에서의 상태 관리 패턴 + 라이브러리

 

공식 문서 : Vuex

 

바로 위에 작성했던 코드와 비슷한 형태로 조금 더 편하게 관리 할 수 있는 라이브러리이다.

사용 방법은 비슷하다

 

1. store 생성

 

[store/index.js]

import { createStore } from 'vuex';

export default createStore({
  // 데이터는 항상 함수로
  state() {
    return {
      msg: 'Hello vue',
      count: 1
    };
  },
  getters : {
    reversedMsg(state){
      return state.msg.split('').reverse().join('');
    }
  },
  mutations: {
    increaseCount(state) {
      state.count += 1;
    },
    updateMsg(state, newMsg){
      state.msg = newMsg;
    }
  },
  actions: {
    async fetchTodo({commit}){
      const todo = await fetch('https://jsonplaceholder.typicode.com/todos/1').then(res=>res.json());
      console.log(todo),
      commit('updateMsg', todo.title);
      
    }
  }
});

createStore으로 저장소 생성 후 객체에 아래의 내용들을 선언할 수 있다.


state (상태)

: store에서 다루는 데이터


getters (계산된 데이터)

: 함수로 이루어진 계산된 데이터들이 담기는 곳. 첫 번째 인자로 현재 컴포넌트의 state 값을 제공한다.


mutations (상태 변경)

: 상태를 변경하는 함수들이 담기는 곳. 첫 번째 인자로 현재 컴포넌트의 state 값을 제공한다.


actions (비동기)

: 비동기 동작들을 다루는 곳. 첫 번째 인자는 context를 제공한다.
context에는 state, getters, commit, dispatch이 있고
commit은 mutaion을 실행할 때 사용되며
dispatch는 action을 실행할 때 사용된다.

 

 

2. app에 연결

store를 app에 연결한다.

import { createApp } from 'vue';
import App from './App.vue';
import store from '~/store';

const app = createApp(App);
app.use(store);
app.mount('#app');

 

3. store의 state/getter 사용하기

computed 내부에서 함수의 반환 값으로 데이터를 받아온다.

<template>
  <h1>Hello 컴포넌트</h1>
  <h1>{{ msg }}</h1>
  <h1>{{ count }}</h1>
  <h1>{{ reversedMsg }}</h1>
</template>

<script>
export default {
  computed: {
    // 계산된 값으로 사용
    msg(){
      return this.$store.state.msg;
    },
    count(){
      return this.$store.state.count;
    },
    reversedMsg(){
      return this.$store.getters.reversedMsg;
    }
  }
};
</script>

 

4. store의 mutaions/actions 사용

$store의 commit과 dispatch를 사용하여 변경을 요청한다.

<template>
  <Hello />
  <button @click="increaseCount">
    increaseCount
  </button>
  <button @click="fecthTodo">
    UpdateMSG
  </button>
</template>

<script>

import Hello from '~/components/Hello';
export default {
  
  components:{
    Hello,
  },

  data(){
    return {
      msg: '안녕~'
    };
  },
  methods: {
    increaseCount() {
      this.$store.commit('increaseCount'); // mutaions
    },
    fecthTodo(){
      this.$store.dispatch('fetchTodo'); // actions
    }
    
  }
};
</script>

 

 

Vuex store 모듈화

state의 값이 많아지면 한 곳에서 관리하기가 어렵기 때문에

관련있는 state끼리 나누어서 모듈화를 시킬 수 있다.

 

store 모듈 추가

파일을 추가하고 namespaced를 true로 작성하고 내용을 작성해준다.

 

[store/message.js]

export default {
  namespaced: true, // true를 설정해야 module로써 사용할 수 있다.
  state() {
    return {
      msg: 'Store Module'
    };
  },
  getters: {
    reverseMsg(state){
      return state.msg.split('').reverse().join('');
    }
  },
  mutations : {
    changeMsg(state){
      state.msg = 'Change Message';
    },
    updateMsg(state, newMsg){
      state.msg = newMsg;
    }
  },
  actions: {
    async fetchTodo({commit}){
      const todo = await fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(res=>res.json());
      commit('updateMsg', todo.title);
      // 해당 module 안에 존재하기 때문에 경로 입력 X
    }
  }
};

 

index.js에서 mudule옵션을 추가하여 추가로 생성한 message 입력한다.

 

[store/index.js]

import { createStore } from 'vuex';
import message from './message';

export default createStore({
  state() {},
  getters : {},
  mutations: {},
  actions: {},
  modules:{
    message
    // key와 value가 같기 때문에 합침
  }
});

 

모듈화한 store의 state 다루기

state를 가져올 땐 $store.state.모듈명.데이터명으로 가져올 수 있고

getter을 가져올 땐 대괄호를 사용하여 모듈 명을 작성한다.

actions와 mutations를 가져올 땐 모듈 명과 해당 함수 명을 입력해주면 된다.

<template>
  <h1>{{ storeMessage }}</h1>
  <h1>{{ reversedMsg }}</h1>
  <button @click="changeMsg">
    클릭
  </button>
  <button @click="fecthTodo">
    Get Todo
  </button>
</template>

<script>

export default {
  computed:{
    storeMessage(){
      return this.$store.state.message.msg;
    },
    reversedMsg(){
      return this.$store.getters['message/reverseMsg']; // 일반 괄호가 아니라 []
    }
  },
  methods: {
    changeMsg(){
      this.$store.commit('message/changeMsg');
    },
    fecthTodo(){
      this.$store.dispatch('message/fetchTodo');
      // action을 실행할 때는 dispatch('action이름')
    }
  }
};
</script>

 

솔직히 약간 일관성이 없어서 한번에 습득하기에는 조금 복잡하다고 느껴진다..

그래서인지 vuex에서도 추가 기능을 제공한다.

 

mapping을 통해 코드 개선

vuex에서 제공하는 map함수들로 코드를 좀 더 개선시킬 수 있다.

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';

export default {
  computed:{
    ...mapState('message',[
      'msg'
    ]),
    ...mapGetters('message',[
      'reverseMsg'
    ])
  },
  methods: {
    ...mapMutations('message',[
      'changeMsg']),
    ...mapActions('message',[
      'fetchTodo']),
  }
};
</script>

전역 상태를 가져오려면 map의 첫번째 인자를 생략하면 된다.

 


✍ 느낀 점

 

몇가지 생략하면서 적었음에도 오늘은 내용이 꽤 많다.

아무래도 Vue를 처음 배우다보니 짧은 시간 내에 익혀야 할 것들이 많기도 하고

잘 적용할 수 있을 지는 모르겠다.. 과제 기간은 시작됐지만 과제에 적용할 내용들이

이후 강의들에 나와있어서 이번엔 강의를 잘 들으며 과제를 마칠 수 있을지 걱정이 많이 되지만

일단은 최선을 다해서 해봐야겠다🔥

Comments