Vanilla js를 통해 라우터 기반 SPA 를 만들어본 경험을 바탕으로 Vue.js 를 공부해보자. html, css, js의 조합이 아닌 Vue.js 의 컴포넌트 조합 방식이 처음에 잘 이해가 안 가서, 공부하면서 이해가 잘 가지 않았던 부분을 정리했다.
.vue 의 정체
.vue 의 정체? 클래스?
.vue 파일은 객체 지향의 Class와 매우 유사한 역할을 하는 ‘UI 청사진’ 으로 이해하자.
바닐라 JS에서 클래스를 만들어 new Button()으로 인스턴스를 생성하듯, Vue는 .vue라는 설계도를 바탕으로 화면에 컴포넌트 인스턴스를 찍어낸다.
바닐라 JS 클래스는 어떻게 DOM을 조작할지 명령하지만, .vue는 상태에 따라 화면이 무엇처럼 보여야 하는지 선언하고 관리한다.
| 구분 | JS | Vue.js |
|---|---|---|
| 데이터 (상태) | this.state = { ... } |
<script> 내의 ref 또는 data |
| UI 구조 | render() 메서드 내의 문자열/DOM |
<template> 태그 내의 HTML |
| 스타일 | 별도 .css 파일 혹은 JS 내 삽입 |
<style scoped> 태그 |
| 인스턴스화 | const btn = new Button(); |
<Button /> (선언적 사용) |
비슷한 속성을 공유하는 컴포넌트는 공통 css나 js 설정이 어처피 필요한거 아닌가?
비슷한 속성을 공유하는 컴포넌트들은 공통 CSS나 JS 설정이 필수적이다. 하지만 Vue는 이를 클래스 상속(extends)으로 해결하지 않는다. 바닐라 JS처럼 코드를 복제하거나 방대한 전역 클래스에 의존하는 대신, Props와 조합을 통해 효율적으로 관리한다.
main.css 등에 정의하며, 모든 컴포넌트가 공유하는 최소한의 기본 베이스(폰트, 여백 등)를 설정한다..vue 파일 내에서만 유효한 고유 스타일이다. 전역 오염 없이 컴포넌트만의 개별 디자인을 정의한다.
왜 직접적인 상속 기능을 피한거?
전통적인 클래스 상속(A extends B)은 Vue에서 권장되지 않는다. 상속 계층이 깊어지면 특정 기능의 출처를 파악하기 어렵고, 부모의 작은 변화가 모든 자식에게 치명적인 영향을 주는 ‘취약한 기반 클래스 문제’ 가 발생하기 때문이다. Vue는 복잡한 계층 구조를 만드는 대신, 아래의 도구들을 조합하여 상속의 필요성을 완벽히 대체한다. 기능이나 디자인이 비슷한 컴포넌트 를 만들 때, 상속은 새로운 클래스라는 유전자를 물려받는 과정이지만, Vue의 방식은 동일한 청사진(.vue)에 Props라는 옵션만 다르게 끼워 넣거나 필요한 JS 로직 부품을 조립하여 재사용하는 것이다.
그래서 버튼을 어떻게 컴포넌트 방식으로 생산/관리함?
main.css)프로젝트 전체의 기준값을 정하는 셈. 실제 모양모단 최대한 공통적인 부분을 정의해주자.
:root {
--main-blue: #007bff; /* primary 색상 */
--main-red: #dc3545; /* danger 색상 */
--font-size-base: 16px;
}
.btn-layout {
display: inline-flex;
border-radius: 4px;
border: none;
cursor: pointer;
padding: 10px 20px;
}
MyButton.vue)전역 설정에서 정한 기준값을 바탕으로 실제 버튼을 만들어주자.
이때, 여기서 Props 를 이용해 모양을 결정하는 로직을 딱한 번만 작성한다.
<template>
<button :class="['btn-layout', type]">
<slot></slot> </button>
</template>
<script setup>
defineProps({
// 부모로부터 'primary' 또는 'danger'라는 문자열을 받음
type: { type: String, default: 'primary' }
})
</script>
<style scoped>
/* primary라고 전달받으면 적용될 스타일 */
.primary {
background-color: var(--main-blue);
color: white;
}
/* danger라고 전달받으면 적용될 스타일 */
.danger {
background-color: var(--main-red);
color: white;
}
</style>
이제 다른 페이지에서는 CSS를 단 한줄도 적지 않는다. 위에서 만들어둔 버튼을 가져와서 Props만 지정해주면 된다.
<template>
<div class="login-page">
<h1>로그인</h1>
<MyButton type="primary">확인</MyButton>
<MyButton type="danger">취소</MyButton>
</div>
</template>
<script setup>
import MyButton from '../components/MyButton.vue'
</script>
왜 굳이 Scoped를 사용하는지?
다른 개발자가 만든 Login.vue 안에 실수로 .danger { color: yellow; } 라고 적었다면, 전체 웹사이트의 모든 위험 버튼이 노란색으로 변해버린다. scoped를 붙이면 Vue가 내부적으로 .danger[data-v-f3f3eg] 같은 고유 아이디를 붙여주기 때문에, MyButton.vue 안의 .danger는 오직 이 버튼에만 적용된다.
CSS 뿐만 아니라 JS 로직도 컴포넌트화가 가능?
여러 컴포넌트에서 “클릭 시 로그를 남기는 기능"이나 “API 호출 로직"이 겹친다면, 이를 별도의 JS 파일로 빼서 조립할 수 있다! 별도의 JS 파일에 로직을 만들어 두고, vue로 만든 버튼 등의 컴포넌트에 붙여서 사용하는 것이다.
| 디렉티브 | 설명 | 예시 |
|---|---|---|
v-text |
엘리먼트의 텍스트 콘텐츠를 업데이트 | <span v-text="msg"></span> |
v-html |
HTML 코드를 직접 삽입 (XSS 주의) | <div v-html="rawHtml"></div> |
v-bind |
HTML 속성 동적 바인딩 (:) |
<img :src="imageSrc"> |
v-on |
이벤트 리스너를 연결 (@) |
<button @click="doSomething"> |
v-model |
폼 입력과 데이터 양방향 연결 | <input v-model="name"> |
v-if |
조건부로 엘리먼트를 생성/제거 | <p v-if="seen">보여요</p> |
v-show |
조건부로 CSS display 속성 전환 |
<p v-show="seen">보여요</p> |
v-for |
배열이나 객체를 반복하여 렌더링 | <li v-for="item in items"> |
v-text와 내부 텍스트
v-text 쓰면 내부의 기존 텍스트(abcd)는 못 쓰나?
v-text는 해당 엘리먼트의 textContent를 통째로 덮어버리는 역할v-text="message"는 내부적으로 element.textContent = message를 수행<div v-text="message">abcd</div>라고 적어도, Vue가 실행되는 순간 abcd는 삭제되고 message 변수의 값으로 교체<div>abcd {{ message }}</div>v-model과 ref의 관계
이 둘이 무슨 관계임?
ref
v-model
input)과 그 ref 데이터를 실시간으로 동기화해주는 양방향 연결 고리v-bind와 v-model의 관계
이 둘이 무슨 관계임?
v-model은 사실 **v-bind와 v-on을 합쳐놓은 확장판v-bind
v-model
v-model vs v-bind, v-on)<template>
<div>
/* v-model 방법 */
<input v-model="name" />
/* v-bind, v-on */
<input :value="name" @input="name = $event.target.value" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('')
</script>
:value="name" (v-bind) 부분이 담당ref 변수인 name 값이 변경되면, 연결된 input 태그의 value 속성에 즉시 반영@input="name = $event.target.value" (v-on) 부분이 담당input 이벤트가 발생하고, 이때 입력창에 담긴 최신 값($event.target.value)을 name 변수에 다시 할당v-model은 위 두 가지 과정을 하나로 합쳐 개발자가 일일이 이벤트를 연결하지 않아도 되게끔 도와줌
이 수동 바인딩을 쓰긴 쓰나?
v-model로 충분하지만, 특정 로직이 필요한 경우에는 수동 바인딩 방식을 쓰기도 함v-model은 한글 입력 시 한 글자가 완성되기 전까지 데이터 반영이 미세하게 늦어지는 특성이 있음 ㅠㅠ@input을 직접 사용하면 자음/모음이 입력되는 즉시 데이터를 업데이트할 수 있다.v-if vs v-show 차이
언제 뭘 써야할까?
v-if
v-show
| 구분 | v-if |
v-show |
|---|---|---|
| 방식 | 물리적 제거/생성 (DOM에서 삭제) | CSS 제어 (display: none) |
| 초기 비용 | 조건이 거짓이면 아예 렌더링 안 함 (낮음) | 조건 상관없이 일단 렌더링함 (높음) |
| 전환 비용 | 바뀔 때마다 요소를 뺏다 꼈다 함 (높음) | 단순히 CSS만 바꿈 (낮음) |
v-for 사용 시 :key 가 필수인 이유
v-for에서 :key가 필수인 이유
:key는 각 항목의 “주민등록번호” 역할을 해줌:key를 설정해야 하는 이유
key를 대조해 바뀐 놈만 찾아내어 그 위치에 끼워 넣음key가 없으면 Vue는 리스트 순서가 바뀌었을 때 내부 상태(예: 입력창에 적던 내용, 체크박스 체크 여부)를 엉뚱한 리스트 아이템에 연결하는 버그를 일으킬 수 있음index를 key로 쓰는 것은 피해야 함!!index 번호가 밀려버려 Vue가 어떤 놈이 그놈인지 헷갈리기 때문…item.id)을 쓰는 것이 베스트