1. 실습 프로젝트1: Todo App
가. 함수 및 변수의 타입 선언
1) JS 프로젝트를 TS 기반으로 변경 시
•
우선 type을 any로 전부 바꿔주고 하나씩 type을 바꿔주는 방식을 추천
2) 배열 타입의 축약 정의
•
Array<object> → object[]
function fetchTodoItems_1(): Array<object> {
const todos = [
{ id: 1, title: '안녕', done: false },
{ id: 2, title: '타입', done: false },
{ id: 3, title: '스크립트', done: false },
];
return todos;
}
function fetchTodoItems_2(): object[] {
const todos = [
{ id: 1, title: '안녕', done: false },
{ id: 2, title: '타입', done: false },
{ id: 3, title: '스크립트', done: false },
];
return todos;
}
TypeScript
복사
•
주의할 것은 Tuple 타입과 생김새가 비슷하므로 헷갈리면 안된다는 것
// Tuple 형태 타입 선언
let arr: [string, number] = ['hi', 10];
// Array 형태 타입 선언
let arr: string[] = ['hi', 'hi']
TypeScript
복사
3) 함수의 인자에 대한 타입 선언 시 주의할 점
•
함수에 정의된 매개변수의 갯수 보다 적은 수를 넘길 것에 대한 제한을 두고 싶지 않을 때, ?를 이용
function sum(a: number, b?: number): number {
return a + b;
}
sum(10, 20); // 30
sum(10, 20, 30); // error, too many parameters
sum(10); // 10
TypeScript
복사
•
매개변수의 default 값 설정은 ES6 문법과 동일
function sum(a: number, b = '100'): number {
return a + b;
}
sum(10, undefined); // 110
sum(10, 20, 30); // error, too many parameters
sum(10); // 110
TypeScript
복사
나. 객체 타입의 구체화
•
객체 내 각 필드에 대한 구체적인 타입을 요구할 때, 다음과 같은 형식으로 정의함
// 객체를 요소로 갖는 배열의 경우 아래와 같이 타입을 구체화
function fetchTodoItems(): object[] {
const todos = [
{ id: 1, title: '안녕', done: false },
{ id: 2, title: '타입', done: false },
{ id: 3, title: '스크립트', done: false },
];
return todos;
}
// 타입의 구체화
function fetchTodoItems(): {id: number, title: string, done: boolean}[] {
const todos = [
{ id: 1, title: '안녕', done: false },
{ id: 2, title: '타입', done: false },
{ id: 3, title: '스크립트', done: false },
];
return todos;
}
TypeScript
복사
다. 인터페이스 도입에 따른 중복 타입 제거
•
중복되는 타입에 대해 인터페이스를 도입하여 코드를 간결하게 작성할 수 있음
// 인터페이스 도입 전
function fetchTodoItems(): {id: number, title: string, done: boolean}[] {
const todos = [
{ id: 1, title: '안녕', done: false },
{ id: 2, title: '타입', done: false },
{ id: 3, title: '스크립트', done: false },
];
return todos;
}
// 인터페이스 정의
interface Todo {
id: number; title: string; done: boolean;
}
// 인터페이스 도입
function fetchTodoItems(): Todo[] {
const todos = [
{ id: 1, title: '안녕', done: false },
{ id: 2, title: '타입', done: false },
{ id: 3, title: '스크립트', done: false },
];
return todos;
}
TypeScript
복사
•
인터페이스로 정의한 속성의 갯수와 인자로 전달하는 속성의 갯수가 일치하지 않아도 됨
interface personAge {
age: number;
}
function logAge(obj: personAge) {
console.log(obj.age);
}
let person = { name: 'Capt', age: 28 };
logAge(person); // 28 출력
TypeScript
복사
2. 실습 프로젝트2: AddressBook App
가. 제한요건 설정
1) tsconfig.json
•
엄격한 타입 체킹(Any 타입이라도 반드시 명시할 것, 함수 타입 설정 주의)
"noImplicitAny": true,
"strict": true,
"strictFunctionTypes": true
TypeScript
복사
2) .eslintrc.js
•
비동기 함수(promise 등)에 대해 타입 설정
// 아래 조건에 대한 주석처리
// '@typescript-eslint/no-explicit-any': 'off',
// "@typescript-eslint/explicit-function-return-type": 'off',
TypeScript
복사
나. 제네릭에 대해
1) 함수의 인자 및 반환 값에 대해 동일한 제네릭 타입으로 정의한 경우, 해당 함수 사용 시, 특정 타입에 대해 표시하는 것을 생략할 수 있다.
•
아래와 같인 1, 2번 모두 사용 가능하나, 2번처럼 string 표시를 생략하여 가독성을 높일 수 있다
function logText<T>(text: T): T {
return text;
}
// 1
const text = logText<string>("Hello Generic");
// 2
const text = logText("Hello Generic");
TypeScript
복사
다. Promise 타입에 대해
1) Promise와 같이 비동기 함수를 반환할 때, Promise 타입과 동시에 내부 타입까지 정의해야 한다.
다시 말해, 제너릭을 활용해야 한다
•
반면 동기적 함수의 경우, 타입추론에 따라 반환값에 대한 타입을 명시하지 않아도 됨(함수 내부에서 반환값으로 사용되는 변수가 정의되면 해당 변수의 타입을 자동으로 읽어서 반환값에 대한 타입을 추론해줌)
function fetchContacts(): Promise<Contact[]> {
const contacts: Contact[] = [];
return new Promise(resolve => {
setTimeout(() => resolve(contacts), 2000);
});
}
TypeScript
복사
라. 클래스에 대해
1) 클래스의 생성자에는 타입을 설정하지 않는다
2) 타입 설정과 동시에 값을 할당할 수 있다
function fetchContacts(): Promise<Contact[]> {
const contacts: Contact[] = [];
}
TypeScript
복사
마. Enum을 활용한 타입 정의
•
Enum을 사용하지 않을 경우, phoneType에 대한 유형에 오타가 들어와서 에러가 난다고 해도 쉽게 알 수 없다. 이 부분을 제한하기 위해 enum을 도입하는 것이 유지보수에 이점
•
Enum을 사용하기 전
findContactByPhone(phoneNumber: number, phoneType: string): Contact[] {
return this.contacts.filter(
contact => contact.phones[phoneType].num === phoneNumber
);
}
TypeScript
복사
•
Enum을 사용한 후
enum PhoneType{
Home = 'home',
Studio = 'studio',
Office = 'office'
}
findContactByPhone(phoneNumber: number, phoneType: PhoneType): Contact[] {
return this.contacts.filter(
contact => contact.phones[phoneType].num === phoneNumber
);
}
TypeScript
복사
3. 실습프로젝트 3: React & Typescript로 게임 만들기
가. 환경설정
1) typsecript 모듈 세팅: npm init / npm install typescript
2) package.json 내 script 설정
"scripts": {
"test": "webpack"
},
JSON
복사
3) react 세팅: npm i react react-dom // npm i @types/react @types/react-dom
•
react, react-dom은 JS로 작성되었기 때문에 커뮤니티에서 개발한 TS용 모듈을 설치함
4) webpack 세팅: npm i webpack webpack-cli -D
5) ts-loader 세팅: npm i ts-loader -D
6) tsconfig.json
{
"compilerOptions": {
"strict": true,
"lib": ["ES5", "ES2015", "ES2016", "ES2017", "ES2018", "DOM"],
"jsx": "react"
}
}
JSON
복사
7) webpack.config.js
const path = require("path");
const webpack = require("webpack");
// 이하는 개발환경 모드
// 배포환경 시 mode: 'production' / devtool: 'hidden-source-map'
module.exports = {
mode: "development",
devtool: "eval",
resolve: {
extensions: [".jsx", ".js", ".tsx", ".ts"],
},
entry: {
app: "./client",
},
// ts or tsx 파일이 있으면 해당 loader로 문법 변환
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
},
],
},
// dist 폴더 생성 후 app.js 파일 생성
output: {
filename: "[name].js",
path: path.join(__dirname, "dist"),
},
};
JavaScript
복사
나. Typing: EventHanlder, useRef
1) EventHandler의 타입은 이하 메소드까지 고려해여 작성
•
onSubmit 메소드에서 event
→ event의 직속 method를 사용하므로 event의 타입을 interface type(FormEvent)까지 정의
const onSubmitForm = (event: React.FormEvent) => {
event.preventDefault()
// 생략
}
TypeScript
복사
•
onChange 메소드에서 event
→ event.target의 value를 사용하므로 interface type에 제너릭으로 HTMLInputElement까지 정의
const onChangeInput = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value)
}
TypeScript
복사
2) 본문의 useRef의 타입의 경우 타입추론이 안되기 때문에 수동으로 적절한 제네릭을 삽입해야 함
•
useRef는 특정 DOM 요소를 선택할 때 사용하므로 useRef 정의 시 선택하려는 HTML요소 타입을 제너릭으로 타입을 정함
•
'useRef<HTMLInputElement>(null)', 이와 같은 구조는 제너릭을 타입으로 정의하는 기본적인 구조로 useRef라는 hook 내부에서 HTMLInputElement라는 타입을 사용할 수 있도록 정의함
const inputEl = useRef<HTMLInputElement>(null);
const input = inputEl.current;
if (input) {
input.focus();
}
return (
<>
<div>
<input ref={inputEl} onChange={onChangeInput} type="number" value={InputValue} placeholder="정답을 입력하세요" />
</div>
</>
);
TypeScript
복사
다. Typing: useCallback, useMemo, FC
1) useCallback은 재사용되는 함수의 성능을 최적화시켜줌. 제로초는 JSX 부분에 사용되는 함수의 경우 모두 useCallback을 사용할 것을 권장. 컴포넌트의 결과물을 리렌더링하지 않고 재사용할 수 있기 때문에 성능 향상
→ useCallback의 원리는 배열 인자 안의 값이 바뀌는 경우만 새로 불러오고 그렇지 않으면 캐싱해둔 메소드를 사용함
2) useCallback으로 특정 메소드를 감쌀 경우, 감싼 메소드의 event 인자에 대한 타입추론이 불가하므로 수동으로 제네릭을 정의해야함
// SuggestedWord, InputValue 앞의 두 State의 상태변화 발생 시 useCallbck 호출
const onSubmitForm = useCallback(
(event: React.FormEvent) => {
event.preventDefault()
// 생략
}, [SuggestedWord, InputValue])
TypeScript
복사
3) useMemo는 두번째 매개변수 배열 안의 값이 변경될 때만 재연산 들어감. 의존값이 없다면 리렌더링이 되더라도 해당 값 변경 X
→ 리렌더링 될 때마다 값이 바뀌는 것을 고정하고 싶을 때 사용 가능
4) FunctionComponent(FC): 함수형 컴포넌트에 대한 타입으로 사용됨
•
FC에서 props에 대한 타입을 정의할 때, props 내 속성에 대한 타입을 지정한다
const [bonus, setBonus] = useState<number | null>(null);
return (
<>
<div>당첨 숫자</div>
<div id="결과창">
{winBalls.map((v) => <Ball key={v} ballnumber={v} />)}
</div>
<div>보너스!</div>
{bonus && <Ball ballnumber={bonus} />}
</>
)
TypeScript
복사
const Ball = (props: { ballnumber: number }) => {
return (
<div className="ball" style={{ background }}>
{props.ballnumber}
</div>
);
};
TypeScript
복사
라. Typing: Props
1) 빈배열을 초기값으로 설정할 때, Type Error 발생: 제네릭으로 배열에 대한 타입을 정의하여 해결
2) 사용자 정의 타입의 경우 다른 파일에 몰아서 저장
// types.ts
export interface TryInfo {
try: string;
result: string;
}
export interface TryProps {
tryInfo: TryInfo;
}
TypeScript
복사
import { TryInfo } from './types';
const [tries, setTries] = useState<TryInfo[]>([]);
TypeScript
복사
3) Props 처리
•
Try는 함수형 컴포넌트이므로 기본 타입은 React.FuntionComponent가 들어감
→ import {FunctionComponent} from 'react' 선언하여 FuntionComponent으로만 사용 가능
→ props의 tryInfo 자료의 경우 customized된 형태이므로, 타입도 제네릭으로 cumtomize해야 함
•
props 구조를 뜯어보면 props={tryInfo.try, tryInfo.result) 이와 같음. 따라서 props의 타입도 이에 매칭되어야함 TryProps = {TryInfo.try, TryInfo.result}
// Try.tsx
import * as React from 'react';
import { TryProps } from './types';
const Try: React.FunctionComponent<TryProps> = (props) => {
return (
<li>
<div>{props.tryInfo.try}</div>
<div>{props.tryInfo.result}</div>
</li>
);
};
export default Try;
TypeScript
복사
<div>시도: {tries.length}</div>
<ul>
{tries.map((v, i) => (
<Try key={`${i + 1}차 시도 : ${v.try}`} tryInfo={v} />
))}
</ul>
TypeScript
복사
마. Typing: setTimeout, setInterval, useRef
1) setTimeout(setInterval도 같음): node.setTimeout vs window.setTimeout
•
setTimeout에는 nodeJS.timeout으로 반환 받는 타입이 있고, number로 반환 받는 타입이 있음
•
window에서 사용하는 setTimeout이므로 앞에 window를 붙여서 함수의 용도를 적절하게 잡는다
// ResponseCheck.tsx
timeout.current = window.setTimeout(() => {
setState('now');
startTime.current = new Date().getTime();
}, Math.floor(Math.random() * 1000) + 2000);
TypeScript
복사
•
setInterval도 setTimeout과 유사
interval.current = window.setInterval(changeHand, 100);
TypeScript
복사
2) useRef: overload된 3가지의 타입 중 적절한 것을 선택
•
본문에서 current 값을 수시로 변경해야 하므로 readonly 타입을 사용하면 안됨
•
overload된 useRef 중 반환값이 RefObject가 아닌 MutableRefObject인 함수를 사용해야 함.
// 제네릭 인자로 undefined를 받고 MutableRefObect를 반환
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
// 제네릭 인자로 T를 받고, 함수 인자로 T를 받아서 MutableRefObject를 반환
function useRef<T>(initialValue: T): MutableRefObject<T>;
// 제네릭 인자로 T를 받고, 함수 인자로 T 또는 null을 받아서 RefObject를 반환
function useRef<T>(initialValue: T|null): RefObject<T>;
interface MutableRefObject<T> {
current: T;
}
interface RefObject<T> {
readonly current: T | null;
}
TypeScript
복사
// ResponseCheck.tsx
const timeout = useRef<number | null>(null);
TypeScript
복사
•
이를 위해 제네릭 T와 initalValue T를 동일하게 설정하여 함수를 선언해야함
→ X: const timeout = useRef<number>(null);
→ useRef<number>(initialValue: number | null): React.RefObject<number>
→ O: const timeout = useRef<number | null>(null);
→ useRef<number | null>(initialValue: number | null): React.MutableRefObject<number | null>
바. Typing: as, keyof, typeof
1) 'as'는 타입의 고정화에 사용됨. 타입을 보다 구체적으로 만들거나, 지원하는 타입 메소드의 한계를 보완할 때 사용
•
타입을 보다 구체적으로 만들 때
// 앞으로 정의된 값으로만 사용될 상수의 경우, as로 const 상수 타입 표현함
// as const를 제외하면 rspCoords 내 변수의 타입이 string으로 고정됨
const rspCoords = {
바위: '0',
가위: '-142px',
보: '-284px'
} as const;
// as const를 제외하면 scores 내 변수의 타입이 number로 고정됨
const scores = {
가위: 1,
바위: 0,
보: -1,
} as const;
TypeScript
복사
•
타입 메소드의 한계를 보완할 때
// Object.keys(rspCoords))을 통해 의도하는 것은 rspCoords의 key인 바위, 가위, 보를 뽑는 것
// 하지만 rspCoords의 경우 as const로 타입을 고정했기 때문에 keys 메소드의 반환값으로
// rspCoords 타입으로 받아야하지만 keys의 반환 타입은 문자열 배열임.
// 따라서 as를 활용하여 반환 타입을 강제하여 keys 메소드의 한계를 보완함
// ObjectConstructor.keys(o: {}): string[] (+1 overload)
Object.keys(rspCoords) as keysImgCoords[]
// 또는 아래와 같이 타입을 직접 명시
Object.keys(rspCoords) as ['바위', '가위', '보']
TypeScript
복사
2) keyof, typeof: 이미 선언된 타입의 일부분만 활용하여 타입을 생성할 때 사용
•
일부분에 대해 그냥 기입하여 타입을 생성할 수 있지만, 앞으로의 유지보수를 고려할 때, 기존 코드를 수정하면 자동으로 반영될 수 있도록 만드는 것이 효율적
// 두번째 줄 방법으로 만들 수 있지만 추후 rspCoords에 수정될 경우를 감안하여
// 첫번째 줄 방법으로 타입을 선언할 것을 권장
type ImgCoords = typeof rspCoords[keyof typeof rspCoords];
type ImgCoords = "0" | "-142px" | "-284px"
TypeScript
복사
•
rspCoords 내 value로 타입을 만들고 싶을 때, keyof와 typeof를 잘 섞어서 만들 수 있음
const rspCoords = {
바위: '0',
가위: '-142px',
보: '-284px'
} as const;
TypeScript
복사
•
typeof: 특정 타입에 대한 복사
type a = typeof rspCoords;
// typeof를 사용하면 아래와 같이 타입이 정의됨
type a = {
readonly 바위: "0";
readonly 가위: "-142px";
readonly 보: "-284px";
}
TypeScript
복사
•
keyof: 특정 타입의 키로만 이루어진 타입 생성
// keyof를 사용하면 특정 타입 내 key로만 이루어진 타입이 생성됨
type a = keyof typeof rspCoords;
type a = "바위" | "가위" | "보"
TypeScript
복사
•
특정 타입의 일부분만 담은 타입 생성 가능
// rspCoords로 생성한 타입의 value로 이루어진 타입 생성
type a = typeof rspCoords[keyof typeof rspCoords];
type a = "0" | "-142px" | "-284px"
TypeScript
복사
3) undefined 처리
•
아래 코드에서 computerChoice의 리턴타입 중 undefiend가 있음. 이에 대한 처리는 조건문을 활용하여 아래와 같이 처리함
참고) const computerChoice: (imgCoords: ImgCoords) => "바위" | "가위" | "보" | undefined
// !를 활용하여 처리함
const computerChoice = (imgCoords: ImgCoords) => {
return (Object.keys(rspCoords) as keysImgCoords[]).find((k) => {
return rspCoords[k] === imgCoords;
})!
}
TypeScript
복사
사. useState, 고차함수 처리
1) useState 처리: 제네릭 처리에 신경
•
useState 타입의 경우, 초기값으로 제네릭으로 선언한 타입의 일부를 설정하면 useState의 State값은 초기값으로 입력된 제네릭의 일부만 타입으로 추론함
→ 이를 해결하기 위해 제네릭 타입 전체에 대해 useState에 제네릭으로 넘겨야 함
const rspCoords = {
바위: '0',
가위: '-142px',
보: '-284px'
} as const;
const [imgCoord, setImgCoord] = useState<ImgCoords>(rspCoords.바위);
// const [imgCoord, setImgCoord] = useState(rspCoords.바위);
if (imgCoord === rspCoords.바위) {
setImgCoord(rspCoords.가위);
TypeScript
복사
2) 고차함수 처리
•
JSX부분에 EventHandler로 넘기는 함수에 인자를 전달하고 싶은 경우 고차함수를 사용함.
참고) 일반적으로 다음과 같이 인자를 넘기지 않는 식으로 사용함 'onClick={onClickBtn}'
•
고차함수의 경우, 인자를 넘기기 때문에 함수 정의부분에서 해당 인자에 대한 타입을 명시해야 함
const onClickBtn = (choice: keysImgCoords) => () => {
clearInterval(interval.current);
const myScore = scores[choice];
}
return (
<>
<div id="computer" style={{ background: `url(https://en.pimg.jp/023/182/267/1/23182267.jpg) ${imgCoord} 0` }} />
<div>
<button id="rock" className="btn" onClick={onClickBtn('바위')}>바위</button>
</>
);
TypeScript
복사
아. useReducer
참고) 사용패턴 중심으로 useReduceer 쓰임새 정리
•
dispatch → action 발생 함수 또는 acion → reducer(action type에 따라 처리) → 상태 변화
1) initialState 관리
•
전체 State 중 useReducer로 관리한 State에 대해 초기 설정
•
interface로 각 타입에 대해 구체적으로 정의하지 않으면 string, number 등으로 타입을 포괄하여 설정됨
interface ReducerState {
winner: 'O' | 'X' | '',
turn: 'O' | 'X',
tableData: string[][],
recentCell: [number, number],
}
const initialState: ReducerState = {
winner: '',
turn: 'O',
tableData: [
['', '', ''],
['', '', ''],
['', '', ''],
],
recentCell: [-1, -1],
};
TypeScript
복사
2) Action 또는 Action 생성함수 정의
•
각 타입에 대해 as 활용하여 상수로 선언
•
Action 생성함수를 정의하는 기준은 reducer에서 상태변화 시, 특정 변수의 필요유무
export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL' as const;
export const CHANGE_TURN = 'CHANGE_TURN' as const;
export const RESET_GAME = 'RESET_GAME' as const;
interface SetWinnerAction {
type: typeof SET_WINNER;
winner: 'O' | 'X';
}
const setWinner = (winner: 'O' | 'X'): SetWinnerAction => {
return { type: SET_WINNER, winner };
};
interface ClickCellAction {
type: typeof CLICK_CELL;
row: number;
cell: number;
}
const clickCell = (row: number, cell: number): ClickCellAction => {
return { type: CLICK_CELL, row, cell };
};
interface ChangeTurnAction {
type: typeof CHANGE_TURN;
}
interface ResetGameAction {
type: typeof RESET_GAME;
}
TypeScript
복사
3) reducer 정의
•
action 타입이 많으므로 타입 별칭으로 정리 후 사용
•
action 발생 함수 사용하는 action의 경우, 발생 함수에서 사용한 변수를 action.~~로 reducer에서 사용함
type ReducerActions = SetWinnerAction | ClickCellAction | ChangeTurnAction | ResetGameAction;
const reducer = (state: ReducerState, action: ReducerActions): ReducerState => {
switch (action.type) {
case SET_WINNER:
// state.winner = action.winner; 이렇게 하면 안됨.
return {
...state,
winner: action.winner,
};
case CLICK_CELL: {
const tableData = [...state.tableData];
tableData[action.row] = [...tableData[action.row]]; // immer라는 라이브러리로 가독성 해결
tableData[action.row][action.cell] = state.turn;
return {
...state,
tableData,
recentCell: [action.row, action.cell],
};
}
case CHANGE_TURN: {
return {
...state,
turn: state.turn === 'O' ? 'X' : 'O',
};
}
case RESET_GAME: {
return {
...state,
turn: 'O',
tableData: [
['', '', ''],
['', '', ''],
['', '', ''],
],
recentCell: [-1, -1],
};
}
default:
return state;
}
};
TypeScript
복사
4. Typescript 기반 GraphQL Tutorial
가. 환경설정
1) boilerplate 활용
참고) 코드노마드의 Typescript 세팅법
가. 환경 설정
1) typescript 모듈 세팅: yarn init / yarn global add typescript
2) tsconfig.json 생성 및 정책 추가
{
"compilerOptions": {
// JS 파일에 대해 import, export 허용
"module": "commonjs",
// JS로 compile되는 문법
"target": "ES2015",
// TODO: source Map 이란?
"sourceMap": true
},
"include": ["index.ts"],
"exclude": ["node_modules"],
}
JSON
복사
3) package.json 내 script 설정
•
index.ts 실행 명령: yarn start
•
prestart 설정하여 yarn start 전에 해당 파일에 대한 tsc 명령 실행
{
//
"scripts": {
"start": "node index.js",
"prestart": "tsc"
}
}
JSON
복사
4) TSC watch 종속성 설치에 따른 설정 변경(yarn add tsc-watch --dev)
•
tsconfig.json
→ 모든 .ts 파일을 src 이하에서 관리하고
→ compile된 것은 dist 파일로 이동
{
//
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules"],
}
JSON
복사
•
package.json
→ yarn start 입력 시, tsc-watch 종속성에 의해 컴파일 성공 시에만 dist 폴도 이하 index.js를 실행
// 생략
"scripts": {
"start": "tsc-watch --onSuccess \" node dist/index.js\" "
},
"devDependencies": {
"tsc-watch": "^4.2.9"
}
}
JSON
복사
•
에러 해결
→ tsc-watch -onSuccess " node dist/index.js"
Cannot find module 'typescript/bin/tsc'
→ 위 에러에 대해 다음의 명령으로 해결: npm i -D @types/node typescript ts-node