Vue3

2023. 5. 22. 23:24프레임워크̸라이브러리/Vue

2022년 2월 7일부터 Vue.js 라이브러리와 공식문서의 기본 버전이 3버전으로 바뀌었다. Vue2의 지원은 2023년 12월 31일에 종료된다고 한다.

 

내가 가장 마지막에 Vue를 사용한건, k8s 스터디에 참여하기 위해 2020년 10월 17일에 Spring boot + Vue로 만든 To Do List이다.

당시에 Vue2 버전을 사용했었고, 공식 문서 한글번역이 매우 잘 되어있어 편했던 기억이 난다.

 

이후로 계속 React만 써왔지만 추후 Vue를 사용하는 프로젝트에 참여하게 될 수도 있어서 빠르게 훑고 넘어가려고 한다.

템플릿 문법 같은 기본적인 틀은 Vue2와 비슷하고, 두드러지는 특징은 composition API인 것 같다.

 


template

텍스트 바인딩

데이터 바인딩의 가장 기본 형태는 Mustache(이중 중괄호) 문법을 사용한 텍스트 보간법이다.

<span>메세지: {{ message }}</span>

단순 텍스트 바인딩

이중 중괄호 태그 내 message는 해당 컴포넌트 인스턴스의 message 속성의 값으로 대체된다. 또한 message 속성이 변경될 때 마다 업데이트 된다.

<span>메세지: {{ getMessage() }}</span>

getMessage()처럼 표현식 및 함수 호출도 가능하다.

💡 리렌더링 될 때마다 함수를 호출하기 때문에 성능상 이점을 가지지 않는다.

 

 

HTML 출력

<p>텍스트 보간법 사용: {{ rawHtml }}</p>
<p>v-html 디렉티브 사용: <span v-html="rawHtml"></span></p>
텍스트 보간법 사용: <span style="color: red">이것은 빨간색이어야 합니다.</span>
v-html 디렉티브 사용: 이것은 빨간색이어야 합니다.

이중 중괄호는 데이터를 HTML이 아닌 일반 텍스트로 해석한다.

실제 HTML을 출력하려면, v-html 디렉티브를 사용해야한다.

디렉티브는 Vue에서 제공하는 특수한 속성임을 나타내기 위해 접두사 v-를 사용하며, 렌더링된 DOM에 특별한 반응적 동작을 적용한다.
❗️ 보안 경고
웹사이트에서 임의의 HTML을 동적으로 렌더링하면 XSS 취약점이 쉽게 발생할 수 있으므로 매우 위험할 수 있다. 신뢰할 수 있는 컨텐츠에만 v-html을 사용하고 사용자가 제공한 컨텐츠에는 절대 사용하지 않아야한다.

 

 

속성 바인딩

HTML 속성에 데이터를 바인딩할 때는 v-bind:속성명 으로 사용할 수 있다. React와 달리 변수를 사용할 때에도 ""를 사용한다.

<div v-bind:id="dynamicId"></div>

v-bind 디렉티브는 엘리먼트의 id 속성을 컴포넌트의 dynamicId 속성과 동기화된 상태로 유지하도록 Vue에 지시한다. 바인딩된 값이 null 또는 undefined일 경우, 엘리먼트의 속성이 제거된 상태로 렌더링된다.

 

<div :id="dynamicId"></div>

v-bind는 매우 일반적으로 사용되기 때문에 위와 같이 :를 사용한 단축 문법이 있다.

 

 

boolean 속성

<button :disabled="isButtonDisabled">버튼</button>

isButtonDisabled에 true값이 있는 경우, disabled 속성이 표기된다.

값이 빈 문자열인 경우 <button disabled="">의 일관성을 유지하므로 속성이 표기된다.

그 외 false 값의 경우 속성이 생략된다.

 

 

여러 속성을 동적으로 바인딩

const data = {
	id: 'container',
	class: 'wrapper'
}

<div v-bind="data"></div>

v-bind를 사용하여 단일 엘리먼트에 바인딩할 수 있다.

💡 부모에게서 전달받은 Props를 자식 컴포넌트에 전부 전달해야 하는 경우에 이 방법을 사용하면 용이하다.

 

 

자주 사용되는 디렉티브

v-if

조건부 렌더링. 값으로 boolean 타입의 값을 사용할 수 있다.

JavaScript와 마찬가지로 v-if / v-else-if / v-else 세 가지 디렉티브가 존재한다.

<div v-if="loginedIn">로그아웃</div>
<div v-else>로그인</div>

 

v-show

조건에 따라 화면 노출 여부 결정시 사용. v-if는 조건에 따라 렌더링 자체가 되지 않지만, v-show는 렌더링은 되지만 display: none인 상태이다.

