Dandy Now!
  • [개발자의품격][부트캠프][1기][23차시] Vue.js #16 | Vuex
    2022년 03월 19일 03시 26분 04초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    Vuex

    https://vuex.vuejs.org/

    vuex 라이브러리를 반드시 써야 하는 것은 아니다. (한 번도 안 쓰는 프로젝트도 있다.)

    Vue 프로젝트 생성 시 Vuex를 사용하는 것으로 선택하면 src 폴더 내에 store폴더와 index.js 파일이 자동으로 생성되고 main.js에 store 폴더가 import 된다.

    Vuex는 상태를 관리하기 위한 패턴 라이브러리이다. 애플리케이션 내 모든 컴포넌트의 중앙 집중식 데이터 저장소로서 역할을 한다.

    각 컴포넌트에는 그 컴포넌트에서만 사용할 수 있는 data()를 정의할 수 있다. 이때 어떤 데이터는 다른 컴포넌트들이 그 데이터에 빈번하게 접근해야 하는 경우가 있다. 이런 데이터는 중앙에서 관리하는 것이 효율적이다. Vuex는 이런 데이터를 효율적으로 관리한다. 예를 들어 로그인 정보의 경우 각 컴포넌트는 로그인된 사용자 데이터를 공유하여 알맞은 화면을 보여주어야 한다. 

    상태는 예측 가능한 방식(규칙)으로만 변경될 수 있고 변경된 경우에는 알려준다.

    Vuex는 사용자 정보, 장바구니 데이터, Todo 리스트 등에서 사용하며, 모든 컴포넌트에서 접근이 가능하다.

    Vuex는 컴포넌트에서 data()라고 쓰지 않고 상태 관리를 강조하기 위해 state()라고 쓴다.

    // src/store/index.js
    import { createStore } from 'vuex'
    
    export default createStore({
      state() {
        return {
        
        }
      },

     

    Vuex를 이용한 Todo list 실습

    할 일 목록 데이터(todos)를 state에 넣는다.

    // src/store/index.js
    import { createStore } from 'vuex'
    
    export default createStore({
      state() {
        return {
          todos: [
            { id: 1, title: 'todo 1', done: true },
            { id: 2, title: 'todo 2', done: false },
            { id: 3, title: 'todo 3', done: false }
          ]
        }
      },

     

    mutations 

    mutations 함수에 todos 데이터 변경을 위한 함수를 선언한다. state의 변경은 mutations에서만 가능하다. 변경이 일어나면 state의 데이터를 사용하고 있는 다른 컴포넌트에 변경사항을 알려준다.

    // src/store/index.js
    import { createStore } from 'vuex'
    
    export default createStore({
      ...
      mutations: {
        // 함수를 정의할 때 첫 번째 파라미터는 반드시 state이다.
        add(state, item){
          state.todos.push(item)
        },
        done(state, id){
          state.todos.filter(todo => todo.id === id)[0].done = true
        }
      },

     

    actions

    actions 함수는 states 내 데이터를 수정할 수 있다는 점에서 mutations 함수와 매우 유사하다. 이때 직접 수정할 수도 있지만 일반적으로는 mutations를 통해서 수정한다. mutations와의 가장 큰 차이점은 actions가 비동기 처리를 할 수 있다는 점이다. 데이터의 영속성을 위해서는 DB에 저장을 해야 하는데 mutations에서는 할 수 없는 일이다. 이런 경우 비동기 처리가 가능한 actions에 함수를 정의해 사용한다.

    // src/store/index.js
    import { createStore } from 'vuex'
    
    export default createStore({
      ...
      // 비동기 처리 가능
      actions: {
        add: ({ commit }, item) => {
          // 서버에 new todo 저장 후 성공하면 commit함수를 통해 mutations의 해당 함수를 호출해 state의 데이터를 변경한다.
          commit('add', item) // 'add'는 mutations 내 함수 이름, item은 전달할 파라미터
        }
      },

     

    setTimeout() 함수로 1초 뒤 commit() 함수를 호출하는 비동기 처리를 해보겠다.

    // src/store/index.js
    import { createStore } from 'vuex'
    
    export default createStore({
      ...
      // 비동기 처리 가능
      actions: {
        add: ({ commit }, item) => {
          // 서버에 new todo 저장
          setTimeout(() => {
            commit('add', item)
          }, 1000)
        },
        done: ({ commit }, item) => {
          // 서버에 할일 처리여부 done 저장
          setTimeout(() => {
            commit('done', item)
          }, 1000)
        }
      ...

     

    TodoView.vue에 아래 코드를 추가하면 [그림 1]과 같이 state의 todos 데이터가 조회된다.

    // src/views/6_vuex/TodoView.vue
    <template>
      <div>
        <div>{{ $store.state.todos }}</div>
      </div>

     

    [그림 1] state의 todos 데이터 조회

     

    "추가" 버튼과 item을 추가하는 addItem() 함수를 선언한다. "추가" 버튼을 클릭하면 "id:4" item이 추가된다. 영속적인 추가가 아니기 때문에 새로고침 하면 값이 사라진다.

    <!-- src/views/6_vuex/TodoView.vue -->
    <template>
      <div>
        ...
        <button @click="addItem">추가</button>
      </div>
    </template>
    <script>
    export default {
      ...
      methods: {
        addItem() {
          // commit함수를 통해서만 mutations의 함수를 호출할 수 있다.
          this.$store.commit('add', { id: 4, title: 'todo 4', done: false })
        }
      }
    }
    </script>

     

    [그림 2] "추가" 버튼을 클릭하면 "id:4" item이 추가된다.

     

    computed

    computed를 이용하여 $store.state.todos를 todos라는 새로운 데이터를 만들었다. 실무에서는 코드 길이를 짧게 사용하기 위해 이렇게 새롭게 정의하여 사용한다.

    <!-- src/views/6_vuex/TodoView.vue -->
    <template>
      <div>
        <!-- <div>{{ $store.state.todos }}</div> -->
        <div>{{ todos }}</div>
        ...
    <script>
    export default {
      ...
      // computed는 다른 데이터를 참조하여 데이터에 정의되지 않은 새로운 데이터를 만든다.
      computed: {
        todos() {
          return this.$store.state.todos
        }
      },
      ...

     

    addItem() 함수는 mutations에 정의된 함수를 호출하고, addItem2() 함수는 actions에 정의된 함수를 호출하도록 코딩한다. 해당 버튼도 추가한다. addItem2() 함수는 actions의 비동기 함수를 호출하여 설정된 1초 후에 값이 표시된다. [그림 3]은 "추가(actions)" 버튼 클릭 후 "추가(mutations)" 버튼 클릭하였으나 mutations가 actions 보다 먼저 표시되었다.

    <!-- src/views/6_vuex/TodoView.vue -->
    <template>
      <div>
        ...
        <button @click="addItem">추가(mutations)</button>
        <button @click="addItem2">추가(actions)</button>
      </div>
    </template>
    <script>
    export default {
      ...
      methods: {
        addItem() {
          // mutations에 정의된 함수를 호출할 때는 commit 사용
          this.$store.commit('add', { id: 4, title: 'mutations', done: false })
        },
        addItem2() {
          // actions에 정의된 함수 호출할 때는 dispatch 사용
          this.$store.dispatch('add', { id: 5, title: 'actions', done: false })
        }
      }
    }
    </script>

     

    [그림 3] "추가(actions)" 버튼 클릭 후 "추가(mutations)" 버튼 클릭 결과

     

    getters

    getters를 이용하면 항목수를 편리하게 간단하게 표시할 수 있다. getters에 정의된 함수의 값은 vuex의 데이터가 변경될 때 자동으로 반영된다.

    // src/store/index.js
    import { createStore } from 'vuex'
    
    export default createStore({
      ...
      getters: {
        todosCount(state) {
          return state.todos.length
        },
        doneTodosCount(state) {
          return state.todos.filter((todo) => todo.done).length
        },
        notDoneTodosCount(state) {
          return state.todos.filter((todo) => !todo.done).length
        }
      },
      ...

     

    TodoView.vue 컴포넌트에 "전체 항목수, 완료된 항목수, 미완료 항목수"를 각각 출력되게 한다. <template>에 동적으로 출력되는 값들은 coputed에서 새롭게 정의된 데이터를 받아오는 것이다.

    <!-- src/views/6_vuex/TodoView.vue -->
    <template>
      <div>
        ...
        <div>전체 항목수: {{ todosCount }}</div>
        <div>완료된 항목수: {{ doneTodosCount }}</div>
        <div>미완료 항목수: {{ notDoneTodosCount }}</div>
        ...
      </div>
    </template>
    <script>
    export default {
      ...
      computed: {
        ...
        todosCount() {
          return this.$store.getters.todosCount
        },
        doneTodosCount() {
          return this.$store.getters.doneTodosCount
        },
        notDoneTodosCount() {
          return this.$store.getters.notDoneTodosCount
        }
      },
      ...

     

    [그림 4] getters 이용한 항목수 출력

     

    modules

    module 방식은 가독성이 좋고 협업에서도 유리하기 때문에 실무에서 주로 사용한다. store 폴더에서 todo.js를 생성하고 index.js에서 state() 함수가 포함된 오브젝트를 export const todo에 넣는다.

    // src/store/todo.js
    export const todo = {
      state() {
        return {
          todos: [
            { id: 1, title: 'todo 1', done: true },
            { id: 2, title: 'todo 2', done: false },
            { id: 3, title: 'todo 3', done: false }
          ]
        }
      },
      getters: {
        todosCount(state) {
          return state.todos.length
        },
        doneTodosCount(state) {
          return state.todos.filter((todo) => todo.done).length
        },
        notDoneTodosCount(state) {
          return state.todos.filter((todo) => !todo.done).length
        }
      },
      mutations: {
        // 함수를 정의할 때 첫 번째 파라미터는 반드시 state이다.
        add(state, item) {
          state.todos.push(item)
        },
        done(state, id) {
          state.todos.filter((todo) => todo.id === id)[0].done = true
        }
      },
      // 비동기 처리 가능
      actions: {
        add: ({ commit }, item) => {
          // 서버에 new todo 저장
          setTimeout(() => {
            commit('add', item)
          }, 1000)
        },
        done: ({ commit }, item) => {
          // 서버에 할일 처리여부 done 저장
          setTimeout(() => {
            commit('done', item)
          }, 1000)
        }
      }
    }

     

    index.js에 앞서 만든 todo.js를 import 하고, modules에 todo를 추가한다. 그러면 [그럼 5]와 같이 출력되는데 state 데이터가 보이지 않는다.

    // src/store/index.js
    ...
    import { todo } from './todo'
    
    export default createStore({
      modules: {
        todo // {todo:todo}
      }
    })

     

    [그림 5] modules 처리 직후 출력 결과. 데이터가 보이지 않는다.

     

    modules 사용법

    state 데이터에 접근하기 위해 todo.js에 namespaced를 추가한다.

    // src/store/todo.js
    export const todo = {
      namespaced: true,
      ...

     

    TodoView.vue 컴포넌트의 computed의 코드를 수정하면 [그림 4]와 동일하게 출력된다. 하지만 "추가" 버튼은 작동하지 않는다.

    // src/views/6_vuex/TodoView.vue
    ...
    <script>
    export default {
      ...
      computed: {
        todos() {
          // 데이터 가져올 때
          return this.$store.state.todo.todos
        },
        todosCount() {
          // return 값 가져올 때
          return this.$store.getters['todo/todosCount']
        },
        doneTodosCount() {
          return this.$store.getters['todo/doneTodosCount']
        },
        notDoneTodosCount() {
          return this.$store.getters['todo/notDoneTodosCount']
        }
      },

     

    TodoView.vue 컴포넌트의 methods의 addItem(), addItem2() 함수의 'add'를 'todo/add'로 수정하면 "추가" 버튼이 작동한다. module의 키 todo로 접근하는 것이다.

    // src/views/6_vuex/TodoView.vue
    ...
    <script>
    export default {
      ...
      methods: {
        addItem() {
          // mutations에 정의된 함수를 호출할 때는 commit 사용
          this.$store.commit('todo/add', { id: 4, title: 'mutations', done: false })
        },
        addItem2() {
          // actions에 정의된 함수 호출할 때는 dispatch 사용
          this.$store.dispatch('todo/add', { id: 5, title: 'actions', done: false })
        }
      ...

     

    login 기능 추가

    user module을 추가하기 위해 store 폴더에 user.js를 생성한다.

    // src/store/user.js
    export const user = {
      namespaced: true,
      state() {
        return {
          userInfo: {
            name: 'Sewol',
            email: 'sewol@gmail.com',
            tel: '010-0000-0000'
          }
        }
      },
      getters: {},
      mutations: {},
      actions: {}
    }

     

    index.js에 user.js를 import 하고 module을 추가한다.

    // src/store/index.js
    ...
    import { user } from './user' // 추가
    
    export default createStore({
      modules: {
        todo: todo,
        user: user // 추가
      }
    })

     

    TodoView.vue 컴포넌트에 사용자 이름을 추가한다.

    // src/views/6_vuex/TodoView.vue
    <template>
      <div>
        ...
        <div>사용자 이름: {{ userInfo.name }}</div>
      </div>
      ...
    <script>
    export default {
      ...
      computed: {
        ...
        userInfo() {
          return this.$store.state.user.userInfo
        }

     

    [그림 6] "사용자 이름"이 추가되었다.

     

    user.js의 state() 함수 내 userInfo를 빈 오브젝트로 만든다. mutations에 setUser() 함수를 생성한다.

    // src/store/user.js
    export const user = {
      namespaced: true,
      state() {
        return {
          userInfo: {} // userInfo 내 데이터 삭제
        }
      },
      ...
      mutations: {
        setUser(state, userInfo) {
          state.userInfo = userInfo
        }
      },
      ...
    }

     

    TodoView.vue 컴포넌트에 "로그인" 버튼을 만든다. 로그인 버튼을 누르면 login() 함수를 호출한다.

    // src/views/6_vuex/TodoView.vue
    <template>
      <div>
        ...
        <div><button @click="login">로그인</button></div>
        <div v-if="userInfo.name">{{ userInfo.name }}님 환영합니다.</div>
      </div>
    </template>
    <script>
    export default {
      ...
      methods: {
        ...
        login() {
          this.$store.commit('user/setUser', {
            name: 'Sewol',
            email: 'sewol@gmail.com'
          })
        }
      }
    }
    </script>

     

    [그림 7] "로그인" 버튼을 클릭하면 환영 메시지가 출력된다.

     

    로그인된 경우에만 페이지 보이기

    router폴더의 index.js에 아래의 코드를 추가한다. beforeEach는 경로 이동 직전에 수행하는 매서드로 사용자가 어떤 메뉴에서 어디로 이동하는지 캐치할 수 있다. 실무에서는 로그인된 사용자에게만 페이지를 보여줘야 하는 경우를 위한 경로 관리에 사용한다. 아래 코드는 로그인이 된 경우에는 next() 함수를 실행하고 그렇지 않으면 로그인 화면으로 redirect 시킨다. 이 기능으로 navigation control을 할 수 있다.

    // src/router/index.js
    ...
    router.beforeEach((to, from, next) => {
      console.log('to', to)
      console.log('from', from)
    
      next()
    })
    
    ...

     

    user.js의 getters에 isLogin() 함수를 선언한다. userInfo.name 값이 있으면(로그인된 경우) true, 없으면 false를 반환한다.

    // src/store/user.js
    export const user = {
      ...
      getters: {
        isLogin(state) {
          if (state.userInfo.name) {
            return true
          } else {
            return false
          }
        }
      },
      ...

     

    router의 index.js에서 로그인 정보를 사용하기 위해 store를 import 하고 router.beforeEach() 함수를 아래와 같이 수정한다.

    // src/router/index.js
    ...
    import store from '../store'
    ...
    router.beforeEach((to, from, next) => {
      // 홈화면
      if (to.path === '/') {
        next()
        // 로그인 화면
      } else if (to.path === '/vuex/todo') {
        next()
      } else {
        // 로그인된 경우(store.getters['user/isLogin']이 true인 경우)
        if (store.getters['user/isLogin']) {
          next()
          // 로그인이 안된 경우, 로그인 화면으로 Redirect
        } else {
          next('/vuex/todo')
        }
      }
    })

     

    [그림 8] 로그인된 상태에서만 About 페이지를 열수 있다.

     

    728x90
    반응형
    댓글