본문 바로가기
Programming Language 이해하기/Javascript 이해하기

[Vue]왜 mutation 에서는 왜 비동기(async wait 문법)를 사용하면 안될까? (수정: 되긴 된다)

by simplify-len 2020. 12. 26.

Photo by  Jon Tyson  on  Unsplash

 

이번에 Vue 프로젝트를 진행하면서 mutations의 남용으로 Vuex에 대한 이해를 도왔습니다.

그 부분을 조금 더 자세히 기입합니다.

문제 발생

    mutations: {
		...
        async getUserInfos(state, loginId) {
            const basicUserInfo = await UserInfoApi.getUserInfoById(loginId);
            state.userinfo.userId = basicUserInfo.data.userId
            state.userinfo.userLoginId = basicUserInfo.data.userLoginId
            state.userinfo.userName = basicUserInfo.data.userName
            state.userinfo.dutyName = basicUserInfo.data.dutyName
            state.userinfo.position = basicUserInfo.data.position
            state.userinfo.deptId = basicUserInfo.data.deptId
            state.userinfo.deptPath = basicUserInfo.data.deptPath
            state.userinfo.deptName = basicUserInfo.data.deptName
            state.userinfo.thumb = basicUserInfo.data.thumb
            const newVar = {
                deptId: basicUserInfo.data.deptId,
                deptName: basicUserInfo.data.deptName
            };
            basicUserInfo.data.departments.unshift(newVar);
            state.userinfo.dept = basicUserInfo.data.departments;
            const startDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(0).format("YYYY-MM-DD");
            const endDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(6).format("YYYY-MM-DD");
            state.dateRangeValue.startDate = startDate
            state.dateRangeValue.endDate = endDate
            const param = {
                userId: state.userinfo.userId,
                startDate: startDate,
                endDate: endDate,
            }

            const homeUserUsageResponse = await UserInfoApi.getHomeUserUsage(param);
            const homeUserUsageData = homeUserUsageResponse.data
            state.userinfo.avgRate = homeUserUsageData.avgRate
            state.userinfo.rank = homeUserUsageData.rank
            state.userinfo.memCount = homeUserUsageData.memCount

            const homeApprovalRankResponse = await UserInfoApi.getHomeApprovalRankBy(param);
            state.userApprovalInfo = homeApprovalRankResponse.data

            return state.userinfo
        },
        ...
getters: {
		...
        currentDeptName: function (state) {
            if (state.userinfo.deptId === '') {
                return
            }
            const deptId = state.userinfo.deptId;
            const filter = state.userinfo.dept.filter(a => {
                return a.deptId === deptId
            });
            return filter[0].deptName
        }
        ...
    },

mutations 에서 왜 비동기를 쓰면 안될까?

처음 위와같이 mutations에 비동기 코드를 넣고, 동작시켰을 경우- 문제없이 동작되는 것처럼 보였습니다. 그러나 localstorage에 저장된 vuex를 지우고, 다시 새로 고침을 할 경우 아래와 같은 에러가 발생했습니다.

 위 코드에서 getters의 currentDetpName 함수는 mutations에서 호출된 Commit 결과 값을 가져와야 했습니다. 그러나, 그러지 못하여 아래와 같은 이슈가 발생했습니다.

그림 1 - 예외 발생 상황

왜 그럴까요?

조금더 문제의 원인을 찾기 위해서는 호출되는 순서를 명확히 이해했을 때 명확해졌습니다.

일단 vuex에 등장인물부터 살펴봅시다.

state

> 여러 컴포넌트 간에 공유할 데이터로서 상태를 표현합니다. 해당 state의 값이 변경되면 계산된 속성이 변경되고 이는 곧 DOM 업데이트로서 트리거가 됩니다.

getter

> state 값을 접근하는 속성이자 computed() 처럼 미리 연산된 값을 접근하는 속성입니다. 첫번째 인자값으로 state를 받습니다. 두번째 인자로는 getter를 받을 수 있습니다.
실행된 결과물은 캐시되지 않으므로 유의해야 합니다.

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
     doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
    }
  }
  
})

 

mutations

> state의 값을 변경할 수 있는 유일한 방법이자 메서드입니다. mutations은 commit() 으로 동작시킵니다. mutations은 이벤트와 유사하며, 첫번째 전달인자로 state를 받습니다. 두번째 인자로는 이벤로 넘길 payload로 추가 전달인자를 사용할 수 있습니다.

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

