145 타입스크립트 리서치 3 - vue2 적용
source: categories/study/vue-experiance/vue-experiance_9-99_46.md
타입스크립트 리서치 3 - vue2 적용
vue2 타입스크립트 프로젝트 생성
- vue create vue2-ts : vue 프로젝트 생성 명령어
- Manually select features : 해당 프로젝트 기능들을 직접 선택
- 최소 Babel, TypeScript, Linter / Formatter 선택
- vue에서 TypeScript를 구성할 때 직접 구현하지 않고 Vue CLI에서 제공하는 TypeScript를 직접 선택해서 구성하는 것을 추천
- 2.x (vue 버전)
- Use class-style component syntax? n
- 개인적으로 class-style component를 추천하지 않는다.
- class component 형태가 아닌 extend 형태로 작성하는 것을 추천드린다.
- Use Babel alongside TypeScript? Y
- Babel 과 TypeScript 를 같이 사용할 것인지 묻고있다.
- Y 를 입력해 같이 구성한다.
- ESLint + Prettier : ESLint + Prettier 로 사용하는 것이 좋다.
- 마이크로소프트에서 타입스크립트를 ESLint로 개발하는 것으로 결정이 낫기 때문에 TSLint는 사용하지 않는 것을 추천드린다.
- Lint on save : 저장할 때마다 Lint 실행
- In dedicated config files : 설정 파일은 별도로 관리 (확장성을 높이기 위함)
- Save this as a preset for future projects? n
- 현재 프리셋을 미래 프로젝트를 위해 저장할 것인지에 대해 묻는 것이다.
- n 아니오 라고 답한다. (개인 기호에 따라 선택하면됨)
class 문법을 권장하지 않는 이유
2019.05.21 vue 창시자 evan you 글
update: the class API proposal is being dropped
- composition API: Vue 3의 가장 핵심적인 API
- 인스턴스 옵션 속성이 하나 추가된 것이라고 보면된다.
- 이러한 논의가 2019년 5월부터 있었다.
- vue 같은 경우는 다른 프레임워크와 다르게 커뮤니티와 많은 대화를 나누고 그리고 우리가 이런 방향으로 갈테니까 너네가 한번 봐줄래? 어때? 이런 문화가 발달해있다.
- RFC(Request For Comment)
- RFC라고 해서 우리가 신규 기능을 이렇게 개발해나갈건데, 너네가 이거를 어떻게 받아들이는지 의견을 줘 이런 것이다.
- 2019년 5월 evan you가 우리가 제안했던 class API를 드랍시키겠다고 발표했다.
- Angular 같은 느낌인 class 문법으로 Vue 3를 만들어야겠다 라고 생각하다가 결국 class API를 드랍시킨 것이다.
- 최종적으로 나온 형태
- vue3에 setup 이라는 옵션이 생겼다.
- 그리고 setup 옵션 안에 여러가지 API들이 제공된다.
- vue2에선 vue component를 작성할 때 Single File Component 내에서
data
,methods
,computed
등을 썼었다. - vue3에선 그거와 마찬가지로
setup
이라는 옵션이 추가되었다. (setup은 vue3에서 추가된 인스턴스 속성) - 이
setup
이라는 속성은 써도되고 안 써도 된다.
-
class API 에 대한 문법들을 완전히 드랍시켰다기 보다는 Vue3가 나아가는 방향에 Class API가 더 이상 없다는 것이다.
-
class API를 권장하지 않는 이유는 다음과 같다.
<!-- Child.vue --> <template> <div class="hello"> <h1>{{ msg }}</h1> </div> </template> <script lang="ts"> import { Vue, Component, Prop } from 'vue-property-decorator'; @Component export default class Child extends Vue { @Prop() private msg!: string; } </script>
- 위 코드를 보면 Angular 느낌이 난다. (별로 내키지않는다.)
- 이는 Vue의 정체성을 많이 훼손시키는 문법이라고 생각한다.
- 그렇다고 꼭 위 문법을 vue3에서 사용 못하게 한다는 것이 아니다.
- 앞으로 위 코드 방향으로 진화하진 않겠다는 뜻이다.
- 지금 vue 프로젝트에 타입스크립트를 시작하시는 분들이라면 기존 프로젝트에선 위와 같이 Class 문법으로해서
vue-property-decorator
로 추가적인 문법들을 배우지 않으시는걸 권장한다. vue extend
문법만 사용해도 타입스크립트의 이점을 충분히 다 가져갈 수 있다.
- 이미 많은 실무 프로젝트들이 class 문법으로 vue에서 typescript를 적용했을 확률이 크다.
- 그랬을 때 그걸 당장 버리고 바꿔야된다가 아니라 추후에 뭔가 개선하게된다면 vue3를 가지고 개선하게될 확률이 큰데
그때vue extend
스타일로 개선하면 된다.
2019.05.21 Even You - 클래스 API 제안이 드랍된 이유
왜?
두가지 주요 이유
- 현재 클래스 API 제안에는 여전히 다양한 엣지 케이스/사양 의존도/해결되지 않은 질문이 있다.
- 고급 반응성 API(Advanced Reactivity API) 및 동적 라이프 사이클 주입(Dynamic Lifecycle Injection)에서 제안된 새로운 API는 Class 보다 더 나은 구성요소(Composition) API 역할을 할 수 있는
Composition Function
이라는 새로운 패턴을 가능하게한다.(일시적으로)
Composition Function
과 Class API
를 모두 제공하면 기본적으로 동일한 작업을 수행하게하는 코드를 작성할 때 3가지 방법으로 작성할 수 있게됩니다.
이것은 우리가 어떤 희생을 치르더라도 피하고 싶은 것입니다.
Class API
에 비해 Composition Function
이 어떤 장점을 가지고 있는지는 아래에서 더 자세하게 설명하겠습니다.
현재 Class 제안의 문제점
타이핑 복잡성
- RFC(Request For Comment)에 명시된 대로 Class API를 도입하는 주요 목표는 더 나은 TypeScript 추론 지원과, 그와함께 제공되는 대체 API를 제공하는 것입니다.
-
그러나 Vue Component가 여러 소스에서 선언된 속성을 단일 this 컨텍스트로 병합해야 한다는 사실은 Class 기반 API에서도 약간의 문제를 만듭니다.
- 한가지 예는
props
의 타이핑입니다. this
에props
를 병합하려면component class
에 대한 일반argument
를 사용하거나decorator
를 사용해야 합니다.- 하지만 둘 다 문제가 있습니다.
- 일반
argument
에는 여전히 런타임props
옵션 선언이 필요합니다.- 이로인해 어색하고 중복되는 이중 선언이 발생합니다.
decorators
를 사용하면 특히TypeScript
의 현재 구현이TC39
제안과 완전히 동기화되지 않은 경우 많은 불확실성이 있는 2단계 사양에 의존하게됩니다.- 또한
this.$props
에decorators
로 선언된props
유형을 노출할 방법이 없습니다.- 이는
TSX
지원을 중단합니다.
- 이는
- 또한 현재
Class Methods
의arguments
에 대한 컨텍스트 타입 지정을 활용할 수 있는 방법이 없습니다. - 즉, Class의 렌더링 함수에 전달된
arguments
는 Class의 다른 properties를 기반으로 유추된 형식을 가질 수 없습니다.
- 일반
- 한가지 예는
예약된 메서드의 네임스페이스
우리는 Class 선언에서 "예약된" 메서드를 처리하는 방법에 대해 논의했습니다.
이 설명은 @glen-84
에 의해 매우 잘 요약되어 있습니다.
제안된 솔루션 중 어느 것도 완벽해보이지 않습니다.
구현 복잡성
Class를 지원하는 것은 추가적인 수많은 엣지 케이스를 내부 코드로 다루어야된다는 것을 뜻합니다.
이것은 반응성(Reactivity) 추적을 위해 this
로 Proxy를 사용해야 하는 우리의 필요성과 생성자의 this
(따라서 모든 클래스 필드 이니셜라이저)가 다른 모든 위치의 this
와 다를 것이라는 사실과 주로 관련이 있습니다. (모든 this
가 다 제각기 다르기 때문에 수많은 엣지 케이스를 내부 코드로 다뤄야된다는 불편함이 생긴다는 뜻인 것 같다.)
또한 Class와 Object Composition Component 선언(트리쉐이킹 할 수 없는)간에 앞뒤로 변환하는 코드를 포함해야합니다.
의심스러운 이점
위의 모든 문제로 인해 Class API를 도입함으로써 얻을 수 있는 이점은 의심스럽습니다.
- 주요 목적을 잘 달성하지 못합니다. (더 나은 TypeScript 지원이라는 목적)
- 내부 구현을 복잡하게 만듭니다.
- 논리 구성을 개선하지 않습니다.
대체: Composition Functions
Class API 이후에 Advanced Reacitvity API 와 Dynamic Lifecycle Injection 이라는 두 가지 새로운 API 세트도 제안했습니다.
이것들을 결합하여 우리는 컴포넌트 로직을 선언하는 새로운 방법인 함수 호출을 사용하는 방법을 고안해냈습니다.
이것은 React Hooks에서 영감을 받았지만 Vue의 자체 반응성(reactivity) 시스템에 뿌리를 두고 있습니다.
Feature Parity (동등한 기능)
composition functions
를 사용하면 component
의 논리가 주로 새 setup()
함수 내에서 함수 호출을 사용하여 선언됩니다. (이것은 본질적으로 data()
이지만 목적에 더 잘 맞도록 이름을 setup
으로 지었습니다.)
이러한 함수 호출은 거의 모든 기존 component options
와 기능이 동일합니다.
// everything tree-shakable
import {
value,
computed,
watch,
onMounted,
inject
} from 'vue'
const App = {
// same as before
props: {
a: String,
b: Number
},
// same as before
components: {
// ...
},
setup(props) {
// data
const count = value(1)
// computed
const plusOne = computed(() => count.value + 1)
// methods
function inc() {
count.value++
}
// watch
watch(() => props.b + count.value, val => {
console.log('changed: ', val)
})
// lifecycle
onMounted(() => {
console.log('mounted!')
})
// dependency injection
const injected = inject(SomeSymbol)
// other options like el, extends and mixins are no longer necessary
// expose bindings on render context
// any value containers will be unwrapped when exposed
// any non-containers will be exposed as-is, including functions
return {
count,
plusOne,
inc,
injected
}
},
// template: `same as before`,
render({ state, props, slots }) {
// `this` points to the render context and works same as before (exposes everything)
// `state` exposes bindings returned from `setup()` (with value wrappers unwrapped)
}
}
더 나은 TypeScript 지원
위의 예는 argument 위치에 있는한 예외 케이스 없이 완벽하게 입력할 수 있습니다.
import { createComponent } from 'vue'
const App = createComponent({
props: {
// ...
},
setup(props) {
// infer : 유추하다.
// props type inferred from `props` option
// composition functions are all easily typed since they
// don't rely on `this`
return {
// ...
}
},
render({ state, props }) {
// `state` type inferred from return value of setup()
// `this` type inferred from a merge of state and props
}
})
// The resulting type of `App` also supports TSX props inference
내부 타이핑도 훨씬 덜 복잡합니다.
더 나은 Composition 기능
여기에 설명된 것처럼 Composition Function을 사용하면 옵션이 아닌 주제별로 논리를 그룹화하기가 매우 쉽습니다.
또한 mixins
의 단점(네입스페이스 충돌 및 불분명한 속성 소스) 없이 다양한 Composition 요소에서 쉽게 추출 및 재사용할 수 있습니다.
더 나은 Compatibility (호환성)
Composition Functions은 2.x 개체 구문 위에 완전히 호환되는 확장(extension)으로 볼 수 있습니다.
이는 마이그레이션이 점진적으로 수행될 수 있음을 의미합니다.
이것이 Class API를 삭제해야하는 설득력있는 이유를 제공하기를 바랍니다.
Composition Functions을 공식적으로 도입하기 위해 별도의 RFC를 마련할 것입니다.
vue.js에서 타입스크립트를 적용하는 방법
- 서비스를 처음 구축할 때부터 타입스크립트를 사용한다.
- 이 경우는 거의 없다. vue.js는 2014년도에 나온 프레임워크이다.
- 2018년부터 여러 기업에서 vue.js로 정말 많은 서비스를 만들고있다. 즉, 이미 구축되어있는 서비스가 많다.
- 이런 서비스에 타입스크립트를 점진적으로 입혀나가는 경우가 많지 애초에 처음부터 타입스크립트를 설정해서 작업해나가는 경우는 거의 없다.
- 기존에 이미 구현된 서비스에 타입스크립트를 점진적으로 적용한다.
- 타입스크립트는 자바스크립트와 별개의 언어이다.
- 따라서 타입스크립트를 점진적으로 적용할 수 있기 때문에 무조건 처음부터 적용해야된다라는 생각은 안해도된다.
tsconfig.json
- tsconfig.json
- 타입스크립트가 현재 프로젝트 폴더에 대해 어떻게 해석해 나갈 것인지를 정해주는 설정 파일이다.
compilerOptions
"target": "esnext"
- target: string (<- 마우스커서 오버시 뜨는 설명)
- Set the JavaScript language version for emitted JavaScript and include compatible library declarations.
- 내보내는 JavaScript 버전(몇 버전으로 내보낼지)을 설정하고 호환되는 라이브러리 선언을 포함합니다.
- target: string (<- 마우스커서 오버시 뜨는 설명)
"module": "esnext"
- module: string (<- 마우스커서 오버시 뜨는 설명)
- Specify what module code is generated.
- 생성되는 모듈 코드를 지정합니다.
- module: string (<- 마우스커서 오버시 뜨는 설명)
"strict": true
- strict: boolean (<- 마우스커서 오버시 뜨는 설명)
- Enable all strict type checking options.
- 모든 엄격한 타입 검사 옵션을 활성화합니다.
- strict: boolean (<- 마우스커서 오버시 뜨는 설명)
- 각 옵션에 마우스 커서를 오버하면 설명이 뜹니다.
- 프로젝트 시작시 타입스크립트를 자동 세팅되게 설정하면 이렇게 vue 코어팀에서 만들어놓은 설정을 그대로 가져다 쓸 수 있다.
tsconfig.json의 paths 속성 관련해서..
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
vue.js 옛 버전에서 jsconfig.json(VScode 내부에서 사용하는 설정파일)
같은 설정 파일로 component 내에서 @/
했을 때, 해당 문구를 src/
로 인식하게끔하는 그런 기능들을 봤을겁니다.
지금 vue 버전은 @
, ~
이런 것들이 이미 내재되어 있습니다.
- 이전엔
jsconfig.json(VScode 내부에서 사용하는 설정 파일)
파일에서 위tsconfig.json
과 동일하게compilerOptions
의paths
에서@/
를 설정했었다. tsconfig.json
은 타입스크립트 설정 파일이지만 에디터에서 이를 인식한다.
즉, 각 에디터에서 이tsconfig.json
을 인식해 똑같이@/
를 인식한다.- 즉,
jsconfig.json
을 따로 설정 안해도된다.
babel.config.js
module.exports = {
preset: ['@vue/cli-plugin-babel/preset'],
}
- vue 내부적으로 이미
babel preset
설정을 해놓았다. 그대로 사용한다.
.eslintrc.js
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
};
.eslintrc.js
도 이미prettier
까지 설정되어있다.rules
안에 원하는prettier
옵션 값을 지정하면된다.
main.ts
- 기존
main.js
에서main.ts
로 바뀐 것뿐이다.
shims-vue.d.ts
- 타입스크립트는 해당 프로젝트를 보고 해석을 한다.
- 그렇게 하기위해서 타입스크립트 내부적으로
TS Language Server
가 작동을 한다.- 이를 흔히 타입스크립트 컴파일이라고 한다.
- 여튼 프로젝트 폴더 내부적으로 파일들의 관계를 해석하거나 타입스크립트의 타입 추론 기능을 제공하기 위해
TS Language Server
가 작동하고 있다 라고 보면된다.
- 그런데 타입스크립트는
*.vue
파일을 제대로 인식 못한다.- 그 이유는 모르는 확장자이기 때문이다.
- 이를
shims-vue.d.ts
파일에 명시해준다. 타입스크립트에게 알려주기 위함이다. -
아래 코드는
*.vue
파일을 모두Vue
타입으로 인식하라는 뜻이다.declare module "*.vue" { // *.vue 파일을 만나면 아래 코드로 인식해줘! import Vue from 'vue'; export default Vue; }
이렇게 모든
*.vue
파일을 타입스크립트가 제대로 인식하게 해주면된다.
Controlled Component
- controlled component
- props를 내리고 event를 올리는 과정에서 한글 입력, IME 처리까지 어떻게 할 수 있는지 알아보도록 하겠다.
<!-- src/components/TodoInput.vue -->
<template>
<div>
<label for="todo-input"></label>
<input type="text" id="todo-input">
<button @click="addTodo" type="button">add</button>
</div>
</template>
<script>
export default {
name: "TodoInput",
methods: {
addTodo() {
console.log("add");
}
}
}
</script>
<style scoped></style>
<!-- src/App.vue -->
<template>
<div>
<h1>Vue Todo with TypeScript</h1>
<todo-input></todo-input>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import TodoInput from '@/components/TodoInput.vue';
export default Vue.extend({
name: "App",
components: {
TodoInput,
}
})
</script>
인풋 컴포넌트에 내려줄 data와 props 속성 정의
- container component / presentational component
- presentational component가 data를 내려주어 data는 따로 처리한다.
- 즉, data를 처리하는 container component와 화면에 그 데이터를 보여주기만하는 presentational component 이렇게 나누어 구조를 이루면된다.
<!-- src/components/TodoInput.vue -->
<template>
<div>
<label for="todo-input"></label>
- <input type="text" id="todo-input">
+ <input type="text" id="todo-input" :value="item">
<button @click="addTodo" type="button">add</button>
</div>
</template>
<script>
export default {
name: "TodoInput",
+ props: ["item"],
methods: {
addTodo() {
console.log("add");
}
}
}
</script>
<style scoped></style>
<!-- src/App.vue -->
<template>
<div>
<h1>Vue Todo with TypeScript</h1>
- <todo-input></todo-input>
+ <todo-input :item="todoText"></todo-input>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import TodoInput from '@/components/TodoInput.vue';
export default Vue.extend({
name: "App",
components: {
TodoInput,
},
+ data() {
+ return {
+ todoText: "",
+ }
+ }
})
</script>
인풋 컴포넌트의 emit 이벤트 정의 및 구현
<!-- src/components/TodoInput.vue -->
<template>
<div>
<label for="todo-input"></label>
- <input type="text" id="todo-input" :value="item">
+ <input type="text" id="todo-input" :value="item" @input="handleInput">
<button @click="addTodo" type="button">add</button>
</div>
</template>
- <script>
- export default {
+ <script lang="ts">
+ import Vue from 'vue';
+ export default Vue.extend({
name: "TodoInput",
props: ["item"],
methods: {
// tsconfig.js - strict: true 설정에 noImplicitAny: true도 포함되어있다.
// 하다못해 any 타입이라도 정의해라 라는 뜻이다.
// noImplicitAny의 default 값은 false이다.
// 일단 이번 시간엔 event emit이 목적이므로 any로 두고 진행하도록 하겠다.
handleInput(event: any) {
// 여기서의 input 이벤트와 위의 input 태그의 input 이벤트는 서로 다른 것이다.
// 위의 input 태그의 input 이벤트는 키보드 input 이벤트이고
// 아래 input 이벤트는 컴포넌트간 통신 수단이다.
this.$emit("input", event.target.value);
},
addTodo() {
console.log("add");
}
}
- };
+ });
</script>
<style scoped></style>
할 일 추가 버튼 기능 구현
- diff 링크
- 하위 컴포넌트에선
event emit
, 상위 컴포넌트에선props
- 이렇게 작성하면 v-model과 같이 양방향 통신을 할 수 있게된다.
- 이런 방식으로 v-model을 구현한다.
- 이런 방식을
controlled component
라고 한다. - 그런데 왜 위와 같은 방법으로
v-model
을 구현할까?v-model
속성을 직접 사용하면 안되나?
- 하위 컴포넌트에선
v-model을 풀어서 구현하는 방식의 장점
v-model을 사용하면 정확도가 떨어진다
input
에 글을 입력하면 반응서이 좀 늦는다.- 이 말의 뜻이 뭐냐면,
input
창엔 "안녕하세요"를 입력했는데,v-model
과 연동된data
엔 "안녕하세"까지만 저장되어있는 경우가 있다.
- 이 말의 뜻이 뭐냐면,
- 위와 같은 현상은
vue2
에서 가지고 있던 한계점이라고 보면된다.- vue 커뮤니티에선 IME 표준 스펙을 따랐다곤 했지만, 한국, 중국, 일본어를 처리할 때 이런 미숙함이 있는 것도 사실이다.
-
위와 같은 현상은
vue3
에선 해결되었다. - 여튼
v-model
을 사용하지 않고,props
,event emit
으로 나누면vue2
에서 위와 같은 문제를 해결할 수 있다.
작은 단위로 쪼갠 input component
- 재사용성 증가
- 관리 용이성
- 한글 입력에 대한 엣지 케이스 보완 가능
@input
, :value
형태로 정의하면 바로 v-model
속성 적용 가능
- diff 링크
v-model
문법을 적용하려면@input
과:value
로 정의해야한다.- 반드시
@input
만은 아니다.@change
도 되고 다른 이벤트도 된다. - 이는 문서를 참조해봐야된다.
- 반드시
인풋 컴포넌트 props 속성 유효성 검사 및 타입 정의
- diff 링크
- validation은 굉장히 중요하다.
- component에 원하지않는 형식의 데이터가 내려왔을 때 에러로 인식하도록 만드는 것이 중요하다.
<!-- src/components/TodoInput.vue -->
<template>
<div>
<label for="todo-input"></label>
<input type="text" id="todo-input" :value="item" @input="handleInput">
<button @click="addTodo" type="button">add</button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: "TodoInput",
props: ["item"],
methods: {
handleInput(event: any) {
console.log(event); // event 타입을 알기 위해 콘솔로 찍어보자
this.$emit("input", event.target.value);
},
addTodo() {
console.log("add");
}
}
});
</script>
<style scoped></style>
InputEvent {...}
// 위와 같이 InputEvent라고 나온다.
// target은 HTMLInputElement인 것을 알 수 있다.
v-model
로 바꿨던 것을 다시props
,event emit
으로 나눈다.- 하위 컴포넌트에서 올라오는
input
이벤트를 감지하여 실행되는updateTodoText
함수의 첫번째 인자는string
타입이다. - 하위 컴포넌트에서 키보드 이벤트로 발생하는
input
이벤트에 걸어놓은 핸들러 함수의 첫번째 인자의 타입은InputEvent
이다.this.$emit("input", event.target.value);
에서 에러가 발생한다.- 그 이유는
event.target
이null
일 수도 있기 때문이다. null
이 아니라는 것을 보장해줘야된다. 어떻게?- 타입 단언(
as
)을 사용한다. - 타입 단언은 약간은 위험한, 안전하지 않은 방법이다.
- 일단은 타입 단언으로 타입을 정의하고 나중에 이를 좀 더 패턴화해서 모듈화해보자.
const eventTarget = event.target as HTMLInputElement;
this.$emit("input", eventTarget.value);
할 일 아이템 목록 컴포넌트 설계 및 구현
에러 발생시 페이지에서 에러 알림창 뜨는거 막는 방법
vue.config.js
에서overlay: false
설정하면된다.
데이터 조회 API 설계
- diff
localstorage
에 저장되는 값key: value
형태key
와value
는 서로 별개로 유니크한 값으로 관리하는 것이 더 좋다.key
를 상수값으로 넣고value
에 들어가는 값들은 직렬화(String)해서 집어넣는게 좋다.- 배열이면 배열을 직렬화(String)하는 것이 좋다.
- 마찬가지로
value
를 다시 꺼내서 사용할 때, 가공해서 사용하는 것이 좋다.
- 예시코드
// v1이란 key
// value는 배열 형태인데 String화
// value를 넣고 뺄 때 변환만 해주면 v1이란 key를 기준으로 데이터 관리가 된다.
localStorage.setItem('v1', '[{ title: "하이", id: 1 }, { title: "1000", id: 2 }]')
데이터 조회 API 타입 에러와 JSON 파싱 에러 해결
"[{ title: '하이', id: 1 }, { title: '1000', id: 2 }]"
위를 일반 JSON 형태로 parsing하려면 title
이나 하이
같은 것들이 전부 쌍따옴표로 감싸져야된다.
const STORAGE_KEY = 'vue-todo-ts-v1';
const todoItems = localStorage.getItem(STORAGE_KEY) || [];
JSON.parse(todoItems); // todoItems error
// TS2345: Argument of type 'string | never[]' is not assignable to parameter of type 'string'. Type 'never[]' is not assignable to type 'string'.
// const todoItems: string | never[]
// 여기서 궁금한점
// todoItems는 왜 never[] 타입으로 추론되었을까? string 타입으로 추론된거까진 이해하는데, 왜 never[]일까?
// 사실 위와 같이만 작성한다면 string | any[] 타입으로 추론되는데, 위 diff의 예시코드에서의 추론은 string | never[] 타입으로 추론된다.
// 왜 never[] 타입인지 잘 이해가 안간다.
- 배열 타입을 정의하지 않으면
never[]
로 추론되는건가?- 어떤 경우엔
any[]
로 추론되는거 같고.. - 어떤 경우엔
never[]
로 추론되는거 같고.. - 그 어떤 경우가 확실치가 않다. 더 조사를 해봐야될 것 같다.
- 배열 내부 요소의 유형을 정해주지 않으면 어떤 요소가 들어와도 에러로 인식한다고 한다.
- 배열에 요소를 집어넣을 때 해당 배열의 요소의 유형이 정해져있어야된다. 안그러면 에러로 인식한다.
- 어떤 경우엔
- 왜
never[]
타입으로 추론되는걸까.
할 일 추가 API 설계 및 구현
할 일 조회 기능 구현
삭제 기능 마크업 및 이벤트 연동
이벤트 연동시 주의할점
props
,event emit
통신방법- 가급적 유지
vuex
또는mixins
를 사용하다보면 나중에 데이터 흐름 파악하기 어려워질 수 있다.- 몇 단계를 거치더라도
event emit
으로 단계 거치게하는 것이 데이터 흐름 파악에 보다 더 직관적이다. - 그렇다면
component
깊이가 깊어질수록 중간에event emit
을 여러번 거쳐야되는데 그러기엔 너무 귀찮다.- 그래서
vuex
를 사용한다. - 이런 문제에 대한
TIP
을 드리자면..
- 그래서
<template>
<li>
<span></span>
<button @click="$emit()">삭제</button>
</li>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "TodoListItem",
props: {
todoItem: {
type: String,
},
},
});
</script>
<style scoped></style>
- 위와 같이 바로
$emit()
을 통해event
를 올릴 수 있다. - 실무에서 위와 같은 코딩 방식은 허용 가능한 범위의 코딩 방식이다.
-
하지만 위와 같이
html
내부에js
코드가 들어가면 나중에 테스트 코딩을 작성하기 매우 어려워진다. - 기본적으로 테스트 코드를 작성할 때, 위 컴포넌트를 모킹하게되는데, 모킹할 때 각각의 테스트 범주에 해당하지 않는 것들은 건드리지 않아야된다.
- 그런데 위와 같이 작성하면 다 건드리게 되어있다.
- 그래서 가급적이면 테스트 가능한 코드를 위해서
html
내엔js
코드 작성을 안하는 방식으로 해야된다. - 아래와 같은 방식으로
<template>
<li>
<span></span>
<button @click="removeItem">삭제</button>
</li>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "TodoListItem",
props: {
todoItem: {
type: String,
},
index: {
type: Number,
},
},
methods: {
removeItem() {
this.$emit("delete", this.index);
},
},
});
</script>
<style scoped></style>
할 일 삭제 API 연동 및 구현
vue에 scss 기능 추가하기
yarn add -D sass sass-loader@10
sass-loader
10 버전을 설치하는 이유는 vue2.x
에선 11 이상의 sass-loader
가 호환되지 않아서이다.
위와 같이 설치하면 자동으로 vue
에서 scss
를 인식한다.
webpack.config.js
에 이미 다 설정되어있기 때문이다.
할 일 목록의 데이터 타입 정의 및 할 일 추가 기능에 적용
- diff
- 현재
todoItems
각 요소는String
- 이 요소들을
Object
로..title
,done
이렇게 2가지 프로퍼티 키, 값을 가지고있는 객체 형태로 만든다. - 배열 내부 요소의 유형을 정해주지 않으면 어떤 요소가 들어와도 에러로 인식한다고 한다.
- 배열에 요소를 집어넣을 때 해당 배열의 요소의 유형이 정해져있어야된다. 안그러면 에러로 인식한다.
- 이 요소들을
vue.runtime.esm.js?2b0e:619 [Vue warn]: Invalid prop: type check failed for prop "todoItem". Expected String, got Object
found in
---> <TodoListItem> at src/components/TodoListItem.vue
<App> at src/App.vue
<Root>
- 위 에러 해결은 다음 시간에 해보도록 하겠다.
props 타입 정의 방법
할 일 완료 기능 구현
할 일 완료 클래스 바인딩 및 computed 타입 정의 방식 안내
FE CONF 2019
목차
- 반응성 - 왜 내 화면은 다시 그려지지 않는걸까?
- DOM 조작 - 오래된 습관 버리기
- 라이프사이클 - 나는 인스턴스를 얼마나 알고있나?
- ref 속성 - 만들다가만 ref 속성
- computed 속성 - 간결한 템플릿의 완성
발표 대상
- Vue.js의 기본 문법을 알고 있는 웹 개발자
- Vue.js로 막 서비스 개발을 시작한 주니어 개발자
- Vue.js로 서비스를 제작중인 웹 개발자
-
Vue.js의 특징이 궁금한 프론트엔드 개발자
- "Vue.js를 소개하는 발표가 아닙니다."
기대 효과
- PR 보냈을 때 사수(시니어)한테 칭찬 받기
발표 자료 및 예제 소스는 아래 링크에서 확인
반응성
- 반응성
- 왜 내 화면은 다시 그려지지 않는걸까?
Vue의 반응성이란?
- 데이터의 변화에 따라 화면이 다시 그려지는 Vue의 성질
var vm = new Vue({
data: {
count: 0,
}
})
vm.count += 1; // count 값이 증가하면 화면에 표시된 count도 증가
반응성은 언제 설정될까?
- 인스턴스가 생성될 때
data
의 속성들을 초기화: Vue 라이브러리 동작- new Vue(): 인스턴스 생성
- Init Events & LifeCycle
- beforeCreate
- Init injections & reactivity
- created - 여기부터 반응성이 주입된 데이터 속성들을 다룰 수 있다.
반응성에 대해 알아야할 점
- 생성하는 시점에 없었던
data
는 반응하지 않는다.
var vm = new Vue({
data: {
user: {
name: 'Captain',
}
}
})
vm.user.age += 1; // age 값이 변하더라도 화면은 갱신되지 않는다.
반응성을 이해하지 못했을 때의 실수 1.
- 화면에서만 필요한 UI 상태 값을 다룰 때
- 체크박스
- 라디오 버튼
export default {
data() {
return {
users: [],
}
},
methods: {
fetchUsers() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
this.users = data;
this.$set(this.users[0], 'checked', false);
})
.catch(error => console.log(error));
}
}
}
- REST로 받아온 유저 목록이 10개라고 가정하자.
- 유저 목록 중 첫번째 유저를 체크하는 함수를 만들고 그 버튼을 만들었다.
- 버튼을 클릭하면 유저 목록 중 첫번째 유저를 체크 (
checked = true
) - 분명 자바스크립트 상으론
checked
가true
로 되었지만 화면에선 갱신이 안된다. -
그 반대로
checked = false
도 화면 갱신이 안된다. - vue developer 확장 프로그램으로봐도 해당
data
는true
,false
로 잘 바뀌지만 화면엔 갱신이 안된다. - 이런 이슈 해결 방법
this.$set(this.users[0], 'checked', false)
- root 수준의 data는
this.$set()
으로 해도 안되지만, root 수준의 data 안에 이렇게 정의를 하면 반응성이 적용된다.
- 위와 같이 수정하면
checked
값 변화에 따라 화면이 잘 갱신된다.
이런 UI 상태 값은 보통 DB에서 관리 안하고 화면에서 관리해줘야하는 값들이라 이런 실수를 종종 내곤한다.
반응성을 이해하지 못했을 때의 실수 2
- 백엔드에서 불러온 데이터에 임의 값을 추가하여 사용하는 경우
const user = {
address: {
},
company: {
},
email: 'Sincere@april.biz',
id: 1,
name: 'Leanne Graham',
phone: '1-770-736-8031 x56442',
username: 'Bret',
website: 'hildegard.org'
}
- 위 코드에
region
이라는 새로운 프로퍼티를 추가해서 화면과 연동region
이란 값은 자바스크립트 상에서 잘 바뀌나 화면에 갱신되지 않는다.- 해당 프로퍼티들에
reactivity
가 주입되었는지 아닌지 확인하는 방법은 다음과 같다.- console.log(해당프로퍼티)
- 해당 프로퍼티에 대한 객체가 콘솔창에 찍힐 것이다.
reactivity
가 주입되어있다면get
,set
이 설정되어있다.reactivity
가 주임 안된region
은get
,set
이 설정되어 있지 않다.
- 이슈 해결 방법
- 아까와 같이
this.$set()
함수 이용 - 또는 팩토리 펑션 비슷하게 속성을 반환해주는 함수를 만들어놓고 호출하거나
- 아니면
data
속성 안에 일일이 지정해놓고 사용하거나
- 아까와 같이
팩토리 펑션이란?
- 함수와 객체를 배우지 않고서는 자바스크립트 프로그래머로서 더 나아갈 수 없을 것이며,
- 함수와 객체, 이 두가지가 함께 사용되는 경우 조합이라 불리는 강력한 객체 패러다임을 시작하는데 필요로하는 빌딩 블록이된다.
-
오늘 우리는 함수, 객체와 프로미스들을 조합하기 위해 사용되는 팩토리 함수의 몇 가지 관용적 패턴들을 살펴볼 것이다.
- 함수가 객체를 반환할 때 이것을 팩토리 함수라고 부른다.
function createJelly() {
return {
type: 'jelly',
colour: 'red',
scoops: 3,
}
}
이 팩토리 함수를 호출하면 매번 jelly
객체의 새로운 인스턴스를 반환할 것이다.
팩토리 함수명으로 create
접두사를 꼭 붙일 필요는 없지만, 명확하게 함수 의도를 전달할 수 있다는 점은 참고해두자.
마찬가지로 type
프로퍼티도 항상 사용할 필요는 없으며, 이것은 프로그램 상에서 흘러다니는 객체들을 구별할 수 있도록 도와준다.
파라미터를 받는 팩토리 함수
모든 함수들처럼, 파라미터로 객체를 변경하여 반환할 수 있는 팩토리를 정의할 수 있다.
function createIceCream(flavour = 'Vanilla') {
return {
type: 'icecream',
scoops: 3,
flavour,
}
}
이론상 다수의 인자값을 파라미터로 받아 특이하고 복잡하게 얽혀있는 객체를 반환하는 팩토리 함수를 사용할 수 있다.
하지만 앞으로 우리가 살펴볼 것처럼, 이것은 조합의 의도와 무관하다.
조합 가능한 팩토리 함수
다른 팩토리 함수를 사용해 팩토리 함수를 정의하면 더 작고 재사용 가능한 조각으로 복잡한 팩토리 함수들을 분리할 수 있다.
예를 들어, jelly
와 icecream
팩토리로 생성된 객체를 반환하는 dessert
팩토리 함수를 만들 수 있다.
function createJelly() {
return {
type: 'jelly',
colour: 'red',
scoops: 3,
}
}
function createIceCream(flavour = 'Vanilla') {
return {
type: 'icecream',
scoops: 3,
flavour,
}
}
function createDessert() {
return {
type: 'dessert',
bowl: [
createJelly(),
createIceCream(),
]
}
}
new
또는 this
없이 임의로 복잡한 객체를 생성하기위해 팩토리 함수를 조합할 수 있다.
is-a
(동등)보다 has-a
(포함) 관계로써 표현되는 객체는 상속 대신에 조합으로 구현될 수 있다.
상속 예시 (이 예시 옳은건가..? prototype을 prototype? 여튼 일단 이렇게 적혀있긴함)
// A trifle *is a* dessert
function Trifle() {
Dessert.apply(this, arguments);
}
Trifle.prototype = Dessert.prototype;
class Trifle extends Dessert {
constructor() {
super();
}
}
팩토리조합 예시
// A trifle *has* layers of jelly, custard and cream. It also *has* topping.
function createTrifle() {
return {
type: 'trifle',
layers: [
createJelly(),
createCustard(),
createCream(),
],
topping: createAlmonds(),
};
}
비동기 팩토리 함수
- 모든 팩토리 함수들이 즉시 데이터를 반환하도록 준비되진 않을 것이다.
-
예로 일부 팩토리 함수들은 처음에 데이터를 패치해야만 한다.
- 이 경우, 프로미스를 대신 반환하는 팩토리 함수를 정의할 수 있다.
function getMeal(menuUrl) {
return new Promise((resolve, reject) => {
fetch(menuUrl)
.then(result => {
resolve({
type: 'meal',
courses: result.json()
})
})
.catch(reject);
})
}
- 이런 종류의 복잡한 중첩 코드는 비동기 팩토리 함수를 읽고 테스트하기 어렵게 만든다.
- 이 코드는 2개의 팩토리 함수로 나눌 수 있으며 이를 조합해보자.
function getMeal(menuUrl) {
return fetch(menuUrl)
.then(result => result.json())
.then(json => createMeal(json));
}
function createMeal(courses = []) {
return {
type: 'meal',
courses,
}
}
- 콜백을 사용할 수도 있지만, 우리는 이미 프로미스 객체들을 반환하는 조합 팩토리 함수인
Promise.all
을 가지고 있다.
function getMeal(menuUrl) {
return fetch(menuUrl)
.then(result => result.json())
.then(json => createMeal(json));
}
function getWeeksMeals() {
const menuUrl = 'jsfood.com/';
return Promise.all([
getMeal(`${menuUrl}/monday`),
getMeal(`${menuUrl}/tuesday`),
getMeal(`${menuUrl}/wednesday`),
getMeal(`${menuUrl}/thursday`),
getMeal(`${menuUrl}/friday`),
])
}
- 비동기로 동작하면서 프로미스를 반환하는 팩토리 함수를 나타내기 위해 컨벤션으로
create
대신get
을 사용하고 있다.
[참고] vuex의 state도 data와 동일하게 취급
state: {
user: {
name: 'Captain'
},
},
mutations: {
// 생성하는 시점에 없었던 데이터는 반응성이 없다.
setUserAge: function (state) {
state.user.age = 23;
},
// 객체 속성을 임의로 추가 또는 삭제하는 경우 뷰에서 감지하지 못한다.
deleteName: function (state) {
delete state.user.name;
}
}
vue 3에서는 괜찮아요
Object.defineProperty()
에서Proxy
기반으로 변화
var obj = {};
// Vue 2.x
Object.defineProperty(obj, 'str', {
//...
})
// Vue 3.x
new Proxy(obj, {
//...
})
Vue framework reactivity
의 한계가 사실 프레임워크 상의 한계가 아니다.- 사실
Object
에도Object.observe()
라고 하는 API가 있는데, 이것이deprecated
되면서 반응성 주입할 때,defineProperty
밖에 할 수가 없었다. - 하지만
vue 3.x
에서는Proxy
베이스로 되어있기 때문에 데이터에 어떤 속성이 동적으로 추가/삭제되는 거에 대해 모두 감지할 수 있다.
DOM 조작
- 오래된 습관 버리기
- (기존) 화면 조작을 위한 DOM 요소 제어 방법
- 특정
DOM
을 검색해서 제어하는 방법
- 특정
// 네이티브 자바스크립트
document.querySelector('#app');
// 제이쿼리 라이브러리
$('#app');
- 사용자의 입력 이벤트를 기반으로한 DOM 요소 제어
// 버튼 요소 검색
var btn = document.querySelector('#btn');
// 사용자의 클릭 이벤트를 기반으로 가장 가까운 태그 요소를 찾아 제거
btn.addEventListener('click', function (event) {
event.target.closest('.tag1').remove();
})
- 이런식의 사고방식이 잘못되었다는 것이 아니다.
- 다만, 이런 것에 너무 익숙해져서 Vue를 100% 활용을 못하는 느낌이다.
- 이런거를 Vue에선 어떻게 제어할 수 있을까?
(Vue.js 방식) ref 속성을 활용한 DOM 요소 제어
- Vue에서 제공하는
ref
속성
<template>
<!-- HTML 태그에 ref 속성 추가 -->
<div ref="hello">Hello Ref</div>
</template>
<script>
// 인스턴스에서 접근 가능한 ref 속성
this.$refs.hello; // div 엘리먼트 정보
</script>
- 이런
ref
속성은 Vue 뿐만아니라 react, angular에서도 많이 사용한다. - vue directive에서 제공되는 정보를 최대한 활용하자.
- vue directive에서 제공하는 속성들을 충분히 활용하면 대부분의 경우 커버 가능하다.
<ul>
<li v-for="(item, index) in items">
<span :id="index">{{ item }}</span>
</li>
</ul>
DOM 제어 사고 전환이 필요한 실제 사례
<ul>
<li @click="removeItem">
<span>메뉴 1</span>
<div class="child hide">메뉴 설명</div>
</li>
<li @click="removeItem">
<span>메뉴 2</span>
<div class="child hide">메뉴 설명</div>
</li>
<!-- ... -->
</ul>
<script>
export default {
methods: {
removeItem(event) {
event.target.lastChild.classList.toggle('hide');
}
}
}
</script>
- 위 코드 수정
<ul>
<li v-for="item in items" @click="removeItem">
<span></span>
<div class="child hide">메뉴 설명</div>
</li>
</ul>
<script>
export default {
data() {
return {
items: ['메뉴 1', '메뉴 2', '메뉴 3'],
}
},
methods: {
removeItem(event) {
event.target.lastChild.classList.toggle('hide');
}
}
}
</script>
ref
속성 적용
<ul>
<li v-for="(item, index) in items" @click="removeItem(index)">
<span></span>
<div class="child hide" ref="listItem">메뉴 설명</div>
</li>
</ul>
<script>
export default {
data() {
return {
items: ['메뉴 1', '메뉴 2', '메뉴 3'],
}
},
methods: {
removeItem(index) {
this.$refs.listItem[index].classList.toggle('hide');
}
}
}
</script>
인스턴스 라이프 사이클
- 나는 인스턴스를 얼마나 알고있나?
인스턴스 라이프사이클이란?
- Vue 인스턴스가 생성되고 소멸되기까지의 생애 주기
- new Vue(): 인스턴스 생성
- 이벤트 및 라이프사이클 초기화
- beforeCreate
- 화면에 반응성 주입
- created
- el, template 속성 확인
- template 속성 내용을 render로 변환
- beforeMount
- vm.$el 생성 후 el 속성 값을 대입
- mounted
- 인스턴스를 화면에 부착
- 인스턴스의 데이터 변경
- beforeUpdate
- 화면 재렌더링 및 데이터 갱신
- updated
- 인스턴스 내용 갱신
- 인스턴스 접근 가능
- beforeDestroy
- 컴포넌트, 인스턴스, 디렉티브 등 모두 해제
- destroyed
- 인스턴스 소멸
Vue의 template 속성
- Vue의 template 속성에 대해 이해하고 있으면 좋다.
- 왜냐하면 template 속성이랑 Vue 인스턴스 라이프사이클이랑 밀접한 관계가 있기 때문이다.
- template 속성이란 인스턴스, 컴포넌트의 표현부를 정의하는 속성이다.
<template>
<div>{{ str }}</div>
</template>
- 위 방식으로 정의하든 아래 방식으로 정의하든 같은 template 속성이다.
- 정의하는 방법만 다를 뿐 같은 template 속성이다.
// 인스턴스 옵션 속성
new Vue({
data: {
str: 'hello world',
},
template: '<div></div>'
})
template 속성은 어떤 역할을 하는가? - Vue template 속성의 정체
- 실제 DOM 엘리먼트가 아니라
Virtual DOM(자바스크립트 객체)
<!-- 사용자가 작성한 코드 -->
<template>
<div>{{ str }}</div>
</template>
- 위와 같이 작성된 코드에서
template
태그는 실제DOM
엘리먼트가 아니다. - 위는 Vue 라이브러리에서 아래처럼 변환이된다.
// 라이브러리 내부적으로 변환된 모습
function render() {
with(this) {
return _c('div', [_v(_s(str))]);
}
}
- 구글에
vue template explorer
라고 검색하면 아래와 같이template
태그가 어떻게 변환되는지를 볼 수 있다. - vue template explorer
<div id="app">{{ msg }}</div>
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v(_s(msg))])
}
}
template 속성이 실제로 유효한 시점
- template 속성이 실제로 유효한 시점에 대해서도 제대로 이해해야된다.
- 인스턴스가 화면에 부착(
mounted
)되고난 후이다. mounted
가 되고난 후template
에 작성한 태그(요소)에 접근이 가능하다.
- 인스턴스가 화면에 부착(
- beforeMount
- Create vm.$el and replace el with it
- mounted - 여기부터 작성한 태그(요소)에 접근 가능하다.
인스턴스 부착 시점을 이해하지 못한 사례
<!-- 템플릿 속성 -->
<template>
<canvas id="myChart"></canvas>
</template>
<script>
// 인스턴스 옵션
created: function () {
var ctx = document.querySelector('#myChart'); // null
new Chart(ctx, chartOptions);
}
</script>
- 위와 같이 적용 후 안되다보니
nextTick
이란걸 검색해서 발견후 적용하게되는 경우도 있다. mounted
에서 확인할 수 있는 것은mounted
에서 해결하는 것이 좋다.nextTick
은 최후의 수단이다.
<!-- 템플릿 속성 -->
<template>
<canvas id="myChart"></canvas>
</template>
<script>
// 인스턴스 옵션
new Vue({
created: function() {
this.$nextTick(function() { // 업데이트 시점 혼란 야기 및 코드 복잡도 증가
var ctx = document.querySelector('#myChart');
new Chart(ctx, chartOptions);
})
}
})
</script>
- 적절한 수정방법
<!-- 템플릿 속성 -->
<template>
<canvas id="myChart"></canvas>
</template>
<script>
// 인스턴스 옵션
new Vue({
mounted: function() {
var ctx = document.querySelector('#myChart');
new Chart(ctx, chartOptions);
}
})
</script>
ref 속성
- 만들다가만 ref 속성
- 규칙이 조금 존재한다.
- 알고있으면 좋을만한 규칙들에 대해 소개하도록 하겠다.
ref 속성이란?
- 특정
DOM
엘리먼트나 하위컴포넌트
를 가리키기위해 사용한다. DOM 엘리먼트
에 사용하는 경우DOM 정보
에 접근한다.하위 컴포넌트(커스텀 컴포넌트)
에 지정하는 경우 컴포넌트인스턴스 정보
애 접근한다.v-for
디렉티브에 사용하는 경우Array
형태로 정보를 제공한다.
- 특정
DOM
요소를 조작하고 싶을 때 사용하는 속성이다.
ref 속성 사용할 때 주의할점 1
ref
속성은template 코드
를render 함수
로 변환하고나서 생성한다.- 접근할 수 있는 최초의 시점은
mounted
라이프사이클 훅이다.
<p ref="pTag">Hello</p>
<script>
created: function () {
this.$refs.pTag; // undefined
},
mounted: function () {
this.$refs.pTag; // <p>Hello</p>
}
</script>
ref 속성 사용할 때 주의할점 2
v-if
디렉티브와 사용하는 경우 화면에 해당 영역이 그려지기 전까진DOM
요소 접근 불가- 간단한 해결 방법
v-show
활용
<div v-if="isUSer">
<p ref="w3c">W3C</p>
</div>
<script>
new Vue({
data: {
isUser: false,
},
mounted: function() {
this.$refs.w3c; // undefined
}
})
</script>
ref 사용할 때 주의할점 3
- 하위 컴포넌트의 내용을 접근할 순 있지만 남용하면 안된다.
- 아래 예시 코드의 방법은 지양하는 것이 좋다.
- 컴포너트간 데이터 통식 방식
props
를 내리고event emit
으로 올리는 통신방식을 어기는 것이다. - 아래 처럼 접근하게되면 나중에 디버깅할 때도 어려워진다.
- 컴포너트간 데이터 통식 방식
<div id="app">
<TodoList ref="list"></TodoList>
</div>
<script>
new Vue({
el: '#app',
methods: {
// 상위 컴포넌트에서 불필요하게 하위 컴포넌트를 제어하는 코드
fetchItems: function () {
this.$refs.list.fetchTodos();
}
}
})
</script>
- 특히 위와 같은 코드는 하위 컴포넌트인
TodoList
의 라이프사이클 훅을 활용해 해결 가능하다.
// 하위 컴포넌트인 TodoList에서 라이프사이클 훅을 활용해 해결 가능하다.
var TodoList = {
created: function () {
this.fetchTodos();
},
methods: {
fetchTodos: function () {
// ...
}
}
}
이렇게 커스텀 컴포넌트에 ref
속성을 적용하는 경우
- 써드 파티 라이브러리를 사용할 때, 해당 라이브러리에서 생성된 컴포넌트에 접근할 때 사용하는 것이 더 좋다.
computed 속성
- 간결한 템플릿의 완성
computed 속성이란?
- 간결하고 직관적인 템플릿 표현식을 위해 Vue에서 제공하는 속성
<p>{{ 'hello' + str + '!!' }}</p><!-- 템플릿 표현식만 이용하는 경우 -->
<p>{{ greetingStr }}</p><!-- computed 속성을 활용하는 경우 -->
<script>
new Vue({
data: {
str: 'world'
},
computed: {
greetingStr: function () {
return 'hello' + this.str + '!!';
}
}
})
</script>
computed 속성 활용처 1. 조건에 따라 HTML 클래스를 추가, 변경할 때
- 조건에 따라 HTML 클래스를 추가, 변경할 때
<li :class="{ disabled: isLastPage }"></li>
<script>
computed: {
isLastPage: function () {
var lastPageCondition = this.paginationInfo.current_page >= this.paginationInfo.last_page;
var nothingFetched = Object.keys(this.paginationInfo).length === 0;
return lastPageCondition || nothingFetched;
}
}
</script>
- computed return 값을 아예 Object로..
<li :class="listItemClass"></li>
<script>
computed: {
listItemClass: function () {
// ...
}
}
</script>
computed 속성 활용처 2. 스토어(vuex)의 state 값에 접근할 때
- 스토어(
vuex
)의state
값에 접근할 때
<div>
<p>{{ this.$store.state.module1.str }}</p>
<p>{{ moduleStr }}</p>
</div>
<script>
new Vue({
computed: {
moduleStr: function () {
return this.$store.state.module1.str;
}
}
})
</script>
computed 속성 활용처 3. Vue i18n과 같은 다국어 라이브러리에 활용
Vue i18n
과 같은 다국어 라이브러리에도 활용 가능
<p>{{ 'userPage.common.filter.input.label' }}</p>
<p>{{ inputLabel }}</p>
<script>
computed: {
inputLabel: function () {
return $t('userPage.common.filter.input.label');
}
}
</script>
- 특정 컴포넌트에
computed
가 많아질 경우, 중간에 레이어 하나(컴포넌트)를 더 줘서props
를 내려서 관리하는.. - 그런식으로도 코드를 줄일 수 있을 것이다.
다시 타입스크립트로 돌아와서…
- diff
- 위와 같이 정의하면 브라우저 콘솔창을 보지 않아도 해당 데이터의 타입을 알 수 있다.
- 에디터상에서 인텔리 센스라는 기능을 통해 타입을 자동으로 추론해준다.
할 일 목록 정렬 기능 구현
할 일 목록 정렬 관련 타입 오류 해결 및 타입 정의 완료
첫 번째 프로젝트 요약
Vue Todo With TypeScript
학습한 내용
- Vue + TypeScript 프로젝트 생성 방법
- Please pick a preset: Manually select features
- Check the features needed for your project: Choose Vue version, Babel, TS, Linter
- Choose a version of Vue.js that you want to start the project with 2.x
- Use class-style component syntax? No
- Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
- Pick a linter / formatter config: Prettier
- Pick additional lint features: Lint on save
- Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
- Save this as a preset for future projects? No
- class-style component는 사용 안하는 것이 좋다.
- 기존에 class-style component로 쓰여진 프로젝트를 하시는 분들은 고민이 많이 되실 수 있는데,
그렇다고해서 기존 class-style component 코드를 다 버리기보다는 점진적으로 개선해나가는 것이 좋다. - Vue 3부터는 단순히 Vue.extend() 문법으로 변환하는 것이 아니라 Composition API가 나와서 재사용성을 훨씬 더 높여주면서 타입스크립트 지원을 빵빵하게 해줄 수 있는 Vue3가 나와있기 때문에 Vue3 쓰면서 Composition도 쓰고 그러면서 자연스럽게 다른 새로운 기능들도 쓰면된다.
- dedicated config files가 아니라 package.json을 선택하면 나중에 확장하기 어려워진다.
가급적이면 설정 파일들을 따로 분리하는 것이 좋다.
- Vue TypeScript 프로젝트 기본 구조
- main.ts
- shims-tsx.d.ts
- shims-vue.d.ts
- tsconfig.json
- 재활용성을 고려한 공통 컴포넌트 설계 방법(인풋, 목록 아이템)
- 인풋 태그의 한글 입력 처리 방법
.vue
파일에서의 타입스크립트 정의 방식data
todoItems: [] as Todo[]
methods
return
값이 있을 땐return
값의 타입에 대해 정의return
값이 없으면 기본 리턴 값으로void
가 설정되어 있음- 따로
return
값에 대해 정의하지 않고 코드 내에return 'hi'
이런식으로 되어있다면 해당 메소드의return
값을string
이라고 추론 - 다른데서
string
유형이 아닌 다른 유형으로 쓰이면 에러로 잡힘 - 메소드에서는 파라미터의 타입을 정의하는 것이 가장 중요
- 원하지 않은 파라미터 타입이 들어가는 것을 방지
props
todoItem: Object as PropType<Todo>
- 더 이상 Vue에서 제공하는
validator
를 사용하지 않아도 된다. - 물론
todoItem: { type: Object as PropType<Todo> }
이렇게 써도된다.
computed
- 메소드와 다르게 반드시 리턴값이 필요하다
- 쯕, 리턴값의 유형을 반드시 설정해줘야된다.
- 타입스크립트를 사용했을 때의 이점
- 안전한 코딩(secure coding)
- 의도하지 않은 동작에 대해 미리 예방 가능
두번째 프로젝트 시작하기
yarn install && yarn serve
위 두번째 프로젝트 코드의 문제점
- News / Ask / Jobs 각각의 탭 메뉴를 누르면
- 해당 탭 메뉴의 data로 바뀐 후
- 트랜지션(전환) 효과 발생 - 스르륵 없어졌다가 스르륵 나타나는거
- 그 이유
- 현재
listItem
컴포넌트에서store
의list
를 바라보는데 api
요청 후 받아온 데이터를store
의list
에 다시 담음- 그런데 각각의 News / Ask / Jobs 탭메뉴 모두 공통
listItem
컴포넌트를 쓰고있고
공통listItem
컴포넌트가store
의list
를 바라보고있으니list
값이 바뀌면 자동으로 화면 갱신(반응성reactivity
) - 그 후에
next()
함수 실행 - 해당 컴포넌트로 이동, 이때 트랜지션(전환) 효과 발생
- 현재
- 코드 공통화를 위해 hoc 컴포넌트 방법을 사용
- 하지만 이것이 오히려 트랜지션(전환) 효과에선 독이된 것 같다.
- 이를 해결할 수 있는 방법이 뭐 없을까?
해결책 1. computed가 아닌 created로..
- 이러면 데이터가 실시간으로 바뀌어도 화면에 실시간 갱신 기능은 사라진다.
- 그런데 어차피 화면 실시간 갱신 기능은 기존 상태에서도 아직은 안되긴한다.
- 데이터를 실시간으로 받아오는 웹소켓 같은 처리를 해야지 실시간 갱신기능이 있지,
현재 상태는 data가 바뀌면 값이 바뀔 수 있는 상태인거지 그 기능이 구현되어있지는 않음
- 데이터를 실시간으로 받아오는 웹소켓 같은 처리를 해야지 실시간 갱신기능이 있지,
<template>
<ul class="news-list">
<li v-for="news in listItems" :key="news.id" class="post">
<div class="points">
{{ news.points || 0 }}
</div>
<div>
<p class="news-title">
<template v-if="news.domain">
<a :href="news.url">{{ news.title }}</a><small class="link-text" v-if="news.domain">({{ news.domain }})</small>
</template>
<template v-else>
<router-link :to="`/item/${news.id}`">{{ news.title }}</router-link><small><a class="link-text" :href="news.domain" v-if="news.domain">({{ news.domain }})</a></small>
</template>
</p>
<small v-if="news.user" class="link-text">
by
<router-link :to="`/user/${news.user}`" class="link-text">{{ news.user }}</router-link>
</small>
<small v-if="news.time_ago" class="link-text">
{{ news.time_ago }}
</small>
</div>
</li>
</ul>
</template>
<script>
export default {
// 이렇게하면 화면 실시간 갱신은 못함...
// 어떻게 해야되지?
data() {
return {
listItems: [],
}
},
created() {
this.listItems = this.$store.getters.fetchedList;
}
// computed: {
// listItems() {
// return this.$store.getters.fetchedList;
// }
// }
}
</script>
<style scoped>
.news-list {
padding: 0;
margin: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid
#eee;
}
.points {
width: 80px;
height: 60px;
color:
#42b883;
display: flex;
align-items: center;
justify-content: center;
}
.link-text {
color:
#828282;
}
.news-title {
margin: 0;
}
</style>
해결책 2. computed가 계속 store의 list 값 바라보게.. 하지만 이것도 결국 안된다. created랑 내부 돌아가는 과정은 다르지만 같은 결과..
<template>
<ul class="news-list">
<li v-for="news in listItems" :key="news.id" class="post">
<div class="points">
{{ news.points || 0 }}
</div>
<div>
<p class="news-title">
<template v-if="news.domain">
<a :href="news.url">{{ news.title }}</a><small class="link-text" v-if="news.domain">({{ news.domain }})</small>
</template>
<template v-else>
<router-link :to="`/item/${news.id}`">{{ news.title }}</router-link><small><a class="link-text" :href="news.domain" v-if="news.domain">({{ news.domain }})</a></small>
</template>
</p>
<small v-if="news.user" class="link-text">
by
<router-link :to="`/user/${news.user}`" class="link-text">{{ news.user }}</router-link>
</small>
<small v-if="news.time_ago" class="link-text">
{{ news.time_ago }}
</small>
</div>
</li>
</ul>
</template>
<script>
export default {
// 아래와 같이 바꿔봤자 화면엔 라우트 네임이 바뀌어야 갱신
// 아까와 다르게 computed 에서 store 의 list 값을 바라보곤 있지만,
// 그래서 list 값이 바뀌었을 경우 실행되긴 하지만,
// 어차피 route name이 안달라지면 화면에 갱신 안함
// 흠... 실시간 화면 갱신은 이러나 저러나 포기해야될듯?
data() {
return {
routeName: '',
fetchedListItems: [],
}
},
created() {
this.routeName = this.$route.name;
this.fetchedListItems = this.$store.getters.fetchedList;
},
computed: {
listItems() {
if (this.routeName === this.$route.name) {
return this.fetchedListItems;
}
this.fetchedListItems = this.$store.getters.fetchedList;
return this.$store.getters.fetchedList;
}
}
}
</script>
<style scoped>
.news-list {
padding: 0;
margin: 0;
}
.post {
list-style: none;
display: flex;
align-items: center;
border-bottom: 1px solid
#eee;
}
.points {
width: 80px;
height: 60px;
color:
#42b883;
display: flex;
align-items: center;
justify-content: center;
}
.link-text {
color:
#828282;
}
.news-title {
margin: 0;
}
</style>
결국 내가 원하는대로 하려면(실시간 갱신 구현이 가능하면서.. 아니 근데 이건 좀 지나서 생각해보니 이전 상태 코드와 상관 없는듯, 그냥 created 상태로 하는 것이 맞는듯)
여튼 내가 이전에 갈등했던 내용은: 결국 내가 원하는대로 하려면(실시간 갱신도 가능한데, 전환효과 까지 잘 하게하려면) listItem 컴포넌트로 공통화 말고 3개로 다 나눠야하나.. 그방법말곤 없나?
- 일단 원본 형태로 가자
- 이 문제를 해결하려면 컴포넌트를 통일화 못하고 3개로 나눠야되나? 아니면
created
를 사용해야되나? - 현재 뭐 실시간 기능이 적용된건 아니니까, 실시간 적용하려면 API를 주기적으로 호출하던지 주기적으로 웹소켓 같은걸로 데이터를 받던지(주기적으로 백단에서 data 쏴주고)
- 그냥
created
로 가면 될거같긴 함
실습 코드 분석
App.vue
spinner
컴포넌트 사용keyframes
로 애니메이션 효과 구현- 데이터가 로드되기 전에 나타나는 컴포넌트
- 사용자로 하여금 데이터가 로드되는 중이라는 걸 인식할 수 있게 해주는 컴포넌트이다.
loading
이라는data
를props
로 내려준다.loading
이라는props
가true
이면 해당 컴포넌트가 랜더링되고, 아니면 랜더링되지 않는다.EventBus
로on:progress
,off:progress
이벤트를 등록해준다.on:progress
이벤트가 감지되면loading
값을true
로,off:progress
이벤트가 감지되면loading
값이false
가 된다.src/routes/index.js
코드 내용을 보면 5개 라우트로 진입할 때EventBus
로on:progress
이벤트가$emit
되도록 설정되어있다.
beforeEnter
사용- 해당 라우터에 진입하기 전에
beforeEnter
함수가 호출된다. - 해당 라우터 가드에서
on:progress
이벤트를emit
한다. - 그리고
store.dispatch
를 통해store
의actions
메소드를 실행한다. api
호출을 한 뒤에, 성공하면next()
로 해당 라우트로 넘어가도록한다.
실패하면 에러가 발생하게한다.api
호출해서 받아온 값은state.list
에 저장한다.
- 해당 라우터에 진입하기 전에
conponent
호출, 코드를 줄이기 위해createListView
컴포넌트를 활용했다.
여기서render
함수를 활용했다. (hoc
형태)createListView
는 일반 함수 형태이다.createListView
함수에 인자 값으로 해당 컴포넌트의 이름을 전달한다.
그걸로 해당 컴포넌트의 이름을 설정한다.- 해당 컴포넌트가
mounted
되면off:progress
이벤트를emit
한다. render
함수를 실행한다.render
함수의 첫번째 인자는CreateElement
함수이다.
이렇게 설정되어 내재되어있는 듯하다.- 그래서 첫번째 인자, 즉,
CreateElement
함수에 첫번째 인자로ListView
컴포넌트를 전달한다. ListView
에선ListItem
컴포넌트를 불러오고있다.ListItem
컴포넌트에 들어가면 해당 컴포넌트가created
되면서 아까api
호출로 저장했던state.list
값을 꺼내온다.- 그리고 꺼내온
state.list
값을 통해 리스트를 랜더링한다.
/news
,/ask
,/jobs
각 라우트 모두 동일한 형태의 UI를 가진다.- 때문에 각 세개의 라우트에 적용되는 컴포넌트를 각각 만들기 싫어 한개의 컴포넌트만 만들고
routeTo.name(라우트 이름)
을 통해 각 라우트에 해당하는api
호출을 보내고- 그걸 통해 받아온 데이터로 렌더링하도록 했다.
- 코드를 줄이기위해선 이 방법이 좋은거 같긴하다. (하지만 각 페이지별로 다르게하는 수정사항이 온다면?)
- 하지만 한 개의 컴포넌트가
state.list
라는 데이터를 바라보고 있게했기 때문에 - 그리고
computed
를 통해state.list
가 바뀔 때마다 즉각즉각 반응하도록 했기 때문에- 원래는 툴바에서 다른 메뉴를 클릭했을시
- 스피너가 돌다가
- 데이터를 다 받아오면 스피너가 꺼지고
next()
함수로 해당 컴포넌트로 넘어가면서 해당 컴포넌트가 새로운data
로 렌더링되어야 하는데
- 해당 툴바 클릭하고 데이터를 받아오면
next()
함수가 호출되기 전에, 같은ListItem
컴포넌트에서store.list
값을 바라보고 있으므로 화면이 한번 재렌더링되고,- 그 다음에
next()
함수가 호출되면서 화면이 스르륵 전환효과와 함께 다시 재렌더링된다.
- **여튼 이러한 문제를 해결하기 위해
computed
가 아닌created
로 코드 위치를 옮겼다.- 이러면 단점은, 데이터가 실시간으로 바뀌어도 화면에 실시간 갱신(반응성) 기능이 사라진다.
- 이점을 보완해야된다.
- 하지만 이러한 반응성은 당장은 없어도된다.
computed
에 위치시킨다고해서 반응성이 있긴하지만, 데이터를 주기적으로 호출하거나 백단에서 보내주는 상태가 아니므로- 주기적으로 갱신할 필요는 아직 없다.
- 지금 상태에선
created
때 데이터를 받아오는 것으로 수정하는 것이 훨씬 좋은 것 같다.
- 여튼 코드를 줄인다는 방향성에서는
render
함수를 사용하는hoc
기법이 괜찮아보인다.
프로젝트에 타입스크립트 플러그인 추가
-
@vue/cli
3버전 이상일 때는 기본적으로 프로젝트의 구성이vue plugin
의 집합으로 구성이 되게끔 되어있기 때문에@vue/cli
3버전 이상으로 프로젝트를 생성하신 분들에 한해서vue add typescript
를 할 수 있다.
vue add typescript
-
@vue/cli
3버전 이상에선 위와 같은 방법으로 타입스크립트를 추가할 수 있지만, 위와 같은 방법은 추천하지 않는다.
그 이유는 차차 말씀드리도록 하겠다. -
위와 같이 진행하면 처음
cli
로 vue 프로젝트를 생성할 때와는 다른 질문들이 나오기 때문에 이런 부분들을 상당히 주의해야된다.
vue add typescript
#####################################################################################
WARN There are uncommitted changes in the current repository, it's recommended to commit or stash them first.
# 현재 저장소에 커밋안된 변경된 사항이 있다면 우선 커밋하거나 스태쉬 하는 것을 추천한다는 뜻이다.
? Still proceed? (y/N)
# 진행할까?
# y
? Use class-style component syntax? (Y/n)
# n
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?
# y
? Convert all .js files to .ts? (Y/n)
# 모든 자바스크립트 파일을 타입스크립트 파일로 변환할 것인지 묻는 질문입니다.
# 모든 자바스크립트 파일을 타입스크립트 파일로 바꾸면 기본적으로 자바스크립트에서 타입스크립트로 변환되면서 생기는 오류들이 많아지게 됩니다.
# 왜냐면 타입스크립트는 타입이 추가된 언어인거고 자바스크립트는 저희가 원래 사용하던 타입이 없는 언어이다보니까
# 타입이 필요한 부분에 대해서 전부 다 에러가 발생하거나
# 혹은 자바스크립트에서 실행하는 시점에서 타입이 바뀌는 그런 유형들을 타입스크립트에서 타입을 강제하기 시작하면서
# 에러가 많이 발생할 것입니다.
# 즉, 이렇게하면 점진적인 적용은 불가능하고 타입스크립트 파일로 변환한 순간부터 전체를 타입스크립트로 바꿔야하기 때문에 이는 no를 선택하는 것이 좋습니다.
# n
? Allow .js files to be compiled? (y/N)
# 자바스크립트 파일이 컴파일되게 할것인가?
# y
? Skip type checking of all declaration files (recommended for apps)? (Y/n)
# 모든 선언 파일에 대한 타입체킹을 스킵할 것인가?
# 이런 부분들도 y를 선택해야 점진적인 적용에 도움이 될 것이다.
# y
yarn serve
#####################################################################################
# 타입스크립트를 추가 후 실행하면, 여기서부터 기존 프로젝트에 타입스크립트를 추가해서 하는 방법을 추천드리지 않는 이유가 발생하는데,
WARNING: You are currently running a version of TypeScript which is not officially supported by typescript-estree.
# yarn serve를 돌릴 때부터 위와 같은 Warning 메시지가 뜹니다.
# 현재 너가 실행한 타입스크립트 버전이 typescript-estree에 의해 지원되는 공식적인 버전이 아니다.
# typescript-estree 란 아무래도 eslint 관련 플러그인 같다.
# 즉 eslint 버전과 맞지 않아서 나는 에러라고 보시면 될 것 같다.
SUPPORTED TYPESCRIPT VERSIONS: ~3.1.1
# 위 문구를 보시면 현재 eslint는 타입스크립트 버전 3.1.1 까지 지원한다고 되어있다.
YOUR TYPESCRIPT VERSION: 4.1.6
# 방금 추가한 타입스크립트 플러그인 버전은 4.1.6 버전이다.
# package.json에 4.1.6 버전으로 명시되어있다.
타입스크립트 추가로 발생하는 문제들
- 위와 같이 타입스크립트를 추가하여 실행해보면 CSS가 조금 깨지는 것을 볼 수 있다.
App.vue
파일을 확인해보면 타입스크립트 추가할 때 자기 마음대로App.vue
파일 내용을 바꿔버린 것을 볼 수 있다.- 즉,
App.vue
파일에 우리가 기존에 작성했던 코드들이 다overriding
된 것이다. - 기존 코드로 원복시켜야된다.
main.ts
가 다시 한번 쓰여지면서 내용들이 변경되었다.package.json
의 기존 라이브러리들도 좀 오래된 라이브러리들도 있어서 호환 문제도 해결해야될 것이다.
그래서 결론적으로는 이런식으로 기존 프로젝트에 vue add typescript
를 이용해서 프로젝트를 개선하는 것은 추천드리지 않는다.
타입스크립트를 사용한다는 말은 자바스크립트 최신 스팩을 사용한다는 말이고
그 관련된 생태계도 최신 버전, 그리고 @vue/cli
에서 저희가 바로 생성했을 때,
타입스크립트를 끼게되면 그에 맞게 버전들이 설정되어 다 호환이 될 것인데,
중간에 타입스크립트를 끼워넣게되면 호환 문제도 발생하게될 것이다.
그래서 결론적으로 이런 방식으로 타입스크립트를 적용하지 않고 다른 방식으로 적용하는 방법에 대해 알아보도록 하겠다.
역순으로…
- 타입스크립트가 적용된 프로젝트를 생성하고
- 기존 프로젝트 코드를 거기다 적용하는 식으로..
위와 같은 방식으로 진행하도록 하겠다.
프로젝트 구성 및 실행 결과 확인
vue create vue-news-ts
#####################################################################
Vue CLI v4.5.15
? Please pick a preset:
Default ([Vue 2] babel, eslint)
Default (Vue 3) ([Vue 3] babel, eslint)
❯ Manually select features
? Please pick a preset: Manually select features
? Check the features needed for your project:
◉ Choose Vue version
◉ Babel
❯◉ TypeScript
◯ Progressive Web App (PWA) Support
◯ Router
◯ Vuex
◯ CSS Pre-processors
◉ Linter / Formatter
◯ Unit Testing
◯ E2E Testing
? Choose a version of Vue.js that you want to start the project with (Use arrow keys)
❯ 2.x
3.x
? Use class-style component syntax? (Y/n) n
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) y
? Pick a linter / formatter config:
ESLint with error prevention only
ESLint + Airbnb config
ESLint + Standard config
❯ ESLint + Prettier
TSLint (deprecated)
? Pick additional lint features: (Press to select, to toggle all, to invert selection)
❯◉ Lint on save
◯ Lint and fix on commit
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
In package.json
? Save this as a preset for future projects? (y/N) n
# Y 선택시 vue create 명령어 실행 때마다 지금 선택한 preset 대로 프로젝트가 생성될 것이다.
프로젝트 구성 및 실행 결과 확인, 기존 프로젝트 코드를 타입스크립트가 추가된 프로젝트로 옮기기 (점진적으로)
- 우선
src
폴더 안에 있는 모든 폴더(api
,assets
,components
,routes
,store
,utils
,views
)를 옮기자.- 그러기 위해선 타입스크립트를 추가하여 새로 생성한 프로젝트의
assets
,components
폴더를 삭제한다. - 그리고난 후, 옮긴다.
- 그러기 위해선 타입스크립트를 추가하여 새로 생성한 프로젝트의
App.vue
파일도 그대로 들고오자. (복붙)main.js
내용도 그대로 들고오자. (복붙)- 그대로 들고오면 에러가 나는 것이 당연하다.
public/index.html
파일도 그대로 들고오자. (복붙)yarn add axios vuex vue-router
yarn serve
- 에러가 많이난다. 에러가 많이 나야 정상이다.
- 왜냐하면 자바스크립트 파일을 타입스크립트에다 넣었기 때문에 에러가 많을수밖에 없다.
강의에선 타입스크립트 에러가 발생해도 일단 실행은 된다.
타입스크립트 에러랑 기능 에러는 별개이기 때문이다.
그런데 실제 실습했을 때는 실행되지 않는다.
강의에서 타입스크립트 버전은 3.9.7이고 내가 실습하는 지금 시점에선 4.1.6인데, 버전 차이에서 오는 차이인걸까?
그 이유는 잘 모르겠다.
타입스크립트 프로젝트 진행 방식 안내
타입스크립트를 점진적으로 적용하기 위해 강의에선
- 아예 새로운 타입스크립트가 추가되어있는 프로젝트를 생성하고
- 기존 코드를 새로 생성한 프로젝트로 옮겨서 타입스크립트 에러를 해결하는 식으로 진행할 것이다.
- 어차피 실행은 되니깐..이라고하지만 내꺼에선 실행되진 않는다.
여튼 위와 같이 하려고했으나 막상 해보니 실행이 안되는 상태이다.
강의와 현재 차이점은 강의에서 타입스크립트 버전이 3점대 버전이라면, 지금 내가 실습할 땐 4점대 버전..
버전 차이 때문에 이러한 현상이 발생하는걸까?
질문
-
뷰 프로젝트에 타입스크립트를 점진적으로 적용하기 위해
- 타입스크립트가 추가된 새로운 프로젝트 생성
- 해당 프로젝트로 기존 코드들을 옮긴 후 타입스크립트 점진적으로 적용
-
위와 같이 하는 이유
-
타입스크립트 에러가 발생해도 일단 실행은 되기 때문이다.
CSS도 기존 그대로 유직되기 때문에.. 이렇게 이해를 했는데, 위와 같이해도 막상 실행이 되지 않는다.
강의에서는 타입스크립트 에러는 많이 발생하지만 localhost:8081에 화면은 제대로 나온다.
그런데 나는 화면조차도 제대로 나오지 않는다.Could not find a declaration file for module ‘./store/index.js’.
‘/Users/…/src/store/index.js’ implicitly has an ‘any’ type.위와 같은 에러가 발생한다.
아무래도 모듈을 제대로 가져오지 못하는 것 같다.현재 강의 시점의 타입스크립트 버전이 3점대 버전이고, 지금 실습할 당시 기준 타입스크립트 버전이 4점대 버전인데, 주버전이 달라져 생긴 문제인지 궁금합니다.
-
Vue 프로젝트에 타입스크립트를 점진적으로 적용하는 방법
- Vue + TypeScript 프로젝트 생성
- 기존 서비스 코드와 라이브러리를 새 프로젝트에 이동
- 기본적인 빌드 에러 해결
- 타입스크립트의 혜택을 볼 수 있는 주요 파일들 위주로
.js
->.ts
로 변환하며 적용
- 팁: 타입 체킹 정도는 덜 엄격한 방식에서 점점 엄격한 방식으로 적용하는 것을 추천한다.
여튼 버전이 강의 내용과 현재와 다른거에 영향을 끼치는 건 사실인 것 같다.
점진적인 타입스크립트 적용 방식 1단계 - 라우터, HOC, 유틸함수
프로젝트 빌드 에러 해결
yarn serve
- 에러는 많이 발생하지만 서버는 실행되는 상태이다.
- 강의에서는
localhost:8080
으로 들어갔을 때, 페이지가 정상적으로 나오고 기능도 제대로 된다. - 하지만 내 로컬에선 안된다.
-
페이지도 안 뜬다. 여튼 버전 차이가 문제인 것 같다.
- 여튼 강의에선 다시
build
를 실행
yarn build
####
ERROR Build failed with errors.
당연히 빌드 에러가 발생한다.
실행상 에러와 빌드상 에러가 일치한다.
타입스크립트 버전업하면서 훨씬 더 엄격해진건가?
강의에선 3점대 버전 때는 빌드 때만 에러발생하게하고, 그냥 개발시엔 에러가 발생해도 실행되게한거보면…
여튼 빌드 에러나는 것은 확인했기 때문에, 다시 개발 서버를 실행해보도록하자.
yarn serve
main.ts 파일에서 발생하는 에러
TS7016: Could not find a declaration file for module './routes/index.js'.
'/Users/.../vue-news-ts/src/routes/index.js' implicitly has an 'any' type.
# implicitly has an 'any' type.
# 타입에 any라도 명시되어있어야하는데 any 타입도 없다.
# 즉, 이는 strict(엄격한) 옵션이 켜져있기 때문에 발생하는 에러이다. - 물론 타입을 명시해 해결하는 것이 더 좋지만 일단..
# 점진적 적용을 위해 tsconfig.js, 즉, 타입스크립트 설정 파일에 있는 strict 옵션을 낮추고 시작을 해보도록 하겠다.
아래와 같이 옵션을 추가해준다.
어차피 strict: false
를 주면, noImplicitAny
도 자동으로 false
가 되지만, 일단 명시적으로 하기 위해 적어준다.
{
"strict": false,
"noImplicitAny": false
}
다시 실행해보자.
yarn serve
###############################################################################
ERROR in /Users/hyungjulee/vue-training-lv5/vue-news-ts/src/main.ts(9,3):
9:3 No overload matches this call.
Overload 1 of 3, '(options?: ThisTypedComponentOptionsWithArrayProps): CombinedVueInstance>', gave the following error.
Argument of type '{ router: any; store: any; render: (h: CreateElement) => VNode; }' is not assignable to parameter of type 'ThisTypedComponentOptionsWithArrayProps'.
Object literal may only specify known properties, and 'router' does not exist in type 'ThisTypedComponentOptionsWithArrayProps'.
Overload 2 of 3, '(options?: ThisTypedComponentOptionsWithRecordProps): CombinedVueInstance>', gave the following error.
Argument of type '{ router: any; store: any; render: (h: CreateElement) => VNode; }' is not assignable to parameter of type 'ThisTypedComponentOptionsWithRecordProps'.
Object literal may only specify known properties, and 'router' does not exist in type 'ThisTypedComponentOptionsWithRecordProps'.
Overload 3 of 3, '(options?: ComponentOptions, DefaultMethods, DefaultComputed, PropsDefinition>, Record<...>>): CombinedVueInstance<...>', gave the following error.
Argument of type '{ router: any; store: any; render: (h: CreateElement) => VNode; }' is not assignable to parameter of type 'ComponentOptions, DefaultMethods, DefaultComputed, PropsDefinition>, Record<...>>'.
Object literal may only specify known properties, and 'router' does not exist in type 'ComponentOptions, DefaultMethods, DefaultComputed, PropsDefinition>, Record<...>>'.
7 |
8 | new Vue({
> 9 | router,
| ^
10 | store,
11 | render: (h) => h(App),
12 | }).$mount("#app");
Version: typescript 4.1.6
위 에러 하나만 남고 나머진 해결된 모습이다.
routes/index.js
파일에서 발생한 에러인듯 하다.
- 자바스크립트 파일을 타입스크립트에서 인식할 수 있도록 하려면 우리가 배웠던 속성 중에
allowJs
라는 속성을 사용하면 된다.
allowJs
{
"allowJs": true
}
강의에선 main.ts
의 router
에 생기던 에러가 해결되었지만, 내 로컬에선 여전히 router
에 에러가 남아있다.
진짜 버전이 달라서 생기는 문제인걸까?
강의 시점 때 vue-router
, vuex
모두 다 3점대 버전인데, 지금 내가 실습하는 순간엔 4점대 버전이다.
버전업 때문인 것 같다.
vue-router
, vuex
버전 다운그레이드 하니깐 잘 되는 듯.
프로젝트 빌드 에러 해결 (버전업 때문인지 여전히 router 에러 발생)
지금 현재 강의와 상이한 부분이 주버전 업데이트로인해 발생하는 문제인가?
의심되는 모듈 버전
vue-router
: 강의시점 3.4.9, 실습시점 4.0.12vuex
: 강의시점 3.6.0, 실습시점 4.0.2typescript
: 강의시점 3.9.3, 실습시점 4.1.5
…
버전이 달라서 생기는 문제가 맞았다.
위에 3개 라이브러리 버전을 강의시점과 일치 시켰더니 에러가 발생하지 않았다.
좀 더 조사는 필요하다.
결론
즉, 이는 typescript
의 주버전 업데이트가 원인이었다기보단 vue-router
, vuex
라이브러리의 주버전이 업데이트되면서 각 라이브러리의 코드 작성 방식에 무언가 변화가 있었던 것 같다.
그로인해 import
, export
시에 기존과는 다른 작성방식을 했어야됐던거 같다.
애초에 강의시점의 코드 내용을 클론받아 한거기 때문에 그 시점 라이브러리 버전들은 다 예전 꺼였지만,
vue-router
, vuex
는 새 프로젝트 설치할 때, @vue/cli
에서 같이 설치한 것이 아니라
yarn add vue-router vuex
로 따로 설치를해서 4점대 버전이 설치되었다.
그래서 이러한 이슈가 생겼던 것이다.
@vue/cli
로 vue 2점대 버전으로 vue-router
, vuex
같이 프로젝트를 생성하면 vue-router
, vuex
모두 4점대 버전이 아니라 3점대 버전으로 설치가 된다.
여튼 주버전이 달랐던 것이 이번 이슈의 원인이었다. vue-router
, vuex
다 3점대 버전으로 다운그레이드 시키자.
프로젝트 빌드 에러 해결(vue-router, vuex 3점대 버전으로 다운그레이드)
App.vue에 타입스크립트 적용 및 strict 옵션 참고 사항
유틸성 파일에 TS 적용 및 주요 TS 적용 포인트 안내
- HOC 컴포넌트 (지금 예제 코드에서 코드량 줄일려고 공통 컴포넌트 통일화한 부분이 HOC 컴포넌트이다.)
- createListView
유틸성 파일에 TS 적용 및 주요 TS 적용 포인트 안내
라우터 파일에 TS 적용 및 라이브러리 내부 타입 선언 파일 설명
- TS2554: Expected 0-1 arguments, but get 2.
- 인자는 0~1개만 있으면되는데, 2개가 있다는 뜻이다.
- beforeEnter
- 네비게이션 가드
beforeEnter
// export type NavigationGuard<V>
// Alias for:
// (to: Route, from: Route, next: NavigationGuardNext<V>) => any
// vue-router
위와 같이 beforeEnter
네비게이션 가드에 마우스 커서를 올리면 해당 메소드에 대한 설명이 뜬다.
첫번째 인자에는 어떤 유형의 값이, 두번째 인자에는 어떤 유형의 값이, 세번째 인자에는 어떤 유형의 값이 들어오는지에 대한 설명이 다 나와있다.
이런 것들을 사용해 각 인자의 타입을 정의한다.
첫번째 인자와 두번째 인자에 vue-router
라이브러리에서 제공하는 Route
타입을 정의해준다.
일반적으로 저희가 라이브러리를 사용하면, 해당 라이브러리 안에 타입을 잘 정의해놓은 코드가 있다.
@types
라는 라이브러리 또는- 잘 만들어진 라이브러리는 그 라이브러리 내부에
index.d.ts
가 있다.
axios
라이브러리를 봐도 index.d.ts
파일이 있다.
index.d.ts
는 타입스크립트에서 사용하는 타입을 선언한 파일이라고 보면된다.
때문에 axios
는 @types/axios
라이브러리를 따로 설치를 안해도된다.
vue 타입 정의 관점에서의 취약점 - vue 3에선 이런 취약점이 개선되었다.
- vue 라이브러리가 타입 정의, 타입 시스템을 고려하고 만든 라이브러리가 아니기 때문에 타입 정의 관점에서 취약점이 있다.
- 이러한 취약점이 vue 3로 넘어오면서 전체적으로 타입 시스템을 잘 활용할 수 있게 개선되었다.
-
vue-router
에도router.d.ts
파일에 타입들이 정의되어있다. - 현재 vue 2에선 가벼운 수준으로만 타입 정의가 되어있는데, vue3를 나중에 보시면 내부적으로 지원하는 타입이 많아지면서 우리가 컴포넌트 레벨에서 가져다 쓸 수 있는 타입들이 많아지게된다.
- 그렇다고해서
vue2
에서 타입스크립트를 전혀 사용하지 못하는 것은 아니다.
타입 적용 꿀팁 - 마우스 커서 오버시 뜨는 힌트 활용!
마우스 커서 오버시 뜨는 힌트를 적극 활용하자.
라우터 파일에 TS 적용 및 라이브러리 내부 타입 선언 파일 설명
import Vue from "vue";
import VueRouter, { NavigationGuardNext, Route } from "vue-router";
Vue.use(VueRouter);
export default new VueRouter({
path: '/news',
name: 'news',
component: createListVie("NewsView"),
beforeEnter(
routeTo: Route,
routeFrom: Route,
next: NavigationGuardNext<Vue>
) {
}
})
HOC 파일에 TS 적용 및 라우터 네비게이션 함수 로직 개선
- vue에선
render
함수를 활용해 화면을 그려줄 수 있다. - HOC 파일에 TS 적용 및 라우터 네비게이션 함수 로직 개선
// src/routes/index.ts
import Vue from "vue";
import VueRouter, { NavigationGuardNext, Route } from "vue-router";
import bus from '@/utils/bus';
import store from '@/store/index.js';
Vue.use(VueRouter);
export default new VueRouter({
path: '/news',
name: 'news',
component: createListVie("NewsView"),
async beforeEnter(
routeTo: Route,
routeFrom: Route,
next: NavigationGuardNext<Vue>
) {
bus.$emit('on:progress');
try {
await store.dispatch('FETCH_LIST', routeTo.name);
next();
} catch (error) {
new Error("failed to fetch news items");
}
}
})
// src/views/CreateListView.ts
import ListView from './ListView.vue';
import bus from '@/utils/bus';
import { CreateElement } from 'vue';
export default function createListView (name: string) {
return {
name,
mounted() {
bus.$emit('off:progress');
},
render(h: CreateElement) {
return h(ListView);
}
}
}
파일 경로 의문점
import createListView from '../views/CreateListView';
위와 같이 경로를 줬을 땐
TS7016: Could not find a declaration file for module '../views/CreateListView'.
'C:/workspace/.../src/views/CreateListView.js' implicitly has an 'any' type.
위와 같은 에러가 뜨면서
import createListView from '@/views/CreateListView';
이렇게 주니깐 에러가 안뜬다. 왜지?
CreateListView.js
파일을 CreateListView.ts
로 바꾸니까 이런 현상이 발생한다.
점진적인 타입스크립트 적용 방식 2단계 - 컴포넌트, API 함수
스토어 상태 관리에 대한 주의 사항 안내
현재 상황
strict: false
- 타입스크립트 사용시 이득을 가져다주는 부분에 위배되는 옵션이다.
- 위 옵션을
true
로 설정 후 진행해보도록 하겠다. - 위 옵션을
true
로 설정 후 재실행하면 여러 에러가 발생한다.
타입스크립트를 사용함으로써 가장 이득을 볼 수 있는 구간
api
호출하는 구간
store를 사용하지 않아도되면 굳이 사용하지 말자
store
를 남용하면 그게 다 비용이되고 복잡해진다.- 특히
store
과mixins
를 같이 쓰면 정말 난공불락의 성처럼 데이터 흐름을 거의 파악할 수 없는 상황이 되어버린다. - 가급적
store
를 써야하는 상황인지 아닌지 스스로 판단할 필요가있다.
- 특히
<Root>
<APP>
<Spinner>
<ToolBar>
<NewsView key='__transition-10-vue-component-7-NewsView'> = $vm0
<ListView>
<ListItem>
- 위 컴포넌트 구조를 보면
ListView
컴포넌트에서api
호출을 보내 데이터를 받아오면 - 그 데이터를
props
로ListItem
까지 내려주면 될 것 같다. - 굳이
ListItem
에서store
에 있는 데이터를 꺼내오지 않아도말이다.
store 상태 관리에 대한 주의 사항 안내
{
"strict": true
}
API 파일에 타입스크립트 적용 및 호출 로직 구현
에러날시 페이지에 딤드 영역이 화면에 뜨는거 없애려면?
// vue.config.js
module.exports = {
devServer: {
overlay: false,
},
}
위와 같이 옵션을 설정하면 에러가 발생해도 딤드 영역이 화면을 덮지 않는다.
async 함수의 return 값은 프로미스 인스턴스이다
- 비동기 로직 작성할 때 실수를 줄여준다.
API 파일에 타입스크립트 적용 및 호출 로직 구현
// src/routes/index.ts
import Vue from "vue";
import VueRouter, { NavigationGuardNext, Route } from "vue-router";
import bus from '@/utils/bus';
import store from '@/store/index.js';
Vue.use(VueRouter);
export default new VueRouter({
path: '/news',
name: 'news',
component: createListVie("NewsView"),
async beforeEnter(
routeTo: Route,
routeFrom: Route,
next: NavigationGuardNext<Vue>
) {
bus.$emit('on:progress');
next();
}
})
<!-- src/views/ListView.vue -->
<script>
import ListItem from "@/components/ListItem.vue";
import { fetchNews } from "@/api";
export default {
components: {
ListItem,
},
created() {
this.fetchNewsItems();
},
methods: {
async fetchNewsItems() {
const response = await fetchNews();
console.log(response.data);
}
}
}
</script>
하위 컴포넌트에 데이터 연결 및 표시 기능 구현
- 컴포넌트의 코드 줄이 500줄을 넘어가면 위험 신호
- 일반적으로 500줄이 넘어가면 더 세분화하는 것이 좋다.
- 하위 컴포넌트에 데이터 연결 및 표시 기능 구현
<!-- src/components/ListItem.vue -->
<template>
<ul class="news-list">
<li v-for="news in ltems" :key="news.id" class="post">
<div class="points">
{{ news.points || 0 }}
</div>
</li>
</ul>
</template>
<script>
export default {
name: 'ListItem',
props: {
items: {
type: Array,
required: true,
}
}
}
</script>
<!-- src/components/ListView.vue -->
<template>
<div>
<list-item :items="newsItems"></list-item>
</div>
</template>
<script>
import ListItems from '@/components/ListItem.vue';
import { fetchNews } from '@/api';
export default {
name: 'ListView',
components: {
ListItems,
},
data() {
return {
newsItems: [],
}
},
created() {
this.fetchNewsItems();
},
methods: {
async fetchNewsItems() {
const response = await fetchNews();
console.log(response.data);
this.newsItems = response.data;
}
}
}
</script>
API 함수 타입 정의
ListView
컴포넌트의 fetchNews();
함수에 마우스 커서를 올려놓으면 아래와 같은 힌트 메시지가 뜬다.
export function fetchNews(): Promise<AxiosResponse<any>>
그 결과값이 담기는 response
변수에 마우스 커서를 올려놓으면
const response: AxiosResponse<any>
위와 같이 뜬다.
AxiosResponse
타입에 제네릭으로 any
가 들어가있다.
제네릭으로 any
가 들어가있는 이유는 fetchNews
함수의 반환값으로 아무것도 설정 안했기 때문이다.
제네릭 복습
- 제네릭은 타입을 마치 함수처럼, 함수의 파라미터처럼 사용하는 개념이다.
- 타입을 정의하는 시점에 타입을 넘겨서 추론하게하는 것이 바로 제네릭이다.
- 제네릭을 사용하면 좋은 점은 타입 코드가 많이 줄어든다는 점이다. 중복된 타입 정의를 줄일 수 있다.
- 제네릭에 넣은 타입이 반환되어 추론된다.
- 즉,
<>
에 넣은 타입이 반환되어response
변수 타입 추론을 하게해준다. - 우선
fetchNews
함수의 반환 타입에 대해 먼저 정의를 해줘야된다.
데이터 타입 정의할 때
- 개발자도구 네트워크 패널에서 데이터를 그대로 들고오면된다.
- 패널을 확인하기 싫다면 스웨거를 활용해도된다.
- 스웨거에 데이터 타입 뽑아내는 기능도 있다.
// src/api/index.ts
import axios, { AxiosPromise } from "axios";
interface NewsItem {
comments_count: number;
domain: string;
id: number;
points: number;
time: number;
time_ago: string;
title: string;
type: string;
url: string;
user: string;
}
const api = {
news: "https://api.hnpwa.com/v0/news/1.json",
ask: "https://api.hnpwa.com/v0/ask/1.json",
jobs: "https://api.hnpwa.com/v0/jobs/1.json",
user: "https://api.hnpwa.com/v0/user/",
item: "https://api.hnpwa.com/v0/item/",
};
// fetchNews의 return 값은 AxiosPromise이다.
// 여기서 중요한 점! AxiosPromise에서 반환하는 값의 유형은 NewsItem[]이 아니다!!
// AxiosPromise에서 반환하는 값의 data 속성에 담기는 값의 타입이 NewsItem[]이다!!!!!
function fetchNews(): AxiosPromise<NewsItem[]> {
return axios.get(api.news);
}
- 위와 같이 반환 값과
interface
를 정의해놓으면.
으로 속성을 선택할 때 알아서 어떤 속성이 있는지 에디터에서 자동으로 힌트를 준다.
API 함수 타입 정의
props 타입 정의
props
로 넘겨준 값의 데이터 형태도 정의해준다.-
그러면 타입 추론이되어 코드 작성할 때 더 편하게된다.
- props 타입 정의
//src/api/index.ts
import axios, { AxiosPromise } from "axios";
export interface NewsItem {
comments_count: number;
domain: string;
id: number;
points: number;
time: number;
time_ago: string;
title: string;
type: string;
url: string;
user: string;
}
const api = {
news: "https://api.hnpwa.com/v0/news/1.json",
ask: "https://api.hnpwa.com/v0/ask/1.json",
jobs: "https://api.hnpwa.com/v0/jobs/1.json",
user: "https://api.hnpwa.com/v0/user/",
item: "https://api.hnpwa.com/v0/item/",
};
// fetchNews의 return 값은 AxiosPromise이다.
// 여기서 중요한 점! AxiosPromise에서 반환하는 값의 유형은 NewsItem[]이 아니다!!
// AxiosPromise에서 반환하는 값의 data 속성에 담기는 값의 타입이 NewsItem[]이다!!!!!
function fetchNews(): AxiosPromise<NewsItem[]> {
return axios.get(api.news);
}
<!-- src/components/ListView.vue -->
<template>
<div>
<ListItem :items="newsItems"></ListItem>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import ListItem from "../components/ListItem.vue";
import { fetchNews, NewsItem } from "@/api";
export default Vue.extend({
components: {
ListItem,
},
data() {
return {
newsItems: [] as NewsItem[],
};
},
created() {
this.fetchNewsItems();
},
methods: {
async fetchNewsItems() {
const response = await fetchNews();
this.newsItems = response.data; // this.newsItems에 마우스 커서를 올려놓으면 NewsItem[] 이라는 데이터타입이 추론된다.
},
},
});
</script>
<style></style>
<!-- src/components/ListItem.vue -->
<script lang="ts">
import Vue, { PropType } from 'vue';
import { NewsItem } from '@/api';
export default Vue.extend({
props: {
items: {
type: Array as PropType<NewsItem[]>, // ListView에서 내려주는 데이터의 타입이 NewsItem[]이다.
required: true,
}
}
})
</script>
props 속성 타입 정의의 이점과 computed 속성 활용 방법 안내
- 프롭스 속성 타입 정의의 이점과 computed 속성 활용 방법 안내
template
태그 안에서 연산식은 웬만하면 피하자.- 연산식이 들어가면
computed
를 활용하자. -
computed
를 활용하면caching
기능도 있지만, 스크립트 레벨에서 디버깅을 할 수 있다는 장점이있다. - 타입스크립트를 적극 활용하면 코드를 작성하는 중간과정에서 추론이 잘 되는 이점이 있다.
<!-- src/components/ListItem.vue -->
<template>
<small v-if="news.time_ago" class="link-text">
<!-- {{ news.time_ago }} -->
{{ timeAgo(news) }}
</small>
</template>
<script lang="ts">
import Vue from 'vue';
import { NewsItem } from '@/api';
export default Vue.extend({
methods: {
timeAgo(news: NewsItem): string {
return news.time_ago.concat(", 2021");
}
}
})
</script>
axios 타입 정의 마무리
- 몇개 라이브러리는 별도의
@types/라이브러리이름
을 설치하지 않더라도 해당 라이브러리 폴더 안에 타입이 잘 정의된index.d.ts
파일 같은걸 갖고있다. - axios 타입 정의 마무리
// src/api/instance/index-instance.ts
import axios, { AxiosInstance } from 'axios';
function create(url: string, options = {}): AxiosInstance {
// create 메소드는 axios 인스턴스를 생성하는 메소드이다.
const instance = axios.create(Object.assign({ baseUrl: url }, options));
// 즉, 이 함수의 return 값이 instance 변수이므로 이 instance 변수에 담기는 데이터의 형태는 AxiosInstance 이므로
// 이 함수의 반환값 형태로 AxiosInstance를 지정해준다.
return instance;
}
function createWithAuth(url: string, options = {}): AxiosInstance {
const instance = axios.create(Object.assign({ baseUrl: url }, options));
// ...
}
점진적인 타입스크립트 적용 방식 3단계 - vuex store 타입 정의
그런데 vuex
에서의 타입정의는 꼭 필요하진 않을수도?
왜냐하면 vuex
헬퍼 함수를 사용하니깐?
좀 더 공부해보자.
선행학습 - vuex 타입 정의 방법
Vue.extend()
방식을 이용하여 vuex
를 타이핑하려면 vuex
내부적으로 제공되는 타입을 약간 변형해줘야 합니다.
코드 작성 방식을 알아보기 위해 토큰을 설정하는 vuex
코드를 작성해보겠습니다.
선행학습 - vuex 기본 코드
먼저 store/index.ts
에 아래와 같이 정의합니다.
// store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = {
state: {
token: '',
}
}
export default new Vuex.Store(store);
선행학습 - state 정의
위 기본 코드에서 state
를 별도의 파일로 분리합니다.
store/state.ts
에 놓겠습니다.
// store/state.ts
export const state = {
token: '',
}
export type RootState = typeof state;
state
를 정의한 다음 해당 객체 구조의 타입을 RootState
의 타입 변수로 선언합니다.
선행학습 - mutations 정의
mutations
코드도 store/mutations.ts
파일에 별도로 작성합니다.
// store/mutations.ts
import { RootState } from './state';
// 뮤테이션 타입
export enum MutationTypes {
SET_TOKEN = 'SET_TOKEN',
}
// 뮤테이션 속성 함수
export const mutations = {
[MutationTypes.SET_TOKEN](state: RootState, token: string) {
state.token = token;
}
}
export type Mutations = typeof mutations;
추후 mutations
속성 함수의 타입 추론을 위해 mutations
함수의 이름은 모두 enum
값으로 선언하고 해당 값을 함수의 이름으로 정의해줍니다.
앞에서 정의한 state
의 타입인 RootState
를 들고와서 mutations
속성 함수의 첫번째 인자 타입으로 연결해줬습니다.
선행학습 - 뷰 컴포넌트에서 활용할 수 있도록 vuex 커스텀 타입 정의
글 서두에 언급한 것처럼 vuex
의 내부 타입 방식으로는 위에서 정의한 state
와 mutations
가 올바르게 추론되지 않습니다.
이 문제를 해결하기 위해 store/types.ts
에 아래와 같이 작성합니다.
// store/types.ts
import { CommitOptions, Store } from 'vuex';
import { Mutations } from './mutations';
import { RootState } from './state';
// MyMutations라는 타입 별칭을 통해 객체 타입을 정의했다.
// 이 객체 타입에는 commit이라는 프로퍼티 키가 있는데, 이 키는 함수 타입이고 그 함수의 타입은 아래 내용과 같다.
type MyMutations = {
// commit의 타입을 지정하기 위해서 아래와 같이..
// commit은 vuex store에서 mutations에 정의되어있는 속성, 함수를 불러올 때 사용한다.
// 즉 아래 코드를 작성하는 이유는 commit 함수를 작성할 때 mutations에 있는 속성과 두번째 인자에 대한 타입 추론이 잘 되게하기 위함이다.
// commit 함수엔 두가지 인자가 넘어온다.
// 첫번째 인자는 mutations 객체안에 속성들 중 하나를 적어주면돼고
// 두번째 인자는 payload이다.
// 아래 두가지는 각 인자의 타입을 정해주는 것이다.
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K, // 첫번째 인자의 타입은 Mutations의 각 프로퍼티 키이다.
payload?: P, // payload는 있어도되고 없어도되는 선택적인 인자이다.
// Parameters 타입은 타입스크립트에 내재되어있는 유틸리티 타입이다. https://www.typescriptlang.org/docs/handbook/utility-types.html
// 함수 유형 Type의 매개변수에 사용된 유형에서 튜플 타입을 생성한다. // 튜플 타입이라는 것이 중요하다.
// type T0 = Parameters<() => string>; // T0의 타입은 []
// type T1 = Parameters<(s: string) => void>; // T1의 타입은 [s: string]
// type T2 = Parameters<<T>(arg: T) => T>; // T2의 타입은 [arg: unknown]
// declare function f1(arg: {a: number; b: string}): void;
// type T3 = Parameters<typeof f1>; // T3의 타입은 {a: number; b: string;}
// type T4 = Parameters<any>; // T4의 타입은 unknown[]
// type T5 = Parameters<never>; // T5의 타입은 never
// type T6 = Parameters<string>; // error 발생 // Type 'string' does not satisfy the constraint '(...args: any) => any'. // T6의 타입은 never
// type T7 = Parameters<Function>; // error 발생 // Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.
// T7의 타입은 never
// 위 조사를 통해 알 수 있는 사실은 다음과 같다!!! --------------------
// Parameters<Mutations[K]>[1]
// Mutations[K]에 해당하는 함수, 예를 들어 [MutationTypes.SET_TOKEN](state: RootState, token: string) { state.token = token; } -> 즉, [state: RootState, token: string] 타입으로 추론된다.
// [state: RootState, token: string] 이렇게 추론되는데 여기서 [1] 즉, 2번째 인자를 선택했으므로 결국 token: string이 선택된 것이다.
// payload의 타입추론은 token: string이 된다.
options?: CommitOptions
): ReturnType<Mutations[K]>;
// ReturnType은 타입스크립트에 내재되어있는 유틸리티 타입이다. https://www.typescriptlang.org/ko/docs/handbook/utility-types.html
// 함수 Type의 반환 타입으로 구성된 타입을 생성한다.
// type T0 = ReturnType<() => string>; // T0의 타입은 string이 된다.
// type T1 = ReturnType<(s: string) => void>; // T1의 타입은 void
// type T2 = ReturnType<<T>() => T>; // T2의 타입은 unknown
// type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // T3의 타입은 number[]
// declare function f1(): {a: number; b: string;}
// type T4 = ReturnType<typeof f1>; // T4의 타입은 {a: number; b: string;}
// type T5 = ReturnType<any>; // T5의 타입은 any
// type T6 = ReturnType<never>; // T6의 타입은 never
// type T7 = ReturnType<string>; // error 발생 : Type 'string' does not satisfy the contraint '(...args: any) => any'. // T7의 타입은 any
// type T8 = ReturnType<Function>; // error 발생 : Type 'Function' does not satisfy the contraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.
// T8의 타입은 any
// 위 조사를 통해 알 수 있는 사실은 다음과 같다!!! --------------------
// ReturnType<Mutations[K]>, 즉, Mutations[K] 함수의 반환값의 타입이 위의 commit 함수의 return 값의 타입이된다.
}
// Store는 vuex 라이브러리에 내재되어있는 타입이다.
// RootState은 내가 정의한 state 타입이다.
// Store의 타입에서, commit 프로퍼티를 빼고, 위에서 정의한 MyMutations 타입을 intersection type(&)으로 합쳐준다.
// 즉, 기존에 정의되어있던 commit 타입을 빼고, 내가 새로 정의한 commit 타입을 넣어준다.
export type MyStore = Omit<Store<RootState>, "commit"> & MyMutations
위 코드는 vuex
내부적으로 정의된 타입에 우리가 애플리케이션에서 사용하기 위해 작성한 state
, mutations
타입 코드를 추가한 코드입니다.
새로 정의된 MyStore
타입을 이제 프로젝트에서 인식할 수 있게만 해주면 됩니다.
vuex
내부 타입이 궁금하신 분들은 Store
타입을 쫓아서 들어가보시면된다.
선행학습 - 프로젝트 타입 정의 확장하기
위에서 정의한 MyStore
타입을 아래와 같이 컴포넌트 옵션 속성에서 추론될 수 있게 해보겠습니다.
vue
+ typescript
프로젝트 루트 레벨에 src/types/project.d.ts
파일을 생성하고 아래 내용을 작성합니다.
// src/types/project.d.ts
import Vue from 'vue';
import { MyStore } from '../store/types';
declare module "vue/types/vue" {
interface Vue {
$store: MyStore;
}
}
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
store?: MyStore;
}
}
다음으로 프로젝트의 타입스크립트 설정 파일(tsconfig.json
)에 아래 옵션을 추가합니다.
// ...
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx",
"src/types/**.d.ts", // 이거추가
],
"exclude": [
// ...
]
그리고 node_modules/vuex/types/vue.d.ts
파일을 삭제합니다.
의문
그런데 node_modules
폴더는 git에 올라가는게 아니므로 이 기록은 공유가 되지 않는데?
그 문제는 어떻게 해결할까?
여튼 이제 사용중인 개발 툴이나 코드 편집기를 종료하고 다시 실행하여 추론이 잘 되는지 확인합니다.
vue 2에는 node_modules 밑의 타입 선언 파일을 지워줘야하지만, vue 3에서는 내부 라이브러리를 건드리지 않고도 확장할 수 있게 다음과 같은 인터페이스가 제공된다.
Vuex 4 릴리즈 노트
아니 그럼 vue2에선 vuex 타입 정의 방법이 없는거야?
선행학습 - actions 정의
actions
함수도 아래와 같이 정의할 수 있습니다.
// store/actions.ts
import { ActionContext } from 'vuex';
import { Mutations } from './mutations';
import { RootState } from './state';
export enum ActionTypes {
FETCH_NEWS = 'FETCH_NEWS'
}
interface News {
title: string;
id: number;
}
// MyActionContext 타입 별칭을 정의하고 타입을 정의한다.
type MyActionContext = {
// MyActionContext 타입엔 commit이라는 속성이 있다. 이 commit은 함수 형태이다.
// K는 Mutations의 프로퍼티 키이다.
commit<K extends keyof Mutations>(
key: K, // 첫번째 인자의 타입은 K
payload?: Parameters<Mutations[K]>[1] // 두번째 인자는 있어도되고 없어도된다. // 두번째 인자의 타입은 Mutataions[K] 함수의 두번째 인자의 타입이다.
): ReturnType<Mutations[K]>; // 이 commit 함수의 반환값 타입은 Mutations[K] 함수가 반환하는 값의 타입이다.
} & Omit<ActionContext<RootState, RootState>, "commit">;
// MyActionContext 타입은 ActionContext라는 vuex 내재 타입에서 commit 프로퍼티를 뺀 것과 커스텀 객체를 합한 타입이다.
export const actions = {
// 위에서 정의한 MyActionContext 타입을 아래 함수의 첫번째 인자의 타입으로 설정한다.
async [ActionTypes.FETCH_NEWS](context: MyActionContext, payload?: number) {
const res = await fetch('https://jsonplaceholder.typicode.com/users/1');
const user: News[] = await res.json();
return user;
}
}
export type Actions = typeof actions;
store
커스텀 타입이 정의된 파일에 아래 내용을 추가합니다.
// store/types.ts
import { CommitOptions, DispatchOptions, Store } from "vuex";
import { Actions } from "./actions";
import { Mutations } from "./mutations";
import { RootState } from "./state";
type MyMutations = {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload?: P,
options?: CommitOptions
): ReturnType<Mutations[K]>;
};
type MyActions = {
// MyActions 타입 객체 안에 dispatch 라는 프로퍼티가 있다. dispatch 프로퍼티는 함수 타입이다.
// Actions 타입의 키를 K 타입으로 정의한다.
dispatch<K extends keyof Actions>(
key: K, // 첫번째 인자의 타입은 Actions 타입의 프로퍼티 키이다. 'commit'
payload?: Parameters<Actions[K]>[1], // 두번째 인자의 타입은 Actions[K] 함수의 두번째 인자의 타입
options?: DispatchOptions
): ReturnType<Actions[K]>; // dispatch 함수의 리턴값의 타입은 Actions[K] 함수의 리턴값의 타입이다.
};
// MyStore 타입은 Store<RootState> 타입에서 commit, dispatch 속성을 빼고 MyMutations, MyActions 타입을 합한 타입이다.
export type MyStore = Omit<
Store<RootState>,
"commit" | "dispatch"
> &
MyMutations &
MyActions;
선행학습 - getters 정의
getters
속성 함수는 다음과 같이 정의합니다.
// store/getters.ts
import { RootState } from './state';
export const getters = {
getToken(state: RootState) {
return state.token + "!";
}
}
export type Getters = typeof getters;
store
커스텀 파일에 아래와 같이 추가합니다.
import { Action, CommitOptions, DispatchOptions, Store } from "vuex";
import { Actions } from "./actions";
import { Getters } from "./getters";
import { Mutations } from "./mutations";
import { RootState } from "./state";
type MyMutations = {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload?: P,
options?: CommitOptions
): ReturnType<Mutations[K]>;
};
type MyActions = {
dispatch<K extends keyof Actions>(
key: K,
payload?: Parameters<Actions[K]>[1],
options?: DispatchOptions
): ReturnType<Actions[K]>;
};
type MyGetters = {
getters: {
// K는 Getters의 프로퍼티 키이다.
// K에 할당되는 값들의 타입은 Getters[K] 함수가 반환하는 값의 타입이다.
[K in keyof Getters]: ReturnType<Getters[K]>;
};
};
export type MyStore = Omit<
Store<RootState>,
"getters" | "commit" | "dispatch"
> &
MyMutations &
MyActions &
MyGetters;
스토어 TS 파일 변환 및 컴포넌트의 타입 추론 문제 소개
store/index.js
파일을store/index.ts
파일로 수정한다.- 수정해도 딱히 타입스크립트 에러는 발생하지 않는다.
store/index.ts
를 컴포넌트에서 가져다쓸 때 어떤 문제가 생기는지 보자.
store/index.js 파일을 store/index.ts 파일로 수정하고나서 컴포넌트에서 가져다 쓸 때 발생하는 에러
- 스토어 TS 파일 변환 및 컴포넌트의 타입 추론 문제 소개
this.$store.state.
: 여기서state
에 무엇이 있는지 추론이 되질 않는다.- 타입스크립트를 사용하는 의미가 없게된다.
스토어의 타입 추론이 안되는 이유 분석
- 타입스크립트 선언 병합(declaration merging)
- 추론이 안되는 이유를 분석하기 위해 내부 추론 코드를 보도록 하겠습니다.
this.$store
에 마우스 커서를 올려놓고 웹스톰은 ctrl + b
// node_modules/vuex/types/vue.d.ts
/**
* Extends interfaces in Vue.js
*/
import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
store?: Store<any>;
}
}
declare module "vue/types/vue" {
interface Vue {
$store: Store<any>;
}
}
- 위에 보면
$store
타입을 Vue 내부에서Store<any>
로 정의해놨다. - 그런데 정의된 코드를 보면
node_modules/vue/types
폴더 안에vue.d.ts
파일에 정의되어있는interface Vue
에$store
타입을 확장해서 넣어놓았다. - 아래와 같이
node_modules/vue/types/vue.d.ts
파일에 이미 여러 타입들이 정의되어있고 여기에vuex/types/vue.d.ts
에서$store
에 대한 타입을 확장했다는 것이다.
// node_modules/vue/types/vue.d.ts
// ...
export interface Vue {
readonly $el: Element;
readonly $options: ComponentOptions<Vue>;
readonly $parent: Vue;
readonly $root: Vue;
readonly $children: Vue[];
readonly $refs: { [key: string]: Vue | Element | (Vue | Element)[] | undefined };
readonly $slots: { [key: string]: VNode[] | undefined };
readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined };
readonly $isServer: boolean;
readonly $data: Record<string, any>;
readonly $props: Record<string, any>;
readonly $ssrContext: any;
readonly $vnode: VNode;
readonly $attrs: Record<string, string>;
readonly $listeners: Record<string, Function | Function[]>;
$mount(elementOrSelector?: Element | string, hydrating?: boolean): this;
$forceUpdate(): void;
$destroy(): void;
$set: typeof Vue.set;
$delete: typeof Vue.delete;
$watch(
expOrFn: string,
callback: (this: this, n: any, o: any) => void,
options?: WatchOptions
): (() => void);
$watch<T>(
expOrFn: (this: this) => T,
callback: (this: this, n: T, o: T) => void,
options?: WatchOptions
): (() => void);
$on(event: string | string[], callback: Function): this;
$once(event: string | string[], callback: Function): this;
$off(event?: string | string[], callback?: Function): this;
$emit(event: string, ...args: any[]): this;
$nextTick(callback: (this: this) => void): void;
$nextTick(): Promise<void>;
$createElement: CreateElement;
}
// ...
즉, 위와 같이 vue3
가 아니더라도 vue2
에서도 간단하게 타입추론이 일어날 수 있도록 타입 정리를 해놓은 것을 볼 수 있다.
- 여튼
vuex/types/vue.d.ts
,vue/types/vue.d.ts
에서 동일하게interface Vue
를 가져다 썼으므로 인터페이스 머징이라는 것이 일어나게된다. - 인터페이스 머징
- declaration merging: 선언을 합친다.
// node_modules/vuex/types/vue.d.ts
/**
* Extends interfaces in Vue.js
*/
import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
store?: Store<any>;
}
}
declare module "vue/types/vue" {
interface Vue {
$store: Store<any>;
}
}
- 여튼
vuex
를 썼을 때interface Vue
가 확장된다. - 여기서 제일 중요한 것은
Store<>
여기에any
라는 타입이 정의되어있기 때문에- 실제로 컴포넌트 레벨에서
$store
에 접근하게되면, 즉,this.$store
에 마우스 커서를 올리면any
라고 타입이 추론되게 된다.
- 실제로 컴포넌트 레벨에서
- 즉,
this.$store
안에 내부적으로 설계한 타입들이 전부 다any
를 기준으로 추론되게 된다.
this.$store 안에 내부적으로 설계한 타입들이 전부 다 any를 기준으로 추론되게 된다 라는 말은 무슨말?
// node_modules/vuex/types/vue.d.ts
/**
* Extends interfaces in Vue.js
*/
import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
store?: Store<any>;
}
}
declare module "vue/types/vue" {
interface Vue {
$store: Store<any>;
}
}
위 코드의 Store
에 마우스 커서를 올려놓고 ctrl + b를 누른다.
// node_modules/vuex/types/index.d.ts
// ...
export declare class Store<S> {
constructor(options: StoreOptions<S>);
readonly state: S;
readonly getters: any;
replaceState(state: S): void;
dispatch: Dispatch;
commit: Commit;
subscribe<P extends MutationPayload>(fn: (mutation: P, state: S) => any, options?: SubscribeOptions): () => void;
subscribeAction<P extends ActionPayload>(fn: SubscribeActionOptions<P, S>, options?: SubscribeOptions): () => void;
watch<T>(getter: (state: S, getters: any) => T, cb: (value: T, oldValue: T) => void, options?: WatchOptions): () => void;
registerModule<T>(path: string, module: Module<T, S>, options?: ModuleOptions): void;
registerModule<T>(path: string[], module: Module<T, S>, options?: ModuleOptions): void;
unregisterModule(path: string): void;
unregisterModule(path: string[]): void;
hasModule(path: string): boolean;
hasModule(path: string[]): boolean;
hotUpdate(options: {
actions?: ActionTree<S, S>;
mutations?: MutationTree<S>;
getters?: GetterTree<S, S>;
modules?: ModuleTree<S>;
}): void;
}
// ...
- 제네릭
<>
여기에any
가 들어가고 그걸 통해서 다른 타입들도 추론된다. - 결론적으로
readonly state: S;
의state
에any
가 들어와버려서this.$store.state
에 마우스 커서를 올려놨을 때any
가 추론된다는 것이다. - 그래서
this.$store.state
에 설정해놓은news
라던가list
라던가jobs
라던가 이런 속성들을 정의하더라도 자동으로 추론되지 않는, 이런 문제가 있는 것이다. - 이런 부분들을 프로젝트 레벨에서
store
에 코드들을 정의했을 때 자동으로 추론되게끔 하는 방법에 대해 알아보도록 하겠다.
vuex 타입 정의 방법 안내 - state
-
Vue.extend()
방식을 이용하여 컴포넌트에 타입의 정의하고 있기 때문에
Vue.extend()
안에서this.$store.state
또는this.$store.state
등을 했을 때
vuex
에 정의한 것들이 추론될 수 있게하는 방법에 대해 정리한 글이다.
vuex 기본 코드
- 아래 기본 코드는 자바스크립트건 타입스크립트건 아래와 같이 작성해야된다.
- 아래와 같이 기본 코드를 작성한 후에 추가적으로 타입 추론을 위한 코드를 넣으면된다.
// store/index.ts
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const store = {
state: {
token: '',
}
}
export default new Vuex.Store(store);
state 정의
위 기본 코드에서 state
를 별도의 파일로 분리한다.
store/state.ts
로 만들도록하자.
// store/state.ts
export const state = {
token: '',
}
export type RootState = typeof state;
사실 분리를 안해도되지만 분리를 하는 것을 추천드린다.
왜냐하면 타입스크립트 코드들도 같이 들어가기 때문에 store/index.ts
파일 안에 몽땅 다 작성해버리면, store/index.ts
파일의 내용이 엄청 많아질 것이다.
그렇게되면 코드 파악이 어려워지고 한눈에 보는 것도 어려워지기 때문에 가급적이면 state
, getters
, mutations
, actions
이런 형태로 모듈화를 해주시는 것을 추천드린다.
export type RootState = typeof state;
제일 중요한 코드이다.
export
에서 type
을 선언해주는 것이다.
즉, const state
에 선언한 타입을 뽑아내어 그거를 RootState
라는 타입 변수에 정의해준 것이다.
state 타입 정의
// src/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import { state } from '@/store/state';
Vue.use(vuex);
const store = {
state,
}
export default new Vuex.Store(store);
// src/store/state.ts
import { NewsItem } from '@/api';
const state = {
news: [] as NewsItem[],
}
type RootState = typeof state;
export { state, RootState }
- 아직 추론이 제대로되지 않는다.
store/state.ts
에 정의한 타입이Store
타입의 제네릭으로 넘어가게끔 다음 시간에 설정해보도록 하겠다.- 여전히
this.$store
에 마우스 커서를 올려놓으면any
로 추론되고 있다.
스토어 내부 타입에 state 타입 연결
store
의state
타입이 컴포넌트에서 추론될 수 있도록 작업을 해보도록 하겠다.Store<any>
여기서any
를RootState
로 바꿔주면 된다.this.$store
에 마우스 커서를 찍고ctrl + b
를 누른 다음에 (웹스톰 기준)
any
를 RootState
로 바꾼다..?
이건 근본적인 해결책이 아니다.node_modules
폴더 내에 있는 코드를 수정하면 어차피 git
으로 관리되는 파일이 아니므로 내가 변경한 내용이 다른 작업자들한테도 공유되지 않기 때문이다.
그래서 이 방법은 의미가 없다.
이 이후에 다른 해결책 알려주겠지?
// node_modules/vuex/types/vue.d.ts
/**
* Extends interfaces in Vue.js
*/
import { RootState } from "@/store/state";
import Vue, { ComponentOptions } from "vue";
import { Store } from "./index";
declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
store?: Store<RootState>;
}
}
declare module "vue/types/vue" {
interface Vue {
$store: Store<RootState>;
}
}
mutations 타입 정의 안내
- ES6 computed property 문법
- ES6 Enhanced Object Literals
mutations 정의
mutations
코드도 store/mutation.ts
파일에 별도 작성한다.
// store/mutations.ts
import { RootState } from "./state";
// 뮤테이션 타입
export enum MutationTypes {
SET_TOKEN = "SET_TOKEN",
}
// 뮤테이션 속성 함수
export const mutations = {
[MutationTypes.SET_TOKEN](state: RootState, token: string) {
state.token = token;
},
}
export type Mutations = typeof mutations;
mutations 타입 정의
// store/mutations.ts
import { RootState } from '@/store/state';
import { NewsItem } from '@/api';
enum MutationTypes {
SET_NEWS = 'SET_NEWS',
}
const mutations = {
[MutationTypes.SET_NEWS](state: RootState, news: NewsItem[]) {
state.news = news;
},
}
type Mutations = typeof mutations;
export { mutations, Mutations }
// store/index.ts
import Vue from 'vue';
import Vuex, { StoreOptions } from 'vuex';
import { RootState, state } from '@/store/state';
import { mutations } from '@/store/mutations';
Vue.use(vuex);
const store: StoreOptions<RootState> = {
state,
mutations,
}
export default new Vuex.Store(store);
store 타입 추론을 위한 타입 파일 작성
mutations
코드를 정의했다.
이 mutations
코드의 타입들이 컴포넌트 레벨에서 잘 추론될 수 있도록 중간에 매개 코드를 작성해보도록 하겠다.
node_modules
안에 타입 추론되게 해도 모든 것에 대해서 추론되게 할 수는 없다.
예를 들어 mutations
을 호출하는 API
인 commit
이라던가actions
를 호출하는 dispatch
같은 것을 쓸 때
타입 추론이 자동으로 되게끔 하는 것은 어렵다.
이런 부분들이 vue2
에서는 타입스크립트 추론을 생각하고 그걸 고려해서 만든 것이 아니기 때문에 이렇게 한계점이 있는 것이다.
다행히도 지금 수업을 듣는 이 시점에서는 vue3
가 나와있고, 안정화 버전도 나왔고 vuex
도 4점대 버전이 나와있기 때문에
안정화된 이 2개의 라이브러리들에서 현재보다 훨씬 좀 더 유연하게 타입을 확장할 수 있는 형태로 제공이 되고 있다.
그런 부분들을 다 참고하면 좋을 것 같다.
강의 후반부에 vue3
, vuex
관련해서도 말씀드릴 예정이다.
vue 컴포넌트에서 활용할 수 있도록 vuex 커스텀 타입 정의
글 서두에 언급한 것처럼 vuex
의 내부 타입 방식으로는 위에서 정의한 state
와 mutations
가 올바르게 추론되지 않습니다.
이 문제를 해결하기 위해 store/types.ts
에 아래와 같이 작성합니다.
// store/types.ts
import { CommitOptions, Store } from "vuex";
import { Mutations } from "./mutations";
import { RootState } from "./state";
type MyMutations = {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload?: P, // payload는 있어도되고 없어도되는 선택적인 인자이다.
// Parameters 타입은 타입스크립트에 내재되어있는 유틸리티 타입이다. https://www.typescriptlang.org/docs/handbook/utility-types.html
// 함수 유형 Type의 매개변수에 사용된 유형에서 튜플 타입을 생성한다. // 튜플 타입이라는 것이 중요하다.
options?: CommitOptions
): ReturnType<Mutations[K]>;
}
export type MyStore = Omit<
Store<RootState>,
"commit"
> & MyMutations
// store/types.ts
import { CommitOptions, Store } from 'vuex';
import { Mutations } from './mutations';
import { RootState } from './state';
// MyMutations라는 타입 별칭을 통해 객체 타입을 정의했다.
// 이 객체 타입에는 commit이라는 프로퍼티 키가 있는데, 이 키는 함수 타입이고 그 함수의 타입은 아래 내용과 같다.
type MyMutations = {
// commit의 타입을 지정하기 위해서 아래와 같이..
// commit은 vuex store에서 mutations에 정의되어있는 속성, 함수를 불러올 때 사용한다.
// 즉 아래 코드를 작성하는 이유는 commit 함수를 작성할 때 mutations에 있는 속성과 두번째 인자에 대한 타입 추론이 잘 되게하기 위함이다.
// commit 함수엔 두가지 인자가 넘어온다.
// 첫번째 인자는 mutations 객체안에 속성들 중 하나를 적어주면돼고
// 두번째 인자는 payload이다.
// 아래 두가지는 각 인자의 타입을 정해주는 것이다.
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K, // 첫번째 인자의 타입은 Mutations의 각 프로퍼티 키이다.
payload?: P, // payload는 있어도되고 없어도되는 선택적인 인자이다.
// Parameters 타입은 타입스크립트에 내재되어있는 유틸리티 타입이다. https://www.typescriptlang.org/docs/handbook/utility-types.html
// 함수 유형 Type의 매개변수에 사용된 유형에서 튜플 타입을 생성한다. // 튜플 타입이라는 것이 중요하다.
// type T0 = Parameters<() => string>; // T0의 타입은 []
// type T1 = Parameters<(s: string) => void>; // T1의 타입은 [s: string]
// type T2 = Parameters<<T>(arg: T) => T>; // T2의 타입은 [arg: unknown]
// declare function f1(arg: {a: number; b: string}): void;
// type T3 = Parameters<typeof f1>; // T3의 타입은 {a: number; b: string;}
// type T4 = Parameters<any>; // T4의 타입은 unknown[]
// type T5 = Parameters<never>; // T5의 타입은 never
// type T6 = Parameters<string>; // error 발생 // Type 'string' does not satisfy the constraint '(...args: any) => any'. // T6의 타입은 never
// type T7 = Parameters<Function>; // error 발생 // Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.
// T7의 타입은 never
// 위 조사를 통해 알 수 있는 사실은 다음과 같다!!! --------------------
// Parameters<Mutations[K]>[1]
// Mutations[K]에 해당하는 함수, 예를 들어 [MutationTypes.SET_TOKEN](state: RootState, token: string) { state.token = token; } -> 즉, [state: RootState, token: string] 타입으로 추론된다.
// [state: RootState, token: string] 이렇게 추론되는데 여기서 [1] 즉, 2번째 인자를 선택했으므로 결국 token: string이 선택된 것이다.
// payload의 타입추론은 token: string이 된다.
options?: CommitOptions
): ReturnType<Mutations[K]>;
// ReturnType은 타입스크립트에 내재되어있는 유틸리티 타입이다. https://www.typescriptlang.org/ko/docs/handbook/utility-types.html
// 함수 Type의 반환 타입으로 구성된 타입을 생성한다.
// type T0 = ReturnType<() => string>; // T0의 타입은 string이 된다.
// type T1 = ReturnType<(s: string) => void>; // T1의 타입은 void
// type T2 = ReturnType<<T>() => T>; // T2의 타입은 unknown
// type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // T3의 타입은 number[]
// declare function f1(): {a: number; b: string;}
// type T4 = ReturnType<typeof f1>; // T4의 타입은 {a: number; b: string;}
// type T5 = ReturnType<any>; // T5의 타입은 any
// type T6 = ReturnType<never>; // T6의 타입은 never
// type T7 = ReturnType<string>; // error 발생 : Type 'string' does not satisfy the contraint '(...args: any) => any'. // T7의 타입은 any
// type T8 = ReturnType<Function>; // error 발생 : Type 'Function' does not satisfy the contraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.
// T8의 타입은 any
// 위 조사를 통해 알 수 있는 사실은 다음과 같다!!! --------------------
// ReturnType<Mutations[K]>, 즉, Mutations[K] 함수의 반환값의 타입이 위의 commit 함수의 return 값의 타입이된다.
}
// Store는 vuex 라이브러리에 내재되어있는 타입이다.
// RootState은 내가 정의한 state 타입이다.
// Store의 타입에서, commit 프로퍼티를 빼고, 위에서 정의한 MyMutations 타입을 intersection type(&)으로 합쳐준다.
// 즉, 기존에 정의되어있던 commit 타입을 빼고, 내가 새로 정의한 commit 타입을 넣어준다.
export type MyStore = Omit<Store<RootState>, "commit"> & MyMutations