*개요
웹 게임을 만들며 배우는 Vue 강의를 듣고 유용한 문법을 섹션별로 회고합니다.
데이터 중심으로 사고하라(화면에서 변경되는 부분과 아닌 부분을 잘 구분하라)
섹션 1 끝말잇기
- ref를 사용하면 특정 요소에 접근할 수 있습니다. 예를 들어, this.$refs를 사용해 ref="answer"로 정의한 요소에 계속 커서를 유지하도록 focus()를 걸 수 있습니다.
<template>
<form @submit="onSubmit">
<input type="text" ref="answer" v-model="value" />
<button>제출</button>
</form>
</template>
...
export default {
...
methods : {
onSubmit(e) {
e.preventDefault();
this.$refs.answer.focus();
}
}
}
섹션 2 구구단
- 같은 화면을 여러번 넣고 싶다면(각기 다른 data를 유지) 컴포넌트를 활용합니다.
...
<div id="root">
<word-reply start-word="초밥"></word-reply>
<word-reply start-word="박수"></word-reply>
<word-reply start-word="컴퓨터"></word-reply>
</div>
...
끝말잇기 여러 개를 동시에 실행하는 경우, 각 끝말잇기 입력값을 고유한 data에 저장하기 위해서 컴포넌트를 활용합니다. 그렇지 않다면 keyword1, keyword2, keyword3처럼 끝말잇기 게임을 위한 data 변수와 메서드 및 코드를 중복 생성해야 합니다.
- props를 사용할 때 상위 컴포넌트에서 v-bind를 사용하지 않습니다.
<word-reply start-word="초밥"></word-reply> //옳은 문법
<word-reply v-bind:start-word="초밥"></word-reply> // 잘못된 문법
섹션 3 숫자야구
- npm i webpack webpack-cli -D
npm을 이용해 webpack을 설치할 수 있습니다. (-D는 개발용을 의미)
- webpack.config.js에서 통상 entry, module, plugins, output 4개를 정의합니다.
- entry는 어플리케이션 구조, module은 js이외에 관리할 확장자, plugins는 라이브러리, output은 webpack 빌드 결과를 정의합니다.
- splice를 활용해 랜덤한 숫자를 뽑을 수 있습니다.
const candidate = [1,2,3,4,5,6,7,8,9];
for (let i = 0; i < 4; i++) {
let chosen = candidate.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
}
- Math.random()은 0~1 사이 난수이며 Math.floor는 버림입니다.
ex) Math.floor(Math.random() * 9) 가능한 정수: 0~8
- splice 함수를 사용해 배열을 자를 수 있습니다.
ex) candidate.splice(6, 1): [7]
- candidate.splice(6,1)[0]하면 배열 값을 정수로 뽑을 수 있습니다: 7
섹션 4 반응속도
- package.json의 scripts에 자동으로 코드 변경을 감지해서 반영하도록 할 수 있습니다.
"scripts" : {
"build" : "webpack --watch",
"dev" : "webpack-dev-server --hot"
}
npm run build, npm run dev로 실행할 수 있으며 전자는 서버가 없고 후자는 서버가 존재합니다.
- v-bind를 class에 적용하여 다양한 css를 적용할 수 있습니다.
<template>
<div id="screen" :class="state"></div>
</template>
...
export default {
data() {
return {
state: "",
}
}
}
...
<script scoped>
#screen.waiting: black;
#screen.now: red;
</script>
:class="state" 에서 state는 data에 선언된 변수입니다. state가 변함에 따라서 css를 다르게 적용 할 수 있습니다.
- computed를 사용하면 data에 의해 template이 변경되는 것을 예방할 수 있습니다. 반대로, computed를 사용하지 않으면 data가 변경될 때마다 template를 다시 렌더링해야 합니다.
<template>
<div>{{ message }}</div>
//<div>{{ tries.splice((a,c) -> a + c, 0) / this.result.length || 0 }}</div>
<div>{{ average }}</div>
</template>
export default {
data() {
message: 'hello',
tries: []
},
computed: {
average() {
return return this.result.reduce((a, c) => a + c, 0) / this.result.length || 0;
}
}
}
만약 average를 사용하지 않고 template 내에서 tries 계산을 했으면, message가 변경될 때 tries를 또 계산해야 합니다. 하지만, computed에서 계산한 average를 활용하면 캐싱 효과가 있어서 값을 저장하고 가져다가 쓰면 됩니다. data를 그대로 사용하지 않고 가공이 필요한 경우 computed에 정의해 사용하면 캐싱에 도움이 됩니다.
- || 연산자는 null, undefined, NaN, false, 0, 빈 문자열을 체크해서 디폴트 값을 정할 수 있습니다.
- v-show는 display: none으로 화면에만 보이지 않도록 설정이 가능하며, v-if는 해당하지 않는 경우 화면에 그리지 않고 아예 존재 자체를 제거합니다.
- template을 사용하면 여러개의 요소를 하나의 빈 껍데기로 씌울 수 있습니다.
<template> // 아무런 의미가 없는 코드
<div>1</div>
<div>2</div>
</template> // 아무런 의미가 없는 코드
- css에도 v-bind를 사용하여 data에 따라 다양하게 적용이 가능합니다. :class="state" 를 선언하면, class 속성에 바인딩되는 값에 따라서 다른 css가 적용됩니다.
<template>
<div id="screen" :class="state" @click="onClickScreen">{{message}}</div>
</template>
...
export default {
data() {
return {
state: 'waiting'
}
},
methods: {
onClickScreen() {
if (this.state == 'waiting') {
this.state = 'ready';
...
}
}
},
<style scoped>
#screen {
width: 300px;
height: 200px;
text-align: center;
user-select: none;
}
#screen.waiting {
background-color: aqua;
}
#screen.ready {
background-color: red;
color: white;
}
#screen.now {
background-color: greenyellow;
}
</style>
}
섹션 5 가위바위보
- mounted()는 화면에 DOM이 모두 그려지고 동작할 코드를 정의합니다. 화면이 모두 렌더링되고, 가위바위보가 나와야 하므로 coumpted() 가 아닌 mounted()에 정의합니다.
- :class="state"와 마찬가지로 :style="computedStyleObject"로 css를 관리 할 수 있습니다. 섹션 4의 state의 경우, data()에 정의되어 [:class="data 변수"] -> class="waiting" 으로 사용되었으며, 섹션 5의 computedStyleObject의 경우, computed()에 정의되어 [:style="computed 변수"] -> style="background': 'url(...) 0 0" 으로 사용됩니다. 해당 변수에 맞는 실제 값이 대체됩니다.
- 백틱(`) 안에서 data에 있는 imgCoord는 ${this.imgCoord}로 표현합니다.
<template>
<div id="computer" :style="computedStyleObject"></div>
</template>
<script>
let interval = null;
let timeout = null;
const rspCoords = {
바위: '0',
가위: '-142px',
보 : '-284px',
}
export default {
data() {
return {
imgCoord: rspCoords.바위,
}
},
computed: {
computedStyleObject() {
return background: `url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${this.imgCoord} 0`,
}
},
methods: {
changeHand() {
interval = setInterval(() => {
if (this.imgCoord === rspCoords.바위) {
this.imgCoord = rspCoords.가위;
} else if (this.imgCoord === rspCoords.가위) {
this.imgCoord = rspCoords.보;
} else if (this.imgCoord === rspCoords.보) {
this.imgCoord = rspCoords.바위;
}
}, 100);
...
timeout = setTimeout(() => {
this.changeHand();
}, 1000);
},
},
mounted() {
this.changeHand();
},
beforeDestroy() {
clearInterval(interval);
clearTimeout(timeout);
}
}
</script>
setTimeout은 기본적으로 1번만 실행하며, 중첩 setTimeout으로 무한 실행할 수 있습니다. 정의한 함수 실행이 다 끝나고 대기를 시작합니다. setInterval은 기본적으로 무한정 실행하며, 함수시작과 동시에 대기시간이 시작됩니다.
섹션 6 로또 추첨기
- 로또를 화면에 그리는 작업은 섹션 5와 마찬가지로 DOM이 그려진 이후에 작동하므로, mounted()에 정의합니다.
mounted() {
this.showBalls();
}
- 부모 컴포넌트와 자식 컴포넌트는 props로 data를 주고 받을 수 있습니다. [:자식 컴포넌트props 속성 = "부모컴포넌트 data 변수"] => :number="ball"
//LottoGenerator.vue
<lotto-ball :v-for="ball in winBalls" :key="ball" :number="ball"></lotto-ball>
<lotto-ball :number="bonus"></lotto-ball>
data() {
return {
winBalls: [],
bonus: null,
}
}
//LottoBall.vue
props: {
number: Number
}
- watch를 사용하면 data의 변경을 비동기로 감지할 수 있지만, 많이 사용하는경우 예상치 못한 부작용이 발생할 수 있기 때문에 가급적 사용하지 않습니다.(watch에 선언한 data가 서로 영향을 주고받는 경우 무한 루프에 빠질 수 있습니다.)
- setTimeout()은 beforeDestroy에서 꼭 해제하여 메모리 누수를 예방한다.
beforeDestroy() {
timeout.forEach(t => {
clearTimeout(t);
});
}
섹션 7 틱택토
- vue의 배열에 값을 직접 할당하면 template에서 변화가 감지되지 않습니다. (관련 문서: 배열 변경 감지)
따라서 this.$set(), Vue.set()을 사용해야합니다.
this.$set(rootData.tableData[this.rowIndex], this.cellIndex, rootData.turn); // 클릭한 셀
- 자식 컴포넌트에서 상위 컴포넌트의 data를 호출 할 수 있습니다. this.$root.$data를 사용하면, 최상위 부모 컴포넌트의 data를 조회할 수 있으며, 바로 상위 부모 컴포넌트는 this.$parent.$data를 사용합니다.
- EventBus를 사용하여 부모-자식 컴포넌트 관계에 상관없이 컴포넌트간에 이벤트를 발생시킬 수 있습니다. EventBus를 사용하기 위해 비어있는 파일을 만든 후, 이벤트 등록을 위해 EventBus.$on('이벤트명',함수명), 이벤트 사용을 위해 EventBus.$emit('이벤트명', 매개변수)을 정의합니다. 보통 이벤트 등록은 created()에서 하며, 이벤트 해제는 beforeDestory()에서 합니다.
//EventBus.js
import Vue from 'vue';
export default new Vue();
//TicTacToe.vue
<script>
import EventBus from './EventBus';
export default {
methods: {
onClickTd() {
...
}
},
created() {
EventBus.$on('clickTd', this.onClickTd);
}
}
</script>
//TdComponent.vue
<script>
import EventBus from './EventBus';
export default {
props: {
rowIndex: Number,
cellIndex: Number
},
methods: {
onClickTd() {
EventBus.$emit('clickTd;, this.rowIndex, this.cellIndex);
}
}
}
</script>
- store(Vuex)에서 관리하는 데이터는 state에 정의합니다. 만약 state 데이터들이 여러 컴포넌트에서 사용된다면 각 컴포넌트는 store에서 관리하도록 합니다. 각 컴포넌트에서 공유 state를 사용하지 않으면, 데이터 동기화가 되지 않아 원하는 화면이 그려지지 않을 수 있습니다.
//store.js
const state: {
turn: '',
winner: 'O',
}
//TicTacToe.vue
export default {
//Vuex에서 데이터를 관리하므로 mapState 사용
computed() {
...mapState(['turn', 'winner'])
}
//data에 정의해서 사용하면 Vuex와 동기화가 안될 수도 있다.
data() {
return {
turn: '',
winner: 'O',
}
}
}
'학습 > Vue.js' 카테고리의 다른 글
computed vs watch vs method (0) | 2024.06.12 |
---|---|
[vue 터미널 실행 오류] "vue : 이 시스템에서 스크립트를 실행할 수 없으므로 파일을 로드할 수 없습니다" (0) | 2024.05.29 |