<div v-show="loginedIn">아이디</div>
💡 빈번하게 토글되어야 하는 경우에는 v-show를 사용하는 것이 좋다.

 

v-for

다수의 데이터를 비슷한 템플릿 블록으로 렌더링해야할 때 사용한다. 보통 리스트를 렌더링할 때 자주 사용된다.

대상 타입으로는 Array | Object | number | string | Iterable 이 있다.

<ul>
	<li v-for="item in items">{{ item.id }}</li>
</ul>

 


Script

<script setup>을 사용한 방식으로 설명한다. script 태그에 setup 속성을 추가하면 된다.

<script setup>

</script>

 

setup 속성을 사용하면 script 태그에서 작성된 변수, 함수, import를 다른 설정 없이 템플릿 영역 내부에서 사용 가능하게 된다.

<script setup>
const message = 'Hello World!';
</script>

<template>
	<div>{{ message }}</div>
</template>

 

반응형

script 영역 안에서 변수를 선언하고 해당 변수의 값을 변경하더라도 리렌더링 되지 않는다.

아래의 예제는 div를 클릭했을 때 message 변수의 값을 변경한다. 콘솔에서는 변경된 값으로 출력되지만, 화면에서는 변경되지 않는다. 이는 message가 반응형이 아니기 때문이다.

<script setup>
let message = 'Hello World!';
const onClick = () => {
	message = "Hi";
    console.log(message);
};
</script>

<template>
	<div @click="onClick">{{ message }}</div>
</template>

 

reactive

객체만 반응형 데이터로 만드는데 사용한다. 인자로 받은 객체와 동일한 프록시 객체를 반환해준다. 그래서 일반적인 객체를 사용하는 것 처럼 사용이 가능하다.

또한 깊은 감지를 사용하기 때문에 중첩된 상황에서도 감지가 가능하다.

아래처럼 reactive를 사용하면 의도한대로 클릭 시 정상적으로 화면에 반영된다.

<script setup>
import { reactive } from 'vue';

const obj = reactive({ message: "Hello World!" });

const onClick = () => {
	obj.message = "Hi";
    console.log(obj.message);
};
</script>

<template>
	<div @click="onClick">{{ obj.message }}</div>
</template>
만약 깊은 감지가 아닌 얕은 감지를 사용하고 싶다면, shallowReactive를 사용하면 된다.

 

초기화시에 존재하지 않았던 속성을 나중에 추가하더라도 감지할 수 있다.

<script setup>
import { reactive } from 'vue';

const obj = reactive({});

obj.count = 0;
const onClick = () => {
  obj.count++;
};
</script>

<tempate>
  <div>
    {{ obj.count }}
    <button @click="onClick">증가</button>
  </div>
</template>

 

ref

객체 뿐 아니라 기본 타입에도 반응형을 추가할 수 있다. reactive와 마찬가지로 객체에 반응형을 추가할 시 깊은 감지가 가능하다. ref 호출 후 값에 접근하는 것은 반환된 객체의 value를 통해서 가능하다.

<script setup>
import { ref } from 'vue';

const message = "Hello World!";

const onClick = () => {
	message.value = "Hi";
};
</script>

<template>
	<div @click="onClick">{{ message }}</div>
</template>
💡 template 부분에서는 value를 사용하지 않아도 된다.
reactive와 마찬가지로, 깊은 감지를 피하기 위해서는 shallowRef를 사용하면 된다.

 

reactiveref의 차이점은, reactive는 객체만 가능하다는 점이다.

추가적으로 reactive는 객체 내부 속성의 값을 변경하는 것은 반응형이 유지가 되나, 객체 자체에 값을 재할당 하는 경우에는 반응형으로 사용이 되지 않는다.

아래 예제에서 증가 버튼을 누를 때 까지는 화면에 반영이 되나, 변경을 누른 이후에는 다시 증가를 눌러도 아무런 반응을 하지 않게 된다.

<script setup>
import { reactive } from 'vue';

let obj = reactive({ count: 0 });

const onClick = () => {
	obj.count++;
};

const onChange = () => {
	obj = {
    	count: 100
	};
};
</script>

<template>
	<div>
    	{{ obj.count }}
        <button @click="onClick">증가</button>
        <button @click="onChange">변경</button>
    </div>
</template>

 

값을 재할당할 경우, reactive로 감싸주어도 감지가 되지 않는다.

// ...
const onChange = () => {
	obj = reactive({
    	count: 100
    });
};

 

하지만 ref를 사용하면 반응형을 유지하면서 값을 재할당할 수 있다.

<script setup>
import { ref } from 'vue';

let obj = ref({ count: 0 });

const onClick = () => {
	obj.value.count++;
};

const onChange = () => {
	obj.vlaue = {
    	count: 100
	};
};
</script>

