본문 바로가기
개발자 도전기/[STUDY] JS || TS

TypeScript | 강의 메모 | CLI에서 동작하는 todo앱 프로젝트

by 답수 2022. 4. 13.
728x90
SMALL

 

 

※ 인프런 - 타입스크립트 시작하기(by 이재승) 강의를 기반으로 정리한 내용입니다.

 

 

프로젝트 환경 설정

강의 마지막은 간단한 실습 프로젝트! 커맨드라인에서 동작하는 todo앱을 만드는 것이다.

 

우선 https://github.com/landvibe/inflearn-react-project 에 들어가서 강사님 레포 클론 먼저 하기

 

그리고 이 프로젝트를 내 로컬에서 실행하기 위해서 npm install을 우선 실행한다.

 

package.json을 보자.

{
  "name": "todo-list",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon --watch '*.ts' --exec 'ts-node' src/index.ts",
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/node": "^14.6.0",
    "chalk": "^4.1.0",
    "nodemon": "^2.0.4",
    "ts-node": "^9.0.0",
    "typescript": "^4.0.2"
  }
}

 

dependencies에 보면 설치되어 있는 패키지들이 보인다. 

 

이 프로젝트는 노드 환경에서 실행되기 때문에 노드의 타입 정보가 있는 @types/node를 설치한다.

프로젝트의 start/src/Input.ts를 보면 다음과 같이 작성되어 있다.

import readline from 'readline';

const readlineInterface = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

export function waitForInput(msg: string) {
  return new Promise<string>(res =>
    readlineInterface.question(msg, key => {
      res(key);
    }),
  );
}

 

readline이라는 모듈은 노듈에 있는 모듈이기 때문에 이 패키지에 있는 타입 정보를 활용하게 된다.

 

chalk는 커맨드라인에서 출력되는 텍스트에 폰트 스타일을 적용하기 위해서 사용된다.

 

nodemon과 ts-node는 개발 모드에서 편하게 개발하기 위해 사용되는 패키지들이다.

package.json의 start를 보면 "nodemon --watch '*.ts' --exec 'ts-node' src/index.ts" 라고 되어 있다.

즉 노드를 nodemon을 통해 실행하고, 이때 watch모드로 실행한다. watch모드는 파일이 수정될 때마다 뒤에 있는 파일을 실행할 수 있도록 한다. ts파일의 확장자가 수정될 때마다 뒤에 있는 명령어를 실행한다. 그래서 코드를 수정할때마다 노드를 끄고 다시 시작하는 번거로움 없이 바로 확인 가능하다.

 

ts-node는 ts파일을 자바스크립트 파일로 컴파일하지 않고도 ts파일을 입력으로 받아서 바로 실행할 수 있도록 해준다.

 

노드 실행은 npm start를 입력하면 된다.

근데 에러 발생.

 

뭐가 문제인지 찾고 있는데 인프런 커뮤니티에 어느 귀인의 글...

혹시 npm run start시 ts-node 에러나시는 분들은
nodemon --watch *.ts --exec ts-node src/index.ts
이런식으로 따옴표를 제거해야지 제대로 작동합니다.
모르는 분들 계실까봐... 

즉 지금 나는 윈도우 운영체제에서 코드를 작성하고 있다보니 명령어가 조금은 달라질 수 있다는 것을 간과하면 안 된다.

 

start 내 명령어에 작은 따옴표 제거한 결과:

 

아주 잘된다! 귀인분 감사합니다..!!

 

 

암튼 본론으로 돌아와서, 패키지를 다시 보면 스크립트의 build에는 "tsc"가 보이는데, 이는 타입스크립트의 바이너리 파일인 tsc를 실행하겠다는 의미다. 

 

npm run build 명령어로 실행해보자.

 

빌드를 하면 dist라는 폴더가 생기고, 그 안에는 ts파일들을 컴파일한 js파일들이 생성된다. 

 

start 명령어는 개발할 때 사용하는 것이고, 실제 사용자들에게 전달할 때는 위처럼 js파일로 컴파일한 후, node dist/index.js 명령어로 실행을 해야 한다.

 

 

프로젝트 시작

현재 디렉토리의 구성은 다음과 같다(실제 코딩 짜는 부분만 표현)

.
.
├── start
├── dist
│   └── index.js
│   └── Input.js
│   └── util.js
├── node_modules
├── src
│   ├── index.ts
│   ├── Input.ts
│   ├── util.ts
├── package-lock.json
├── package.json
└── tsconfig.json

 

내가 구현해야 하는 부분은 src 폴더에 있는 파일들이고, 이 프로젝트는 위의 명령어를 설명할 때 말했던 것처럼 index.ts파일을 실행하면서 프로젝트가 시작된다.

 

먼저 index.ts파일은 function main() 함수를 통해 앱을 실행시킬 것이다. 함수 내 while루프를 통해 입력하는 명령어에 따라 앱이 계속 실행될 수 있도록 하고, Input.ts의 waitForInput() 함수를 사용하여 입력하는 key가 무엇인지 판단하게 할 것이다.

// src/index.ts

import { waitForInput } from "./Input";

