2024. 1. 3. 17:44ㆍ뷰(Vue)
이 글은 주 단위로 하던 스터디 를 개인적으로 정리하는 글입니다.
7장
7장에는 실질적인 Vue 프로젝트를 초기 세팅해가는 부분들이었다.
대략적으로만 알면 좋을 것 같다. 회사에서 실질적인 프로젝트를 진행하게되면 초기단계부터 구성하는 경우는 잘 없고 대부분 어느정도 틀이 잡혀있는 상태에서 투입되는 경우가 많기 때문이다.
(물론 구조가 어떻게 되어있고 대략적인 지식은 필요하다)
단일 컴포넌트를 사용한 Vue 세팅 및 설정 도구에 대한 설명.
첫번째로는 npm vue/cli create를 수행하여 프로젝트를 생성하는걸 설명하고 있지만..
현재는 더 빠르게 수행가능한 vite기반의 create-vue를 사용하는 경우가 많다고 함.
만약 Vite를 사용한다고 하면 2가지 방법이 있는데…
첫번째는 직접 설정하는것이고 두번째는 create-vue기반으로 설정된걸 사용하는 것이다.
첫번째
npm init vite [프로젝트명] -- --template vue
두번째
npm init vue@latest
그리고 해당 프로젝트에서 npm명령어를 이용 할 수 있다.
npm install
npm run build
npm run dev
npm run preview
컴포넌트를 사용 할 때 발생하는 기본적인 구조인 props와 그걸 전달하는 event에 대해서 간단하게 설명하고 있다.
props로 전달하는 대상의 경우 지정이 가능하고, 전달받을 Type또한 명시가 가능하다.
아예 동작하지 않는건 아니지만 관련해서 타입을 잘못 전달하는경우 콘솔로 에러를 보여준다거나 하는 동작을 수행한다.
그냥 간단하게 타입만 명시 할 수도있고, 상세하게 필수여부도 가능 하다.
<template>
<li>
<input type="checkbox" :checked="checked"> {{name}}
</li>
</template>
<script setup>
const props = defineProps({
checked: Boolean,
name: {
type: String,
required: true,
},
})
</script>
타입은 다음과 같이 명시가 가능함.
String, Number, Boolean, Array, Object, Date, Function, Symbol
자식 컴포넌트에서 값을 전달할때 event로 전달하는 형태를 가진다.
부모컴포넌트에서는 해당 이벤트를 캐치하여 전달받은 argument를 사용하여 컨트롤 가능하다.
해당 구조는 react에서도 동일하게 보여진다.
그러나 차이점은.. 리액트에서는 부모가 특정 함수를 내려주는 형태였다.
리액트에서의 방식은 다음과 같았다.
부모 컴포넌트
import React, { Component } from 'react';
import Child from './Child';
class Parent extends Component {
//부모 함수 정의
parentFunction = (data) => {
console.log("자식에서 부모로 데이터 넘어옴",data);
}
render() {
return (
<div>
<Child parentFunction={this.parentFunction} />
</div>
);
}
}
export default Parent;
자식 컴포넌트
import React, { Component } from 'react';
class Child extends Component {
childText = 'childText';
childFunction = () => {
this.props.parentFunction(this.childText);
}
render() {
return (
<div>
<button onClick={this.childFunction}>Click</button>
</div>
);
}
}
export default Child;
참고주소
[REACT] 리액트 - 자식 → 부모에게 데이터 전달하기
그러나 Vue에서는 자식 컴포넌트에서 위로 전달할 이벤트 명을 명시하고 어떤 값을 전달할지 명시해서 부모 컴포넌트로 보내는 형태인것 같다.
부모 컴포넌트
<template>
<div>
<InputName @nameChanged="nameChangedHandler" />
<br />
<h3>App 데이터 : {{parentName}}</h3>
</div>
</template>
<script>
import InputName from './components/InputName.vue'
export default {
name: "App4",
components : { InputName },
data() {
return { parentName: "" }
},
methods: {
nameChangedHandler(e) {
this.parentName = e.name;
}
}
}
</script>
자식 컴포넌트
<template>
<div style="border:solid 1px gray; padding:5px;">
이름 : <input type="text" v-model="name" />
<button @click="$emit('nameChanged', { name })">이벤트 발신</button>
</div>
</template>
<script>
export default {
name: "InputName",
//emits : [ "nameChanged1"],
emits: {
nameChanged: (e) => {
return e.name && typeof(e.name) === "string" && e.name.trim().length >= 3 ?
true : false
}
},
data() {
return {
name: ""
};
},
}
</script>
$emit을 통해서 nameChanged 라는 이름으로 이벤트를 발생시키고 그걸 부모 컴포넌트에서 받아서 쓰고있다.
위와같은 구조는 컴포넌트가 단일로 각각 바라볼때는 문제가 없지만 구조가 복잡해 질수록 서로 데이터를 전달하는 과정이 복잡하고 힘들어 질 수 있다.
그렇기 때문에 이벤트를 쉽게 전달하기 위한 EventBus나 상태(State)를 관리하는 상태관리(Vuex, Pinia)가 사용된다.
Vue2.0 에서는 EventBus를 사용했고,
Vue3 버전에서는 Mitt이라는 라이브러리를 주로 사용하는 듯 하다.
그러나 실질적으로 업무에 사용할때는 Pinia를 통한 관리가 편리하기 때문에 거의 쓰지않을듯 하다.
책에서도 비슷하게 설명하고 있다.
이벤트 에미터를 사용하는 방법은 공식적으로 권장하는 방법은 아닙니다. Vue 공식 문서에서는 전역 상태 관리 기능을 제공하는 라이브러리로 Vuex 또는 Pinia를 이용할 것을 권장합니다. (책 저자는 간단한 구조의 application이라면 사용해도 좋다고 부연설명을 하였다.)
8장
컴포넌트들에 대한 심화판 설명이 들어가는것 같다.
초기에 나오는 부분은 CSS 및 style이 적용되는 부분이였다.
동일 컴포넌트에서 각각 다르게 css class를 명시하더라도 마지막에 호출된 클래스의 스타일이 적용되어 버린다.
그렇기 때문에 특정 컴포넌트에서만 스타일이 적용되기를 원한다면 scoped영역 내부에 명시하면 된다.
<style scoped></style>
그리고 부모 컴포넌트에서 자식 컴포넌트로 템플릿 정보를 전달 할 때 slot을 사용하면 편하다고 한다.
결국 자식컴포넌트에 slot이라고 명시된 부분에 부모가 전달한 템플릿이 들어가서 그려진다고 보면된다.
SlotTest.vue
<template>
<div>
<h3>당신이 경험한 프론트 엔드 기술은?(두번째:slot기본)</h3>
<CheckBox2 v-for="item in items" :key="item.id" :id="item.id"
:checked="item.checked" @check-changed="CheckBoxChanged">
<span v-if="item.checked" style="color:blue; text-decoration:underline;">
<i>{{item.label}}</i></span>
<span v-else style="color:gray">{{item.label}}</span>
</CheckBox2>
</div>
</template>
<script>
import CheckBox2 from './CheckBox2.vue';
export default {
name : "SlotTest",
components : { CheckBox2 },
data() {
return {
items : [
{ id:"V", checked:true, label:"Vue" },
{ id:"A", checked:false, label:"Angular" },
{ id:"R", checked:false, label:"React" },
{ id:"S", checked:false, label:"Svelte" },
]
}
},
methods : {
CheckBoxChanged(e) {
let item = this.items.find((item)=> item.id === e.id);
item.checked = e.checked;
}
}
}
</script>
CheckBox2.vue
<template>
<div>
<input type="checkbox" :value="id" :checked="checked"
@change="$emit('check-changed', {id, checked: $event.target.checked })" />
<slot>Item</slot>
</div>
</template>
<script>
export default {
name : "CheckBox2",
props : ["id", "checked"]
}
</script>
위 코드에서는 <slot></slot>라고 명시되어 있는 부분에 부모가 전달한
<span v-if="item.checked" style="color:blue; text-decoration:underline;">
<i>{{item.label}}</i></span>
<span v-else style="color:gray">{{item.label}}</span>
가 그려지는 것이다.
확장성 있게 사용하고 싶다면..? 해당 방법도 괜찮은 듯 하다.
위 방법은 하나의 슬롯만 사용하는 경우이고, Slot을 여러개 사용하고싶다면 named Slot방법을 사용해야한다.
name값으로 명시된 이름과 부모에서 전달해주는 이름이 같아야한다.
#자식
<slot name="icon"></slot>
#부모
<CheckBox3 v-for="item in items" :key="item.id"
:id="item.id" :checked="item.checked"
@check-changed="CheckBoxChanged">
<template v-slot:icon>
<i v-if="item.checked" class="far fa-grin-beam"></i>
<i v-else class="far fa-frown"></i>
</template>
</CheckBox3>
전달된 슬롯이 없다면
<slot></slot> 사이의 값이 default로 표시된다.
예를들어 <slot name="icon"><span>abc</span></slot>
라고 명시되어있다면 icon이라는 슬롯이 전달되지 않으면 abc라는 span태그가 보여진다.
named slot의 경우에는 사용할때
v-slot으로 명시할수도 있고 그걸 축약하여 #으로 표현할 수도 있다.
예를들면 다음과 같다.
<template v-slot:icon>
<i v-if="item.checked" class="far fa-grin-beam"></i>
<i v-else class="far fa-frown"></i>
</template>
<template #icon>
<i v-if="item.checked" class="far fa-grin-beam"></i>
<i v-else class="far fa-frown"></i>
</template>
이름을 명시하지 않는 slot의 경우 default라는 이름을 가진다.
다음과 같다면 named된 slot의 경우 각각의 이름을 가지고 명명되지 않는 경우에는 default라는 이름을 가진것과 같다. 대신 사용하는쪽에서는 이름을 명시해야한다.
<slot name="a">a</slot>
<slot>b</slot>
<slot name="c">c</slot>
<template #a>
</template>
<template #default>
</template>
<template #b>
</template>
- 동적 컴포넌트
화면의 동일한 위치에 여러 컴포넌트를 표현해야 한다면..?
동적 컴포넌트를 사용 할 수 있다.
app.vue
<template>
<div class="header">
<h1 class="headerText">태평양 전쟁의 해전</h1>
<nav>
<ul class="nav nav-tabs nav-fill">
<li v-for="tab in tabs" :key="tab.id" class="nav-item">
<a style="cursor:pointer;" class="nav-link"
:class="{ active : tab.id === currentTab }"
@click="changeTab(tab.id)">{{tab.label}}</a>
</li>
</ul>
</nav>
</div>
<div class="container">
<keep-alive include="MidwayTab,CoralSeaTab">
<component :is="currentTab"></component>
</keep-alive>
</div>
</template>
<script>
import CoralSeaTab from './components/CoralSeaTab.vue'
import LeyteGulfTab from './components/LeyteGulfTab.vue'
import MidwayTab from './components/MidwayTab.vue'
export default {
name: 'App',
components : { CoralSeaTab, LeyteGulfTab, MidwayTab },
data() {
return {
currentTab : 'CoralSeaTab',
tabs : [
{ id:"CoralSeaTab", label:"산호해 해전" },
{ id:"MidwayTab", label:"미드웨이 해전" },
{ id:"LeyteGulfTab", label:"레이테만 해전" }
]
}
},
methods : {
changeTab(tab) {
this.currentTab = tab;
}
}
}
</script>
<style>
.header { padding: 20px 0px 0px 20px; }
.headerText { padding: 0px 20px 40px 20px; }
.tab { padding: 30px }
</style>
여기서 중요한 부분은 정적인 내용이라면 계속 그려주는건 리소스 낭비가 되는데…
keep-alive로 감싸주게되면 캐싱기능이 적용된다.
그리고 특정 컴포넌만 포함할지 include, exclude로 설정이 가능하다.
다음과 같이 변경되는 것의 경우 캐싱을 할 경우 최초 로딩됬을때 데이터를 가진다.
<div>{{ (new Date()).toTimeString() }}</div>
- provide 와 Injection
부모 컴포넌트에서 전달할것을 제공(provide)하고 자식 컴포넌트 들에서 주입(injection)하여 사용 할 수 있는 방식인듯한데..
값이 변경됨에 따라 새로 랜더링 하는 경우가 많을때는 권장되지 않는 방식이라고 한다.
Vuex, Pinia를 사용하는걸 추천하였다.
- Teleport
모달이나 툴팁과 같이 메인 화면과 독립적이면서 공유UI를 제공해야 한다면 컴포넌트 계층 구조와 관계없이 사용 할 수 있어야하는데.. 그때 사용하면 좋다고 한다!
예를들면 로그인창, 결제창, 모달 같은 ui를 구현한다면 현재 화면을 뚫고 최상위로 올라와야 하는 작업을 해야하는데 이 경우 보통 컴포넌트가 그려지는 영역인 app이 아닌 명시된 이름(여기선 model)영역에 그려지게 되는것이다.
책의 예시코드에서는
app영역과는 별개로 modal영역을 만들어두었다.
index.html
<body>
<div id="modal"></div>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
Modal.vue
<template>
<div class="modal">
<div class="box">
처리중
</div>
</div>
</template>
<script>
export default {
name : "Modal"
}
</script>
<style scoped>
.modal { display: block; position: fixed; z-index: 1;
left: 0; top: 0; width: 100%; height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4); }
.box { display: flex; flex-direction: column;
align-items: center; justify-content: center;
position: absolute; left: 50%; top: 50%; background-color:aqua;
border: double 3px gray; width:100px; height: 80px;
margin-top:-50px; margin-left:-50px;}
</style>
그외 내용
- 스피너
로딩시 사용자에게 보여주는 부분인데 Vue 라이브러리 받아서 사용 할 수도 있고 그냥 컴포넌트에 하나 따로 만들어서 사용 할 수도 있다.
css기반 csspin 이다.(로딩시 돌아가는 스핀이라고 보면 될듯하다)
https://github.com/stepanowon/vue-csspin
'뷰(Vue)' 카테고리의 다른 글
setup() 과 script setup의 차이 (2) | 2024.01.04 |
---|---|
원쌤의 Vue.js 퀵스타트 책을 보았다(Vue3)_7 (2) | 2024.01.03 |
원쌤의 Vue.js 퀵스타트 책을 보았다(Vue3)_5 (1) | 2023.12.29 |
원쌤의 Vue.js 퀵스타트 책을 보았다(Vue3)_4 (0) | 2023.12.20 |
원쌤의 Vue.js 퀵스타트 책을 보았다(Vue3)_3 (1) | 2023.12.20 |