[Vue] 컴포넌트 간 통신 - 반응형 데이터 (toRef, toRefs)

 



 이번 포스팅에서는 Vue3에서의 반응형 데이터(ref, reactive)를 다루면서, 컴포넌트 간 반응형을 유지하고

통신하는 방법에 대해 다룹니다.



1. 예시 설명

  부모, 자식, 손자 순서로 세 단계로 전파되는 입력 값을 제시하고, 각 위치에서 변경된 값이 연관된 부모 혹은 자식 컴포넌트에 전달되는 환경을 구성합니다.



2. 자식 컴포넌트에서 반응성이 소실되는 경우 (Props)

 아래는 자식 컴포넌트에서 부모의 반응형 데이터 (originData)를 props로 받고 변수에 다시 할당하면서, 반응성을 상실하게 되는 예제 입니다. 
 
[Parents.vue]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
  <section>
    <h1># 컴포넌트 간 통신(반응형) 예제 실습</h1>
  </section>
  <section>
    <div class="parents-content">
      <h2>Parents</h2>
      <div>
        <!-- 현재 컴포넌트에서 입력받을 값 -->
        <span>Input : </span>
        <input type="text" v-model="originData" />
      </div>
      <div>
        <!-- 자식에게 전달할 값 -->
        <FirstChildren :originData="originData"> </FirstChildren>
      </div>
    </div>
  </section>
</template>

<script setup lang="ts">
import FirstChildren from '@/components/FirstChildren.vue'
import { ref } from 'vue'

const originData = ref('test value')
</script>

  • originData : 반응형(ref) 데이터, v-model로 input 태그에 바인딩
  • 자식 컴포넌트에 props로 전달

[Child.vue]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
  <div class="child-content">
    <h2>Child</h2>
    <section class="container">
      <div class="item">
        <!-- 부모에게 전달받은 원본 -->
        <span>1) Origin Data : </span>
        <div>{{ props.originData }}</div>
      </div>
      <div class="item">
        <!-- 현재 컴포넌트에서 입력받을 값 -->
        <span>2) My Data : </span>
        <input type="text" v-model="myData" />
      </div>
    </section>
    <div>
      <!-- 자식에게 전달할 값 -->
      <GrandChild :originData="myData"></GrandChild>
    </div>
  </div>
</template>

<script setup lang="ts">
import GrandChild from '@/components/GrandChild.vue'

const props = defineProps<{
  originData: any
}>()

const myData = props.originData
</script>

  • props.originData : 부모로 부터 전달받은(props) 원본 값
  • myData : 부모에서 받은 값을 기반으로, 새로 다뤄질 변수


[반응성이 상실된 예제]

: 초기 값은 부모로 부터 전달 받지만, myData에서 'props.originData'를 직접 할당 받으므로 반응성이 소실됩니다. 
자식의 props는 여전히 반응성을 유지하여, Child의 "Origin Data" 필드는 변경되지만 "My Data" 영역은 바뀌지 않는 현상을 볼 수 있습니다.



3. 소실된 반응성을 유지하는 방법 (toRef, toRefs)

 앞 선 예시의 "myData"를 "props.originData"를 바라보면서 반응성을 유지하는 몇 가지 방법입니다.

[Child.vue]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
  <div class="child-content">
    <h2>Child</h2>
    <section class="container">
      <div class="item">
        <!-- 부모에게 전달받은 원본 -->
        <span>1) Origin Data : </span>
        <div>{{ props.originData }}</div>
      </div>
      <div class="item">
        <!-- 현재 컴포넌트에서 입력받을 값 -->
        <span>2) My Data (toRef): </span>
        <input type="text" v-model="myData" />
      </div>
      <div class="item">
        <!-- 현재 컴포넌트에서 입력받을 값 -->
        <span>3) My Data (toRefs): </span>
        <input type="text" v-model="originData" />
      </div>
    </section>
    <div>
      <!-- 자식에게 전달할 값 -->
      <GrandChild :originData="myData"></GrandChild>
    </div>
  </div>
</template>

<script setup lang="ts">
import GrandChild from '@/components/GrandChild.vue'
import { toRef, toRefs } from 'vue'

const props = defineProps<{
  originData: any
}>()

const myData = toRef(props, 'originData')
const { originData } = toRefs(props)
</script>

  • (Line::36) toRef : 객체(props) 내 특정 속성(originData)의 반응성 참조를 반환
  • (Line::37) toRefs : 객체(props)의 모든 속성의 반응성 참조를 반환. 구조분해할당으로 "originData"만을 취급함.


[반응성이 유지되는 예제]




4. 양방향 바인딩 구현 (Computed)

자식 컴포넌트에서 부모의 반응형 데이터를 양방향 바인딩과 같은 동작을 기대할 수는 없습니다.
Vue는 반응형 유무 뿐만 아니라, 대체적으로 (부모 → 자식) 방향으로의 단방향 바인딩을 지향하기 때문입니다.
따라서, 위 예제에서 input 태그에 v-model로 바인딩된 값을 변경하려할 경우 아래와 같은 경고가 발생합니다.
(자식 컴포넌트에서 할당한 myData도 동일)

 : Set operation on key ~ failed : target is readonly

이 경우, 아래와 같이 computed로 부모의 반응형 데이터 (originData)를 참조하고, 변경 시 emit으로 부모의 원본 값을 변경하는 방향으로 구현합니다.

[Parents.vue]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
  <section>
    <h1># 컴포넌트 간 통신(반응형) 예제 실습</h1>
  </section>
  <section>
    <div class="parents-content">
      <h2>Parents</h2>
      <div>
        <!-- 현재 컴포넌트에서 입력받을 값 -->
        <span>Input : </span>
        <input type="text" v-model="originData" />
      </div>
      <div>
        <!-- 자식에게 전달할 값 -->
        <FirstChildren
          :originData="originData"
          @update:originData="
            (newValue) => {
              originData = newValue
            }
          "
        ></FirstChildren>
      </div>
    </div>
  </section>
</template>

<script setup lang="ts">
import FirstChildren from '@/components/FirstChildren.vue'
import { ref } from 'vue'

const originData = ref('test value')
</script>
  • (Line::17) 원본(originData)를 변경하도록 emit(update)를 정의

[Child.vue]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
  <div class="child-content">
    <h2>Child</h2>
    <section class="container">
      <div class="item">
        <!-- 부모에게 전달받은 원본 -->
        <span>1) Origin Data : </span>
        <div>{{ props.originData }}</div>
      </div>
      <div class="item">
        <!-- 현재 컴포넌트에서 입력받을 값 -->
        <span>2) My Data : </span>
        <input type="text" v-model="myData" />
      </div>
    </section>
    <div>
      <!-- 자식에게 전달할 값 -->
      <GrandChild :originData="myData"></GrandChild>
    </div>
  </div>
</template>

<script setup lang="ts">
import GrandChild from '@/components/GrandChild.vue'
import { computed } from 'vue'

const props = defineProps<{
  originData: any
}>()

const emit = defineEmits(['update:originData'])

const myData = computed({
  get: () => props.originData,
  set: (newValue) => {
    emit('update:originData', newValue)
  }
})
</script>
  • (Line :: 34) computed 객체의 get을 통해 반응성 객체를 참조로 값을 획득함.
  • (Line :: 35) myData의 변경이 있는 경우, emit 이벤트를 발생시켜 부모의 원본 데이터 update를 발생시킴.

0 댓글