본문 바로가기

원데이 프로젝트

TypeScript를 이용한 포켓몬 모음집 만들기

이 글은 

https://dev.to/ibrahima92/a-practical-guide-to-typescript-how-to-build-a-pokedex-app-using-html-css-and-typescript-4h7p?fbclid=IwAR0nvUvnUcUFRpcZ5GpK-6A1WCXRWdOTmzMsxJGlRbVt4tBo5CQnICYoBok

 

A Practical Guide to TypeScript - How to Build a Pokedex app using HTML, CSS, and TypeScript

TypeScript is a superset that needs to compile to plain JavaScript. It offers more control over your...

dev.to

를 실제로 만들어보면서 일부 번역 + 추가 작성한 글입니다. 타입스크립트를 간단하게 써보는 데 도움이 될 듯 합니다. 

 

결과물 사이트 : http://catched.me/ts-pokedex/

Github repo : https://github.com/chi-su/ts-pokedex

타입스크립트 설치하기

타입스크립트는 자바스크립트로의 컴파일이 필요합니다. 컴파일러는 아래 커맨드로 받을 수 있습니다. 

yarn add -g typescript

npm을 쓰신다면, 

npm install -g typescript

-g 플래그를 붙임으로써 타입스크립트를 글로벌하게 설치했고, 이는 어느 곳에서든 액세스할 수 있다는 점을 참고하세요. 

 

그럼 이제 설정 파일에 대해 간단히 알아봅시다.

설정파일을 꼭 추가할 필요는 없지만, 많은 경우 추가하는 것이 좋습니다.

파일을 만들어두면 컴파일러가 작동할 때 따르기 위한 규칙을 설정할 수 있어서 유용하거든요.

tsconfig로 타입스크립트 설정하기

tsconfig는 타입스크립트를 설정하는 데 도움을 주는 JSON 파일입니다. 이 설정 파일을 만들기 위해서, 여러분은 Pokedex라는 폴더를 만들고 그 폴더로 들어가야 합니다. 그리고, 터미널이나 IDE를 연 후(이번에는 VSC를 사용했습니다) 새로운 TS 설정 파일을 만들기 위해 아래 커맨드를 입력합니다. 

tsc --init

한번 파일이 생성되면, IDE에서 이를 확인할 수 있습니다. 

tsconfig.json

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "outDir": "public/js"
        "rootDir": "src",
        "strict": true,
        "esModuleInterop": true
        "forceConsistentCasingInFileNames": true
    },
    "include": ["src"]
}

하나씩 살펴보죠. 

  • target : 타입스크립트가 JS로 컴파일될때의 ECMAScript 타깃의 버전을 의미합니다. 여기서, 우리는 ES5를 타깃으로 잡았습니다(모든 브라우저를 지원하기 위해). 그리고 이 설정은 여러분이 원하는 대로 바꿀 수 있습니다. 
  • module : 컴파일된 코드의 모듈을 설정합니다. 모듈은 CommonJS, ES2015, ES2020 등이 설정될 수 있습니다. 
  • outdir : 컴파일된 코드가 출력되어 저장될 폴더위치를 명시합니다. 
  • rootDir : 컴파일될 타입스크립트 코드들의 위치를 명시합니다. 
  • include : 컴파일할 디렉토리를 명시합니다. 이 설정이 필요한 이유는, 이 값이 없을 경우 컴파일러가 프로젝트 내의 모든 ts파일을 컴파일하기 때문입니다. 

타입스크립트의 인터페이스

인터페이스와 타입 앨리아스는 객체와 같은 형태의 자료구조를 정의할 수 있게 도와줍니다. 

이 둘은 구조를 봤을 때 사뭇 비슷해 보이지만, 그 둘이 다르다는 사실을 명심하시기 바랍니다. 

 

그러나, 개발자들 사이에서는 interface를 사용하는 것이 일반적인데, 이는 tslint 규칙 모음에서 interface가 기본적으로 들어가 있기 때문입니다. 

 

이제, 인터페이스와 타입 앨리아스를 만들어 봅시다. 

interface ITest {
  id: number;
  name?: string;
}

type TestType = {
  id: number,
  name?: string,
}

function myTest(args: ITest): string {
  if (args.name) {
    return `Hello ${args.name}`
  }
  return "Hello Word"
}

myTest({ id: 1 })

보시다시피, 인터페이스와 타입 앨리아스의 구조는 자바스크립트의 객체와 비슷하게 보입니다. 이들은 타입스크립트에서 주어진 데이터의 형태를 정의해야 합니다. 

 

