IT/언어

[Vue.js] props의 데이터를 변경하는 방법(Avoid mutating a prop directly에러)

개발자 두더지 2023. 2. 19. 21:30
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 부모 컴포넌트에서 props으로 건내 받은 변수를 자식 컴포넌트에서 갱신하고 싶을 때 대처법에 대해서 기록하고자 한다. 

 

인트로


 Vue.js는 부모 컴포넌트에서 전달된 변수를 자식 컴포넌트에서 갱신하려고 하면 아래와 같은 경고가 발생한다.

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "data"

 부모 컴포넌트로부터 전달된 변수를 자식 컴포트에서 갱신하는 코드는 구체적으로 다음의 케이스와 같다.

<!-- ParentView.vue -->

<template>
  <div>
    <NgComponent :data="param1"></NgComponent>
    부모:[{{ param1 }}]
  </div>
</template>

<script>
import NgComponent from './NgComponent.vue'

export default {
  name: 'HelloWorld',
  components: {
    NgComponent,
  },
  data: () => ({
    param1: 1,
  }),
}
</script>
<!-- NgComponent.vue(자식) -->

<template>
  <div>
    <button v-on:click="handle">버튼</button>
    <div>자식:{{ data }}</div>
  </div>
</template>

<script>
export default {
  props: {
    data: Number,
  },
  methods: {
    handle() {
      console.log('NG패턴 start')
      console.log(this.data)
      this.data += 10 // 부모에게 받은 파라미터를 갱신하려고 하면, 여기에서 경고가 발생한다.
      console.log(this.data)
      console.log('NG패턴 end')
    },
  },
}
</script>

 실무라면 "자식 컴포넌트에 전달한 배열에 컴포넌트측이 항상 Firestore에서 가져온 최신의 데이터를 계속해서 업데이트한다"와 같은 케이스가 있을 것이다.

 그럼 위의 경고문이 떴을 때, 자식 컴포넌트에서는 값이 갱신되지만, 부모 컴포넌트쪽에서는 반영되지 않는다. 대처법은 전달 변수의 형을 Object로 한다. 즉 다음과 같이 한다.

<!-- 부모 -->
  
  data: () => ({
    param1: { p1: 1 },
  }),
<!-- 자식 -->

<script>
export default {
  props: {
    data: Object,
  },
  methods: {
    handle() {
      console.log('NG패턴 start')
      console.log(this.data)
      this.data.p1 += 10
      console.log(this.data)
      console.log('NG패턴 end')
    },
  },
}
</script>

 그치만 원래 Vue.js에서는 컴포넌트간에 결합을 약하기 하기 위해 자녀 컴포넌트에서의 값 변경을 권장하고 있지 않다. 따라서 다음과 같이 해야한다.

  •  부모에서 자식 컴포넌트 > props 프로퍼티 경유로 참조를 전달
  • 자식에서 부모 컴포넌트 > $emit이라는 이벤트를 이용해 부모에 통지(그때 변경된 데이터도 전달)

 자녀 컴포넌트에서의 데이터 업데이트를 부모에게 전달하는 방법을 더욱 구체적으로 살펴보도록 하자.

 

 

$emit를 이용한 통지


 부모로부터 받은 변수는 직접 변경할 수 없지만 $emit으로 이용하여 다음과 같이 구현한다.

  • 부모에서 value이라는 속성으로 변수를 전달
  • 자식 컴포넌트쪽에서 computed한 변수(아래의 코드에서는 localParam)을 정의
  • localParam의 getter/setter으로 부모에게 받은 변수를 조작(갱신x)하도록 정의
  • 조작이란 구체적으로는 setter으로 (변경은 안되므로) $emit을 사용하여 input이라는 이름의 이벤트를 발생시켜, 부모에게 변경한 값을 통지
  • 부모는 input 이벤트를 감시하여 거기서 획득한 $event 변수 경유로 변경된 값을 받아 원래의 파라미터에 반영

 코드로 살펴보면 다음과 같다.

<!-- ParentView.vue(부모) -->

<template>
  <div>
    <h2>대책안1(OkComponent1.vue)</h2>
    <OkComponent1 :value="param1" @input="param1 = $event"></OkComponent1>
    [{{ param1 }}]
  </div>
</template>

<script>
import OkComponent1 from './OkComponent1.vue'

export default {
  name: 'HelloWorld',
  components: {
    OkComponent1,
  },
  data: () => ({
    param1: 1,
  }),
}
</script>
<!-- OkComponent1.vue(자식) -->

<template>
  <div>
    <button v-on:click="handle">버튼</button>
    <input type="text" v-model="localParam" />
    <div>{{ localParam }}</div>
  </div>
</template>