---

store.commit('increment', {
  amount: 10
})

 여기서 궁금한점은 왜 state를 직접 변경하지 않고, mutations로 변경해야 되는걸까?

더보기

여러 개의 컴포넌트에서 아래와 같이 state 값을 변경하는 경우 어느 컴포넌트에서 해당 state를 변경했는지 추적하기가 어렵습니다. 또한 특정 시점에 어떤 컴포넌트가 state를 접근하여 변경한 건지 확인하기 어렵기 때문입니다. 따라서, 뷰의 반응성을 거스르지 않게 명시적으로 상태 변화를 수행합니다. 

methods: {
	increaseCounter() {this.$store.state.counter++; }
}

action

비동기 처리 로직을 선언하는 메서드. 비동기 로직을 담당하는 mutations로서, 데이터 요청, Promise, ES6 async과 같은 비동기 처리는 모두 actions에 선언해야 합니다.


다시 처음 문제로 돌아가보겠습니다.

이번에는 로그를 남겨 호출되는 순서를 조금더 명확히 해봅시다.

getters:{
        currentDeptName: function (state) {
            console.log("getters > currentDeptName start")
            const deptId = state.userinfo.deptId;
            const filter = state.userinfo.dept.filter(a => {
                return a.deptId === deptId
            });
            console.log("getters > currentDeptName end")
            return filter[0].deptName
        }
    },
    mutations: {
		...
        async getUserInfos(state, loginId) {
            console.log("mutations > getUserInfos start")
            const basicUserInfo = await UserInfoApi.getUserInfoById(loginId);
            state.userinfo.userId = basicUserInfo.data.userId
            state.userinfo.userLoginId = basicUserInfo.data.userLoginId
            state.userinfo.userName = basicUserInfo.data.userName
            state.userinfo.dutyName = basicUserInfo.data.dutyName
            state.userinfo.position = basicUserInfo.data.position
            state.userinfo.deptId = basicUserInfo.data.deptId
            state.userinfo.deptPath = basicUserInfo.data.deptPath
            state.userinfo.deptName = basicUserInfo.data.deptName
            state.userinfo.thumb = basicUserInfo.data.thumb
            const newVar = {
                deptId: basicUserInfo.data.deptId,
                deptName: basicUserInfo.data.deptName
            };
            basicUserInfo.data.departments.unshift(newVar);
            state.userinfo.dept = basicUserInfo.data.departments;
            const startDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(0).format("YYYY-MM-DD");
            const endDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(6).format("YYYY-MM-DD");
            state.dateRangeValue.startDate = startDate
            state.dateRangeValue.endDate = endDate
            const param = {
                userId: state.userinfo.userId,
                startDate: startDate,
                endDate: endDate,
            }

            const homeUserUsageResponse = await UserInfoApi.getHomeUserUsage(param);
            const homeUserUsageData = homeUserUsageResponse.data
            state.userinfo.avgRate = homeUserUsageData.avgRate
            state.userinfo.rank = homeUserUsageData.rank
            state.userinfo.memCount = homeUserUsageData.memCount
            console.log("mutations > getUserInfos end")
            return state.userinfo
        },
        ...
}
methods: {
    getUserInfos(loginId){
      console.log("methods > getUserInfos start")
      this.$store.commit('getUserInfos', loginId);
      this.$router.push({name: "Dashboard"})
      console.log("methods > getUserInfos end")
    },
  }

호출 되는 순서는 아래 그림과 같이 나옵니다.

이 순서의 의미는 methods 영역에서 mutations 의 이벤트를 commit 할 경우, 아직 mutations의 commit이 끝나지 않은 시점에서 method 이벤트가 끝났다는 것을 알 수 있습니다.

그러므로, state의 값이 적절하게 변경되지 않은 상태에서 getters에서 state의 값을 읽어들이기 위한 행위를 시도하다 보니, 이슈가 발생합니다.

왜 그럴까요? 바로 Http 요청이 mutations 안에서 언제 응답을 받을지 모릅니다. 그러므로 vuex의 mutations 가이드에는 mutations는 무조건 동기적이여야 한다고 말하고 있습니다. 실제로 commit이 되었을 때 http 요청의 골백은 아직 호출되지 않았으며, 실제로 콜백이 호출 될 시기는 더욱 알 수 없습니다. 