참고로, 저는 꼭 필요하지 않은 필드에는(name) 물음표를 붙였습니다. 이렇게 하면 해당 프로퍼티가 optional이 되는데, 이것은 값이 들어가지 않아도 따로 문제가 되지않고, 리턴할 경우 undefined가 값으로 리턴됩니다. 

 

이제, 우리는 ITest 인터페이스를 myTest라는 함수의 인자로 사용할 것입니다.

그리고 변수와 마찬가지로 함수도 특정 유형을 반환하도록 정의할 수 있습니다. 

여기서는,  리턴값은 반드시 문자열이어야 합니다(그렇지 않을 경우 타입스크립트가 에러를 냅니다). 

 

지금까지, 우리는 타입스크립트를 시작하기 위한 모든 기본지식을 다루었습니다. 이제, HTML과 CSS로 Pokedex를 만들어 봅시다. 

 

이 프로젝트는 Pokemon API에서 데이터를 가져오고, 각각의 가져온 포켓몬을 타입스크립트로 표시해줄 것입니다.

 

그럼, 3개의 파일을 만드는 것으로 프로젝트를 시작해봅시다. 

index.html / public/style.css / src/app.ts

또한 타입스크립트 설정을 위해 우리는 앞에서 만들어두었던 tsconfig.json 파일을 사용할 것입니다. 

마크업

  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="style.css" />
    <title>TypeScript Pokedex</title>
  </head>
  <body>
    <main>
      <h1>Typed Pokedex</h1>
      <div id="app"></div>
    </main>
    <script src="public/js/app.js"></script>
  </body>
</html>

보시다시피 우린 상대적으로 간단한 마크업을 사용합니다. 여기에는 두가지 중요한 부분이 있습니다. app이라는 id를 가진 div 태그는 타입스크립트로 불러온 데이터를 넣어줄 태그입니다. 그리고 script 태그는 컴파일 때 타입스크립트가 자바스크립트로 변환되어 들어갈 public 폴더를 가리키고 있습니다. 

CSS 

원본에서는 길이를 이유로 올리지는 않았지만 편의상 여기에 올려둡니다. 

@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&display=swap');

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background: #333;
    line-height: 1.5;
    font-family: 'Caveat', cursive;
}

main {
    padding: 1rem;
    max-width: 1100px;
    margin: auto;
}

main > h1 {
    text-align: center;
    color: #e4c439;
    margin-bottom: 2rem;
    font-size: 2rem;
    text-transform: uppercase;
}

#app {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
    grid-gap: 1.5rem;
    justify-content: center;
    align-items: center;
}

.card {
    width: 12rem auto;
    background: #444;
    color: #e4c439;
    padding: 1rem;
    border-radius: 10px;
    border-top: 0.5px solid #cebf7b;
    border-bottom: 0.5px solid #cebf7b;
    text-align: center;
    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
    position: relative;
    overflow: hidden;
    cursor: pointer;
    transition: 0.2s ease-in-out all;
}

.card:hover {
    transform: scale(1.05);
}

.card--id {
    background: #cebf7b;
    width: 3rem;
    color: #333;
    padding: 0.1rem;
    font-weight: 700;
    position: absolute;
    border-radius: 0 0 10px 0;
    top: 0;
    left: 0;
}
.card--name {
    text-transform: capitalize;
    color: #fff;
    font-size: 2rem;
    font-weight: 700;
}

.card--image {
    width: 150px;
    display: block;
    margin: auto;
}

.card--details {
    font-size: 1.3rem;
    color: #dbca80;
}

타입스크립트

id가 app인 div태그를 선택하는 것으로, TS 코드 작성을 시작해 봅시다. 

const container: HTMLElement | any = document.getElementById("app")
const pokemons: number = 100

interface IPokemon {
  id: number;
  name: string;
  image: string;
  type: string;
}

여기서 우리는 아직 다루지 않은 타입 표기를 사용하고 있습니다. (첫번째 줄) 

이것은 Union 타입이라는 건데, 주어진 변수에 대해 대안적인 타입을 가지는 것을 허락해줍니다. 

즉 컨테이너가 HTMLElement 타입이 아닐 경우 ,타입스크립트는 다시 한번 체크해서 만약 그 값이 파이프(|) 뒤의 타입과 맞는지 확인합니다. 이게 유니온 타입 안에서 우리가 여러 타입을 가질 수 있는 이유입니다. 

 

이제, 우리는 포켓몬 객체를 정의하는 IPokemon이라는 인터페이스르 가지고 있습니다. 이 인터페이스는 포켓몬들을 표시할 함수에서 사용될 것입니다. 

  • src/app.ts