async function main() {
    while (true) {
        // 처음 시작시 화면 클리어
        console.clear();

        const key = await waitForInput("input command: ");  // waitForInput이 프로미스 객체를 반환하기 때문에 async-await 사용
        /**
         * await는 프로미스 객체가 resolve나 reject가 될때까지 기다려줌
         * resolve가 됐을 때 값을 가져옴
         */
    }
}

main();

 

// src/Input.ts

import readline from 'readline';

const readlineInterface = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

export function waitForInput(msg: string) {
  return new Promise<string>(res =>
    readlineInterface.question(msg, key => {
      res(key);
    }),
  );
}

 

 

다음으로 main()함수에 앱의 상태를 정의하는 객체 state를 만든다.

// src/index.ts

import { waitForInput } from "./Input";
import { Appstate, Priority } from "./type";
import Todo from "./Todo";

async function main() {
    // 앱의 상태 정의
    const state: Appstate = {
        todos: [
            new Todo("test1", Priority.High),
            new Todo("test2", Priority.Medium),
            new Todo("test3", Priority.Low),
        ],
    }
    
    while (true) {
        // 처음 시작시 화면 클리어
        console.clear();

        const key = await waitForInput("input command: ");  // waitForInput이 프로미스 객체를 반환하기 때문에 async-await 사용
        /**
         * await는 프로미스 객체가 resolve나 reject가 될때까지 기다려줌
         * resolve가 됐을 때 값을 가져옴
         */
    }
}

main();

state의 타입은 인터페이스 Appstate로 만들고, Todo인스턴스의 타입은 enum을 이용하여 Priority로 만들고, todo리스트의 우선순위를 High, Medium, Low로 설정한다. 

 

타입을 설정하는 코드들은 type.ts라는 파일 내에 몰아둔다.

import Todo from "./Todo";

export enum Priority {
    High,
    Medium,
    Low
}

export interface Appstate {
    todos: Todo[];  // Todo 클래스의 배열
}

 

 

Todo인스턴스는 Todo클래스를 통해 생성한다.

// src/todo.ts

import { Priority } from "./type";

// Todo 클래스
export default class Todo {
    static nextId: number = 1;  // 객체별로 이 변수를 관리할 필요가 없기 때문에 static으로 정의
    constructor(
        private title: string, 
        private priority: Priority,
        public id: number = Todo.nextId,
    ) {
        Todo.nextId++;
    }
    toString() {
        return `${this.id}) 제목: ${this.title} (우선순위: ${this.priority})`;
    }
}

 

 

다음은 커맨드 클래스를 생성하고, 이 클래스를 통해 todo를 커맨드에 출력하는 파일을 만든다.

// src/command.ts

import { waitForInput } from "./Input";
import { Appstate } from "./type";

export abstract class Command {     // 추상클래스 Command
    constructor(
        public key: string,     // key: 키보드로 입력한 값
        private desc: string
    ) {}
    // 화면에 해당 커맨드가 하는 일이 뭔지 설명해주기 위한 메소드
    toString() {
        return `${this.key}: ${this.desc}`;
    }
    // 실행함수. 당장 구체화시키지 않을 것이기 때문에 abstract로 생성
    abstract run(state: Appstate): Promise<void>;
}

// 모든 todo 프린트하는 클래스
export class CommandPrintTodos extends Command {
    constructor() {
        super("p", "모든 할 일 출력하기");
    }

    async run(state: Appstate): Promise<void> {
        for (const todo of state.todos) {
            const text = todo.toString();   // 모든 todo 출력
            console.log(text);
        }
        await waitForInput("press any key: ");
    }
}

 

 

다시 index.ts로 돌아와서, todo리스트를 가져올 배열을 생성하고, 메인 함수에 리스트를 출력하는 코드를 작성한다.

import { Command, CommandPrintTodos } from "./Command";
import { waitForInput } from "./Input";
import Todo from "./Todo";
import { Appstate, Priority } from "./type";

// 커맨드 클래스 통해 todo리스트 가져올 배열 생성
const commands: Command[] = [new CommandPrintTodos()];

async function main() {
    // 앱의 상태 정의
    const state: Appstate = {
        todos: [
            new Todo("test1", Priority.High),
            new Todo("test2", Priority.Medium),
            new Todo("test3", Priority.Low),
        ],
    }

    while (true) {
        // 처음 시작시 화면 클리어
        console.clear();

        // todo에 있는 목록 가져오기
        for (const command of commands) {
            console.log(command.toString());
        }
        console.log();

        const key = await waitForInput("input command: ");  // waitForInput이 프로미스 객체를 반환하기 때문에 async-await 사용
        /**
         * await는 프로미스 객체가 resolve나 reject가 될때까지 기다려줌
         * resolve가 됐을 때 값을 가져옴
         */
        console.clear();

        // 입력한 값이 커맨드에 있다면
        const command = commands.find(v => v.key === key);
        if (command) {
            await command.run(state);
        }
    }
}

main();

 

 

npm start 입력하여 실행

 

p를 입력하면 state 객체 내에 있는 todo리스트들이 출력된다.

728x90
LIST

댓글