<template>
	<div>
	    {{ obj.count }}
    	<button @click="onClick">증가</button>
	    <button @click="onChange">변경</button>
	</div>
</template>

결론

  • 일반 타입 → reactive 사용
  • 값을 재할당 해야 하는 경우 → ref 사용

 

computed

getter 접근자처럼 읽기 전용의 ref 객체를 반환한다. 첫번째 인수로 callback 함수를 전달하며 callback 함수에서 반환된 값이 ref 객체의 값이 된다.

<script setup>
import { ref, computed } from 'vue';

let obj = ref({ count: 0 });
const multiple = computed(() => obj.value.count * obj.value.count);

const onClick = () => {
	obj.value.count++;
};
</script>

<template>
	<div>
	    {{ multiple }}
    	<button @click="onClick">증가</button>
	</div>
</template>

 

set 프로퍼티를 사용하여 수정할 수 있는 ref 객체를 생성할 수도 있다.

<script setup>
import { ref, computed } from 'vue';

let obj = ref({ count: 0 });
const multiple = computed ({
	get: () => obj.value.count * obj.value.count,
    set: (value) => {
    	obj.value.count = value;
    },
});

const onClick = () => {
	obj.value.count++;
};

const onChagne = () => {
	multiple.value = obj.value.count - 1;
};
</script>

<template>
	<div>
	    {{ multiple }}
    	<button @click="onClick">증가</button>
	    <button @click="onChange">변경</button>
    </div>
</template>

 

computed의 getter는 캐싱 기능을 지원한다. getter에서 사용하고있는 반응형 값이 변경되지 않는다면, 리렌더링이 되더라도 호출 되지 않는다. 복잡한 계산을 해야하는 경우 computed를 사용하면 좋다.

아래 예제를 실행하면 증가 버튼을 눌렀을 때에는 콘솔이 출력되지 않고, 변경을 눌렀을 때에만 콘솔이 출력된다.

<script setup>
import { ref, computed } from 'vue';

const message = ref("Hello World!");
let obj = ref({ count: 0 });
const multiple = computed(() => {
	console.log("출력");
    return `${message.value}`;
});


const onClick = () => {
	obj.value.count++;
};

const onChange = () => {
	message.value += 1;
};
</script>

<template>
	<div>
	    <p>{{ multiple }}</p>
    	<p>{{ obj.count }}</p>
	    <button @click="onClick">증가</button>
    	<button @click="onChange">변경</button>
    </div>
</template>

 


option API

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log(`숫자 세기의 초기값은 ${ this.count } 입니다.`)
  }
}
</script>

<template>
  <button @click="increment">숫자 세기: {{ count }}</button>
</template>
  • data
    data()에서 반환된 속성들은 반응적인 상태가되어 this에 노출된다.
  • methods
    methods는 속성 값을 변경하고 업데이트 할 수 있는 함수이다.
    템플릿 내에서 이벤트 리스너로 바인딩 될 수 있다.
  • mounted
    생명주기 훅(LifeCycle Hooks)은 컴포넌트 생명주기의 여러 단계에서 호출된다.
    이 함수는 컴포넌트가 마운트 된 후 호출된다.

 

composition API

composition API를 사용하는 경우, import로 가져온 함수들을 사용하여 컴포넌트의 로직을 정의한다.

SFC(Single-File Component)에서 컴포지션 API는 일반적으로 <script setup>과 함께 사용딘다.

setup 속성은 Vue가 더 적은 코드 문맥으로 composition API를 사용하고, 컴파일시 의도한대로 올바르게 동작할 수 있게 코드를 변환하다록 하는 힌트이다.

예를들어, <script setup>import되어 가져온 객체들과 선언된 최상위 변수 및 함수는 템플릿에서 직접 사용할 수 있다.

<script setup>
import { ref, onMounted } from 'vue'

// 반응적인 상태의 속성
const count = ref(0)

// 속성 값을 변경하고 업데이트 할 수 있는 함수.
function increment() {
  count.value++
}

// 생명 주기 훅
onMounted(() => {
  console.log(`숫자 세기의 초기값은 ${ count.value } 입니다.`)
})
</script>

<template>
  <button @click="increment">숫자 세기: {{ count }}</button>
</template>

위의 예제는 option API 예제와 완전히 동일한 템플릿을 사용하는 동일한 컴포넌트이지만, composition API와 <script setup>을 사용했다.

 

 

리액트를 주로 사용해온 나는 composition API가 좀 더 눈에 익는다.

 

공식 문서에서는 제품용(production)으로 사용하는 경우 아래와 같이 권장하고있다.

  • 빌드 도구를 사용하지 않거나 Vue를 주로 복잡성이 낮은 시나리오에서 사용할 경우 → option API
  • Vue로 규모가 있는 앱의 전체를 구축하려는 경우 → compositoin API + SFC

 

 


Reference