프레임워크.라이브러리/vue

Vue 컴포넌트 데이터 연동 정리

richready2011 2022. 8. 25. 13:54

SPA (Single Page Application) 프로젝트 에서 컴포넌트로 구조화 하는 방법은 핵심 장점으로 알려져 있고, 코드 재사용을 위해서 대체적으로 사용하는 추세이다.

SPA (Single Page Application) 프로젝트라 하더라도, 반드시 컴포넌트 구조로 개발 해야하는 것은 아니다.

이전 SSR(Server Side Rendering) 때의 방식처럼, 현재 페이지에 모든데이터를 한번에 전달해서 처리하는 방법으로 해도 상관없다.

단지 Restful API 작성함에 Server Less 방식과 맞지 않고, 다양한 frontend framework 들이 컴포넌트 단위에 작성방식을 요구하고, 전수하다 보니, 현재 상황은 컴포넌트 단위로 개발하는 것이 정착 되어진 듯 하다.

이것은 MSA(Micro Service Architecture) 방법론 과도 호환된다.

어떤게 먼저 시작되었는지는 모르겠다. 잡소리 였다.

Vue 컴포넌트 데이터 연동 정리

데이터 단방향

Vue 는 컴포넌트 간 데이터 전달이 단방향이다. 즉 한쪽 으로만 흐른다.

부모 컴포넌트에서 자식 컴포넌트로 전달되는 단방향이 기본값 이다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component :text="sendData"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: {{ text }}</p>
            </div>
        `,
        props: ['text'],
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        }

      })
    </script>

  </body>
</html>

ChildComponent 의 propstext 변수를 받고, text 는 부모의 data 속성인 sendData 로 연결되어 있다.

이제 자식컴포넌트 에서 전달받은 text 속성을 변경해 보자.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component :text="sendData"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: {{ text }}</p>
                <button type="button" @click="changeTextProp">text 속성바꾸기</button>
            </div>
        `,
        props: ['text'],
        methods: {
            changeTextProp() {

                this.text = "자식이 text 속성 바꾸다.";

            }
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        }

      })
    </script>

  </body>
</html>

버튼을 누르게 되면, 자식의 text 값이 변경 되지만, 참조처럼 전달될 거 같은 sendData 의 값은 변하지 않는다.

이것이 바로 단방향의 증거다! 부모에게 흐르지 않는다.

  • 위에 코드에는 Vue 가 권고하지 않는 코드가 있다. 전달 받은 text 속성의 값을 직접 변경하지 말아라 라는 규칙이 있다.

물론 에러는 나지 않고, 경고문구가 콘솔에 출력될 수는 있지만, 되도록이면 props 로 전달받은 변수를 직접 바꾸지 않는 것이 국롤 이다.

이벤트 $emit 발생

그래서 이 값을 변경하려면, $emit 메소드를 이용해서, 부모의 이벤트를 발생시키고, 발생된 이벤트에 연결된 함수가 내부 데이터를 변경하는 방식으로 해야만 한다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component :text="sendData" @call-parent-function="eatChildCall"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: {{ text }}</p>
                <button type="button" @click="changeTextProp">text 속성바꾸기</button>
            </div>
        `,
        props: ['text'],
        methods: {
            changeTextProp() {

                // this.text = "자식이 text 속성 바꾸다.";
                this.$emit('call-parent-function', "자식이 text 속성 바꾸다.");

            }
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        },
        methods: {

            eatChildCall( val ) {

                this.sendData = val;

            }

        }

      })
    </script>

  </body>
</html>

위와 같이 this.$emit 으로 부모의 이벤트 이름을 넣으면, 부모의 이벤트와 연결된 함수 eatChildCall 이 호출되어, 전달인자로 값을 받고, 부모 내부에서 sendData 값을 바꾸는 구조로 하면, 변경된 값이 자식에게로 다시 흘러 들어가게 되므로, 자식 컴포넌트의 text 가 변경된다.

이것이 권장하는 데이터 전달 방식이다.

.sync

이렇게 update 를 담당하는 함수를 여러번 호출해 가며, 사용하는 것은 비효율적 이라고 생각해서 였을까?

Vue 는 .sync 라는 것을 지원한다. 이것은 위와 같이 이벤트 전달방식을 줄여 준다.

동기되어야 하는 속성에 .sync 가 추가되고, 업데이트를 담당하던 @call-parent-function="eatChildCall" 부분은 삭제된다.

그리고 $emit 에 규칙이 추가 된다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component :text.sync="sendData"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: {{ text }}</p>
                <button type="button" @click="changeTextProp">text 속성바꾸기</button>
            </div>
        `,
        props: ['text'],
        methods: {
            changeTextProp() {

                // this.text = "자식이 text 속성 바꾸다.";
                // this.$emit('call-parent-function', "자식이 text 속성 바꾸다.");
                this.$emit('update:text', "자식이 text 속성 바꾸다.");

            }
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        },
        methods: {

        }

      })
    </script>

  </body>