const fetchData = (): void => {
  for (let i = 1; i <= pokemons; i++) {
    getPokemon(i)
  }
}

const getPokemon = async (id: number): Promise<void> => {
  const data: Response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
  const pokemon: any = await data.json()
  const pokemonType: string = pokemon.types
    .map((poke: any) => poke.type.name)
    .join(", ")

  const transformedPokemon = {
    id: pokemon.id,
    name: pokemon.name,
    image: `${pokemon.sprites.front_default}`,
    type: pokemonType,
  }

  showPokemon(transformedPokemon)
}

fetchData 함수는 설정한 포켓몬의 숫자만큼 getPokemon 함수를 호출합니다. 

 

데이터를 가져오는 것은 시간이 걸릴 수 있는 문제라, 우리는 void 타입의 프로미스를 리턴하는 비동기 함수를 사용할 겁니다. 즉, 이 함수는 따로 값을 리턴하지 않습니다. 

 

그리고 한번 데이터가 불러와지면, 우리는 transformedPokemon이라는 객체를 생성합니다. 이는 앞에서 정의한 인터페이스 IPokemon을 본뜬(mirror) 것입니다. 그리고 이 객체는 인자로서 showPokemon()함수에 넘겨지죠. 

 

  • src/app.ts
const showPokemon = (pokemon: IPokemon): void => {
  let output: string = `
        <div class="card">
            <span class="card--id">#${pokemon.id}</span>
            <img class="card--image" src=${pokemon.image} alt=${pokemon.name} />
            <h1 class="card--name">${pokemon.name}</h1>
            <span class="card--details">${pokemon.type}</span>
        </div>
    `
  container.innerHTML += output
}

fetchData()

보시다시피, showPokemon 함수는 인자로서 IPokemon 타입의 pokemon 객체를 받습니다. 그리고 void를 리턴하거나, 혹은 아무 값도 리턴되지 않습니다. 이 함수는 그저 container의 도움을 받아 HTML 파일에 데이터를 넘겨줄 뿐입니다.(기억하세요, container는 div 태그입니다. )

 

좋아요! 우리는 꽤 많이 했지만, 그러나 아직 빠진 부분이 있습니다. 그래서 지금 index.html 파일을 열어봐도 아무것도 보이지 않을 겁니다. 아직 타입스크립트를 자바스크립트로 컴파일하지 않았거든요. 이제 컴파일해봅시다. 

타입스크립트를 자바스크립트로 컴파일하기

이 튜토리얼 초반에, 우리는 TS를 JS로 컴파일하는 컴파일러를 설치했습니다.

컴파일을 위해, 터미널에서의 위치를 root 폴더로 옮긴 후 다음 커맨드를 입력합니다. 

tsc

이 커맨드는 .ts 확장자를 가진 모든 파일을 컴파일합니다. 그리고 우리는 tsconfig파일을 가지고 있죠. 컴파일러는 우리가 미리 정했던 규칙을 따를 것이고, 그래서 컴파일러는 오직 src 폴더의 TS 코드들만 JS 파일로 변환해 public 폴더에 넣어줄 겁니다. 

 

컴파일러는 이처럼 파일 하나에 대해서만 컴파일할 수도 있습니다. 

tsc myFile.ts

그리고 만약 여러분이 ts파일 뒤에 별도의 설정을 하지 않으면, (myFile.ts) 컴파일된 파일은 TS 파일과 같은 이름을 가지게 될 것입니다. 

 

그리고 만약 여러분의 코드가 바뀔 때마다 자동 컴파일을 하고싶다면, -w 플래그를 붙여주세요. 이는 컴파일러가 코드 변화를 지켜보다가 필요할 때 자동으로 여러분의 코드를 컴파일해줍니다. 

tsc -w

그리고 이제 index.html 파일을 보면, 귀여운 포켓몬들을 브라우저에서 보실 수 있습니다!!

아이 귀여워

+@ 

여기부터는 이 프로젝트를 진행하면서 겪었던 문제와 해결법을 간단히 공유합니다. 

프로젝트를 진행하다 보면 아래 에러들을 마주할 수도 있습니다. 

Cannot find name 'Promise'. Do you need to change your target library? 
Try changing the `lib` compiler option to es2015 or later

Cannot find name 'document'.
Cannot find name 'console'.

이 에러는 tsconfig.json 에서 아래와 같이 lib 속성을 추가해주면 해결됩니다. 

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": [
      "dom",
      "ES2019"
    ],
    "outDir": "public/js",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
  },
  "include": [
    "src"
  ],
}