그러므로 비동기적은 콜백의 경우에는 actions을 사용하라고 합니다. 그렇다면 액션을 사용했을 때는 어떤 순서로 동작 되는지 살펴봅시다.

위 mutations에 있던 코드를 쪼개고 쪼개서 actions에 필요한 부분을 넣고, state를 변경시키는 부분만 명확히 분리했습니다.

getters: {
		...
        currentDeptName: function (state) {
            console.log("getters > currentDeptName start")
            if (state.userinfo.deptId === '') {
                return
            }
            const deptId = state.userinfo.deptId;
            const filter = state.userinfo.dept.filter(a => {
                return a.deptId === deptId
            });
            console.log("getters > currentDeptName end")
            return filter[0].deptName
        }
        ...
    },
mutations: {
		...
        setUserInfo: (state, payload) => {
            console.log("mutations > setUserInfo start")
            state.userinfo.userId = payload.data.userId
            state.userinfo.userLoginId = payload.data.userLoginId
            state.userinfo.userName = payload.data.userName
            state.userinfo.dutyName = payload.data.dutyName
            state.userinfo.position = payload.data.position
            state.userinfo.deptId = payload.data.deptId
            state.userinfo.deptPath = payload.data.deptPath
            state.userinfo.deptName = payload.data.deptName
            state.userinfo.thumb = payload.data.thumb
            const newVar = {
                deptId: payload.data.deptId,
                deptName: payload.data.deptName
            };
            payload.data.departments.unshift(newVar);
            state.userinfo.dept = payload.data.departments;

            const startDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(0).format("YYYY-MM-DD");
            const endDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(6).format("YYYY-MM-DD");
            state.dateRangeValue.startDate = startDate
            state.dateRangeValue.endDate = endDate
            console.log("mutations > setUserInfo end")
        },
    },
actions: {
		...
		async getUserInfos({commit, state}, loginId) {
            console.log("actions > getUserInfos start")
            const basicUserInfo = await UserInfoApi.getUserInfoById(loginId);
            commit('setUserInfo', basicUserInfo)
            const startDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(0).format("YYYY-MM-DD");
            const endDate = moment(moment().format('YYYY-MM-DD'), "YYYY-MM-DD").day(6).format("YYYY-MM-DD");
            const param = {
                userId: state.userinfo.userId,
                startDate: startDate,
                endDate: endDate,
            }
            const homeUserUsageResponse = await UserInfoApi.getHomeUserUsage(param);
            commit('setHomeUserUsage', homeUserUsageResponse)
            console.log("actions > getUserInfos end")
            return state.userinfo
        },
        ...
    }
methods: {
    getUserInfos(loginId){
      console.log("methods > getUserInfos start")
      this.$store.dispatch('getUserInfos', loginId);
      this.$router.push({name: "Dashboard"})
      console.log("methods > getUserInfos end")
    },

그림 2 actions을 잘 사용할 때


결론

발생된 이슈를 정리하다가 이 포스트 자체가 잘못된 정답으로 이끌었다는 결론에 도달했습니다. getter 코드 안에

            if (state.userinfo.deptId === '') {
                return
            }

 이 부분 때문에 잘못된 결과를 낼 뻔했습니다.

 저는 마치 actions를 활용하면 getters 함수에 actions으로부터 불려진 API call 이 모두 다 호출되어 commit까지 끝나 뒤, state가 모두 다 채워지고 난 뒤에 getter가 되는 줄 알았습니다. actions이 필요한 이유는 사실 이것이 아니라, mutations의 commit이 호출되는 시점 때문이라는 사실을 뒤늦게 알았습니다.

시간 차를 두고 mutations의 같은 함수를 여러 컴포넌트에 commit 하여 state를 변경할 경우 이를 추적하기 어렵기 때문에 actions에 두는 것이다 라는 것이 결론입니다.

그렇다면 getter에 처음부터 호출 될 때 빈값을 넘겨주게 할 수 없는 방법은 없을까???

 

... 어떻게 보면 실패한 포스팅이지만, 그래도 흔적으로 남겨두자..

[참고자료]

github.com/LenKIM/vue-study/blob/master/Vuex.md

vuex.vuejs.org/kr/guide/mutations.html

댓글