</html>
this.$emit('update:text', "자식이 text 속성 바꾸다.");

update: 접두어가 붙어 있다는 것을 기억하자. .sync 와 짝꿍이다.

이것은 부모의 속성과, 자식의 속성이 연동이 필요할 때만 유효한 방법이다.

값 동기화 하는 것 외에 다른처리도 같이 해야 한다면, 이 방법은 쓸모가 없다.

v-model

폼요소의 v-model 경우에는 어떨까? 왠지 될거 같지 않아?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component :text="sendData"></child-component>

      <p>부모데이터 : <input type="text" v-model="sendData" /></p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: {{ text }}</p>
            </div>
        `,
        props: ['text'],
        methods: {
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        },
        methods: {

        }

      })
    </script>

  </body>
</html>

위 코드에는 이상한 점이 없다. input 의 값은 부모의 sendData 이기 때문에, 변경되면 즉시 자식에게 흐른다.

그럼 자식의 v-model 값은 연동될까?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component :text="sendData"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: <input type="text" v-model="text" /></p>
            </div>
        `,
        props: ['text'],
        methods: {
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        },
        methods: {

        }

      })
    </script>

  </body>
</html>

응 안돼~

꽉 막혔다.

부모와 자식간에는 v-model 만으로는 부족하다.

자식컴포넌트 v-model 값 부모에게 보내기

자식 컴포넌트의 값을 부모 v-model 로 전파되게 하는 방법은 emit('input') 을 이용하면 된다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component v-model="sendData"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: <input type="text" v-model="value" /></p>
                <button type="button" @click="changeTextProp">바꿔줘</button>
            </div>
        `,
        props: ['value'],
        methods: {
            changeTextProp() {

                this.$emit('input', this.value);

            }
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        },
        methods: {

        }

      })
    </script>

  </body>
</html>

부모에서 자식 컴포넌트를 불러올 때, v-model 에 값을 대입해 두었는데, 이렇게 v-model 을 이용해서 전달되는 값은, 자식 컴포넌트의 props: 'value' 에 대입된다.

약속이다.

물론 자식 컴포넌트에서 <input type="text" v-model="value" /> 이렇게 할당해 두더라도, 입력값에 따라, 부모컴포넌트에 값이 전달되지 않는다.

부모 컴포넌트의 v-model 로 값을 전달하기 위해서는 this.$emit('input', this.value); 이렇게 input 이라는 이벤트를 발생시켜야만 한다.

데이터 양방향 느낌으로, 입력하자마자 바뀌었으면 좋겠어서, input 태그@input 이벤트로 연결해 보자.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component v-model="sendData"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: <input type="text" v-model="value" @input="changeTextProp" /></p>
            </div>
        `,
        props: ['value'],
        methods: {
            changeTextProp() {

                this.$emit('input', this.value);

            }
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        },
        methods: {

        }

      })
    </script>

  </body>
</html>

뭔가 뚝뚝 끊기는 느낌이다. v-model 의 방식이 입력이 끝난 후 데이터에 반영하기 때문이다.

또 다른 방법으로는 computed 에 연결하는 것이다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">

      <child-component v-model="sendData"></child-component>

      <p>부모데이터 : {{ sendData }}</p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
    <style>
    </style>

    <script type="text/javascript">

      const ChildComponent = {
        template: `
            <div>
                <p>자식데이터: <input type="text" v-model="childModel" /></p>
            </div>
        `,
        props: ['value'],
        computed: {
            'childModel': {

                get() {
                    return this.value;
                },

                set( val ) {
                    this.$emit('input', val);
                }

            }
        },
        methods: {
        }
      }

      new Vue({
        el: '#app',
        components: { ChildComponent },
        mounted() {
          console.log( '구동되었다.' );
        },
        data() {

          return {
            sendData: '안녕?'
          }


        },
        methods: {

        }

      })
    </script>

  </body>
</html>

get, set 을 응용하여 할 수 있다.
(prop 에 받은 value 를 computed 에서 중복사용 할 수 없기 때문에, childModel 을 새로 만들었다.)

결론

  • 부모 v-model 은 자식 컴포넌트의 props: 'value' 에 대입된다.
  • 자식 컴포넌트에서 부모 v-model 에 값을 전파하기 위해서 $emit('input') 이벤트를 사용한다.

또다른 결론

  • 웬만하면 단방향 흐름 으로 작성하자.
  • v-model 은 만능키가 아니다.
  • props 로 받은 변수명을 엥간하면 내부에서 조작 하지 말자.