1. 소개

Vanilla js를 통해 라우터 기반 SPA 를 만들어본 경험을 바탕으로 Vue.js 를 공부해보자. html, css, js의 조합이 아닌 Vue.js 의 컴포넌트 조합 방식이 처음에 잘 이해가 안 가서, 공부하면서 이해가 잘 가지 않았던 부분을 정리했다.




2. .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 /> (선언적 사용)



3. 상속 대신 조합


비슷한 속성을 공유하는 컴포넌트는 공통 css나 js 설정이 어처피 필요한거 아닌가?

비슷한 속성을 공유하는 컴포넌트들은 공통 CSS나 JS 설정이 필수적이다. 하지만 Vue는 이를 클래스 상속(extends)으로 해결하지 않는다. 바닐라 JS처럼 코드를 복제하거나 방대한 전역 클래스에 의존하는 대신, Props조합을 통해 효율적으로 관리한다.

스타일의 계층적 관리




4. Vue에 상속이 없는 이유


왜 직접적인 상속 기능을 피한거?

전통적인 클래스 상속(A extends B)은 Vue에서 권장되지 않는다. 상속 계층이 깊어지면 특정 기능의 출처를 파악하기 어렵고, 부모의 작은 변화가 모든 자식에게 치명적인 영향을 주는 ‘취약한 기반 클래스 문제’ 가 발생하기 때문이다. Vue는 복잡한 계층 구조를 만드는 대신, 아래의 도구들을 조합하여 상속의 필요성을 완벽히 대체한다. 기능이나 디자인이 비슷한 컴포넌트 를 만들 때, 상속은 새로운 클래스라는 유전자를 물려받는 과정이지만, Vue의 방식은 동일한 청사진(.vue)에 Props라는 옵션만 다르게 끼워 넣거나 필요한 JS 로직 부품을 조립하여 재사용하는 것이다.




5. 컴포넌트 관리


그래서 버튼을 어떻게 컴포넌트 방식으로 생산/관리함?

우선 전역 설정 (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>



6. Scoped


왜 굳이 Scoped를 사용하는지?

다른 개발자가 만든 Login.vue 안에 실수로 .danger { color: yellow; } 라고 적었다면, 전체 웹사이트의 모든 위험 버튼이 노란색으로 변해버린다. scoped를 붙이면 Vue가 내부적으로 .danger[data-v-f3f3eg] 같은 고유 아이디를 붙여주기 때문에, MyButton.vue 안의 .danger는 오직 이 버튼에만 적용된다.




7. 중복되는 JS 로직의 경우


CSS 뿐만 아니라 JS 로직도 컴포넌트화가 가능?

여러 컴포넌트에서 “클릭 시 로그를 남기는 기능"이나 “API 호출 로직"이 겹친다면, 이를 별도의 JS 파일로 빼서 조립할 수 있다! 별도의 JS 파일에 로직을 만들어 두고, vue로 만든 버튼 등의 컴포넌트에 붙여서 사용하는 것이다.




8. 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">



9. v-text와 내부 텍스트


v-text 쓰면 내부의 기존 텍스트(abcd)는 못 쓰나?




10. v-modelref의 관계


이 둘이 무슨 관계임?




11. v-bindv-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>



12. 수동 바인딩(v-bind + v-on)을 사용하는 이유


이 수동 바인딩을 쓰긴 쓰나?




13. v-if vs v-show 차이


언제 뭘 써야할까?

구분 v-if v-show
방식 물리적 제거/생성 (DOM에서 삭제) CSS 제어 (display: none)
초기 비용 조건이 거짓이면 아예 렌더링 안 함 (낮음) 조건 상관없이 일단 렌더링함 (높음)
전환 비용 바뀔 때마다 요소를 뺏다 꼈다 함 (높음) 단순히 CSS만 바꿈 (낮음)



14. v-for 사용 시 :key 가 필수인 이유


v-for에서 :key가 필수인 이유