<script>
// 값을 여기 컴포넌트에서 바꿔쓸 경우
export default {
  props: {
    value: Number,
  },
  computed: {
    localParam: {
      get: function() {
        return this.value
      },
      set: function(value) {
        this.$emit('input', value) // 부모에서는 @input에 쓴 메소드가 호출된다. 인수에value
      },
    },
  },
  // data: vm => ({}),
  methods: {
    handle() {
      console.log('v-model패턴 start')
      console.log(this.localParam)
      this.localParam += 10 // 실제는 위의 sette가 호출되어 emit된다.
      console.log(this.localParam)
      console.log('v-model패턴  end')
    },
  },
}
</script>

 이렇게 computed로 정의된 변수의 접근자(getter/setter)을 덮어쓰는 것으로 부모로 부터 받은 변수를 자식이 평범하게 조작하는 감각으로 $emit하는 것이 가능하다.

 자식에서의 변경은 input이라는 이벤트로 통지되므로, 부모쪽에서는 그것을 감시하여 값을 받아 다시 해당 변수에 대입하면 된다.

 

v-model을 사용하는 경우

 위 코드 중 부모쪽의 코드 <OkComponent1 :value="param1" @input="param1 = $event"></OkComponent1>를 v-model을 이용하여 <OkComponent1 v-model="param1"></OkComponent1>로 작성할 수 있다.

<template>
  <div>
    <h2>대책안1(의 syntactic sugar버전)(OkComponent1.vue)</h2>
    <OkComponent1 v-model="param1"></OkComponent1>
    [{{ param1 }}]
  </div>
</template>

<script>
import OkComponent1 from './OkComponent1.vue'

export default {
  name: 'HelloWorld',
  components: {
    OkComponent1,
  },
  data: () => ({
    param1: 1,
  }),
}
</script>

 v-model 방식은 value이라는 속성으로 자식에게 변수를 전달하여 input이라는 이벤트명으로 통지되도록한다.

 

여러 개의 변수를 전달하고 싶은 경우

 v-model은 하나의 변수를 value로 전달하는데, 여러 개의 파라이터를 전달하는 경우도 있을 것이라고 생각된다. 그럴 경우에는 .sync 식별자를 이용한다.

<!-- ParentView.vue(부모) -->

<template>
  <div>
    <h2>여러 개의 파라미터를 전달하는 패턴(.sync미사용 패턴)(OkComponent2.vue)</h2>
    <OkComponent2
      :param="param1"
      :paramObj="param2"
      @update:param="param1 = $event"
      @update:paramObj="param2 = $event"
    ></OkComponent2>
    [{{ param1 }}] [{{ param2 }}]
  </div>
</template>

<script>
import OkComponent2 from './OkComponent2.vue'

export default {
  name: 'HelloWorld',
  components: {
    OkComponent2,
  },
  data: () => ({
    param1: 1,
    param2: { param1: 2 },
  }),
}
</script>
<!-- OkComponent2(자식) -->

<template>
  <div>
    <button v-on:click="handle">버튼</button>
    <input type="text" v-model="localParam" />
    <div>{{ localParam }}</div>

    <input type="text" v-model="localParamObj.param1" />
    <div>{{ localParamObj.param1 }}</div>
  </div>
</template>

<script>
// 값을 여기 컴포넌트에서 바꿔쓰는 경우
export default {
  props: {
    param: Number,
    paramObj: Object,
  },
  computed: {
    localParam: {
      get: function() {
        return this.param
      },
      set: function(value) {
        this.$emit('update:param', value)
      },
    },
    localParamObj: {
      get: function() {
        return this.paramObj
      },
      set: function(value) {
        this.$emit('update:paramObj', value)
      },
    },
  },
  // data: vm => ({}),
  methods: {
    handle() {
      console.log('sync를 사용해, 자식에서 값을 변경하는 패턴 start')
      this.localParam -= 1
      this.localParamObj.param1 += 1
      console.log('syn를 사용해, 자식에서 값을 변경하는 패턴 end')
    },
  },
}
</script>

 부모에서는 :param="param1",:paramObj="param2" 로 변수를 전달한다. 자식쪽에서는 각각의 변수에 대해서 computed한 변수를 정의하여 값을 조작한다. 부모에 통지하는 이벤트는 다음과 같이 한다.

this.$emit('update:param', value)
this.$emit('update:paramObj', value)

 이번의 이벤트는 input이 아닌 update이다.

 

syntactic sugar판

<OkComponent2
  :param="param1"
  :paramObj="param2"
  @update:param="param1 = $event"
  @update:paramObj="param2 = $event"
></OkComponent2>

 이 부분의 코드는 .sync이라는 간단한 표기법으로 기재할 수 있다. 그 방법을 사용하면 다음과 같이 짧아진다.

<OkComponent2 :param.sync="param1" :paramObj.sync="param2"></OkComponent2>

참고자료

https://qiita.com/masatomix/items/ab4f0488083554f5fceb

728x90