TODO 타입스크립트 앱

김동현·2023년 7월 30일
0

개인 프로젝트

목록 보기
13/13

목적

타입스크립트 개발자의 수요가 점점 증가함에 따라 타입스크립트의 필요성을 느끼게 되었다.
이번에 "러닝 타입스크립트" 라는 책을 독파했다.
책을 보는 것만으로는 한참 부족하기 때문에 간단한 프로젝트를 통해 적응해보기로 했다.

지금와서 투두앱 개발은 나에게 너무 간단하다.
하지만 이 프로젝트의 목적은 어디까지나 자바스크립트 개발을 타입스크립트로 전환하면서 타입스크립트의 숙련도를 상승시키기 위함이다.
아니나 다를까, 이런 매우 간단한 프로젝트를 진행하면서도 타입스크립트로 개발하려고 하니 막히는 부분들이 종종 있었다.

사용한 기술

CSS BEM
Typescript
LocalStorage
Vite
Singleton pattern
Intl API

프로젝트 설명

이전에 자바스크립트로 투두앱을 만들었을때와 달리 이번에는 예전에 많이 사용된 MVC 패턴과 유사하게 로직을 구조화 해보았다.

  • model ( 모델 부분 )

    • FullList.ts
    • ListItem.ts
  • template ( 뷰 부분 )

    • ListTemplate.ts
  • main.ts ( 컨트롤러 부분 )

ListItem

ListItem은 하나의 할 일을 나타내는 모델이다.

내용을 의미하는 item: string , 체크 여부를 알려주는 checked: boolean , 각각의 일들을 구분해주는 id: string 을 가지고 있다.

interface Item {
  id: string;
  item: string;
  checked: boolean;
}

export default class ListItem implements Item {
  constructor(
    private _id: string = "",
    private _item: string,
    private _checked = false
  ) {}
  get id() {
    return this._id;
  }
  set id(_id: string) {
    this._id = _id;
  }
  get item() {
    return this._item;
  }
  set item(_item: string) {
    this._item = _item;
  }
  get checked() {
    return this._checked;
  }
  set checked(_checked: boolean) {
    this._checked = _checked;
  }
}

getter 과 setter 를 이용해서 유지보수에 유리하도록 설계했다.

FullList

FullList는 할 일들을 여러개 포함하고 있는 모델이다.
ListItem 배열을 가지고 있어야 한다.

또한 메서드들도 가지고 있는데, 이 메서드들은 localStorage를 편집하는 메서드들이다.

import ListItem from "./ListItem";

interface List {
  list: ListItem[];
  load(): void;
  save(): void;
  clear(): void;
  addItem(listItem: ListItem): void;
  removeItem(id: string): void;
}

export default class FullList implements List {
  static instance: FullList = new FullList();
  private constructor(private _list: ListItem[] = []) {}
  get list() {
    return this._list;
  }
  load(): void {
    const loadList: string | null = localStorage.getItem("myList");
    if (loadList === null) return;
    const parsedList: { _id: string; _item: string; _checked: boolean }[] =
      JSON.parse(loadList);

    parsedList.forEach((listItem) => {
      const newItem = new ListItem(
        listItem._id,
        listItem._item,
        listItem._checked
      );
      this._list.push(newItem);
    });
  }
  save(): void {
    localStorage.setItem("myList", JSON.stringify(this._list));
  }
  clear(): void {
    this._list = [];
    this.save();
  }
  addItem(listItem: ListItem): void {
    this._list.push(listItem);
    this.save();
  }
  removeItem(id: string): void {
    this._list = this._list.filter((listItem: ListItem) => listItem.id !== id);
    this.save();
  }
}

마찬가지로 getter 를 이용해서 유지보수에 유리하도록 설계했다.
setter는 사용하지 않을 것이라서 정의하지 않았다.

또한 생성자가 조금 이상하다는것을 확인할 수 있는데 생성자가 private이다.
이 클래스 밖에서는 이 클래스의 인스턴스를 생성할 수 없다.
static inatance에서 이 클래스의 생성자를 만드는 것을 볼 수 있다.
즉, 이 클래스는 싱글톤 패턴이다.

왜 싱글톤 패턴으로 생성했냐면, 이 클래스의 인스턴스는 여러개일 필요가 없기때문이다.
"하나의 일" 들은 여러개의 인스턴스로 생성되면 "여러개의 일" 이 되지만,
이미 "여러개의 일" 들은 하나의 인스턴스만 필요하기 때문이다.
심지어 이 클래스에서 localStorage와 다이랙트로 연결하는 것을 볼 수 있다.

원래 디비연결 인스턴스는 하나의 인스턴스로만 하는것이 국룰이다.

ListTemplate

화면을 랜더링해주는 클래스이다.
랜더링 하는 인스턴스도 하나면 충분하기 때문에 싱글톤으로 생성했다.
이 클래스는 랜더링을 위한 DOM object들을 속성으로 갖고 화면을 랜더링해주는 render() 메서드를 갖는다

interface DOMList {
  ul: HTMLUListElement;
  weekdayElement: HTMLHeadingElement;
  dateElement: HTMLSpanElement;
  tasksCountElement: HTMLElement;
  render(fullList: FullList): void;
}

export default class ListTemplate implements DOMList {
  ul: HTMLUListElement;
  weekdayElement: HTMLHeadingElement;
  dateElement: HTMLSpanElement;
  tasksCountElement: HTMLElement;
  static instance: ListTemplate = new ListTemplate();

  private constructor() {
    this.ul = document.querySelector(".list")!;
    this.weekdayElement = document.querySelector(".today-weekday")!;
    this.dateElement = document.querySelector(".today-date")!;
    this.tasksCountElement = document.querySelector(".tasks-count>em")!;
  }
  render(fullList: FullList) {
    // 오늘 날짜
    const [weekday, monthDay] = new Intl.DateTimeFormat("en-US", {
      weekday: "long",
      month: "long",
      day: "numeric",
    })
      .format(Date.now())
      .split(",")
      .map((item) => item.trim());
    this.weekdayElement.textContent = weekday;
    this.dateElement.textContent = monthDay;
    // 할 일 개수
    this.tasksCountElement.textContent = `${fullList.list.length}`;
    // 할 일 리스트
    this.ul.innerHTML = "";
    fullList.list.forEach((listItem) => {
      const liElement: HTMLLIElement = document.createElement("li");
      liElement.classList.add("list-item");

      const inputElement: HTMLInputElement = document.createElement("input");
      inputElement.classList.add("form__checkbox");
      inputElement.type = "checkbox";
      inputElement.id = listItem.id;
      inputElement.checked = listItem.checked;
      inputElement.addEventListener("click", () => {
        listItem.checked = !listItem.checked;
        fullList.save();
      });
      liElement.appendChild(inputElement);

      const labelElement: HTMLLabelElement = document.createElement("label");
      labelElement.classList.add("form__checkbox-label");
      labelElement.htmlFor = inputElement.id;
      labelElement.textContent = listItem.item;
      liElement.appendChild(labelElement);

      const buttonElement: HTMLButtonElement = document.createElement("button");
      buttonElement.classList.add("remove-task-btn");
      buttonElement.addEventListener("click", () => {
        fullList.removeItem(listItem.id);
        this.render(fullList);
      });
      buttonElement.textContent = "❌";
      liElement.appendChild(buttonElement);

      this.ul.appendChild(liElement);
    });
  }
}

화면을 랜더링 해주는 부분이 총 3군데가 있다.

이 부분들의 DOM element들을 속성으로 갖고 render메서드 호출할 때 업데이트를 한다.
날짜 정보는 Intl API를 이용해서 작성했다.

main

타입스크립트의 진입점이다.
첫 데이터를 로드하고 첫 랜더링을 한다.
또한 아래의 입력칸에 submit 이벤트 핸들러를 등록해서 데이터를 수정한다.

여기서는 따로 클래스를 만들지 않았다.

const fullList = FullList.instance;
const listTemplate = ListTemplate.instance;

fullList.load();
listTemplate.render(fullList);

const formElement: HTMLFormElement = document.querySelector(".input-box")!;
formElement.addEventListener("submit", (event) => {
  event.preventDefault();
  const id = String(Date.now());

  const inputTask: HTMLInputElement = document.querySelector(".input-task")!;
  const item = inputTask.value.trim();
  if (!item) return;

  const newItem: ListItem = new ListItem(id, item);
  fullList.addItem(newItem);
  listTemplate.render(fullList);
  inputTask.value = "";
  inputTask.focus();
});

HTML 코드를 보면 이 main.ts 파일을 로드하는 부분이 아래와 같이 설정되어 있다.

 <script type="module" src="/src/main.ts"></script>

모듈 타입으로 스크립트를 불러온다.
모듈 타입으로 불러올 땐 기본적으로 defer 어트리뷰트가 있는 일반 스트립트처럼 불러온다.
따라서 DOMContentLoaded 이벤트를 등록하지 않아도 된다.
많이들 실수하는 부분인것 같다. (해도 상관 없긴 하다.)

새로 배운 부분

forEach와 map의 차이점

코딩을 하다가 갑자기 생긴 의문이다.
배열의 forEach 메서드를 호출할 때 콜백함수의 매개변수를 수정하면 배열 자체에 적용이 될까?

const arr = [1, 2, 3];
arr.forEach(item =>{
	item = item + 1;
})
console.log(arr); // [1, 2, 3] 그대로 나옴

내부의 아이템 자체를 다른걸로 바꿔치기 하면 동작하지 않는다.
마치 HTTP method의 PUT과 같은 동작은 수행되지 않는다.

const item = {check : true};
const arr = [item];

arr.forEach(item=> {
  item.check = false;
});
console.log(arr); // [{check: false}] 바뀜

내부의 아이템의 부분만을 바꾸면 배열에 적용이 된다.
마치 HTTP method의 PATCH와 같은 동작은 수행된다.

forEach는 배열의 아이템 자체를 교체하진 못해도 아이템 내부의 값들은 변경 가능하다.
배열의 아이템 자체를 교체하려면 map을 쓰자.

const arr = [1, 2, 3];
const newArr = arr.map(item => item+1);
console.log(newArr);

input과 label에서의 클릭 이벤트

label을 클릭하면 input이 체크되는건 html상에서 일어나는 기본동작이다.
만약 label과 input 클릭에 따라 특정 행동을 하고싶다면
label에 클릭핸들러를 달고 input에 클릭핸들러를 달까?
아니다.
input에만 클릭 핸들러를 달면 된다.
label을 클릭하면 input도 자동으로 클릭이 되고 input에 연결된 클릭핸들러가 실행된다.
label에도 핸들러를 달면 두 번실행되게 된다.

CSS의 appearance

appearance CSS 속성은 운영 체제의 테마에 기반한 UI 컨트롤의 네이티브 외관을 제어하는 데 사용된다.
이 속성을 "none" 으로 설정함으로써 normalize 및 reset 작업을 수행할 수 있다.
사용해보니 input 이나 button과 같은 엘리먼트에 사용하는 것이 적절하다고 생각한다.

CSS의 will-change

CSS의 그래픽 가속작업과 같은 조금은(?) 힘겨운 작업할 때 이 속성으로 해야 할 작업들을 미리 알려주면 작업이 부드럽게 된다.
예를 들어 transform을 하기 전에 바로 윗부분에 will-change: transform 을 설정하면 부드럽게 동작하는 것을 볼 수 있다.

하지만 공식문서에서는 되도록 사용하지 말기를 권장하고 있다.
이미 브라우저는 자신만의 방식으로 최적화가 되어있기 때문에 will-change를 설정하면 브라우저 최적화를 깨뜨리는 행위이기 때문이다.

느낀점

여태 자바스크립트로만 프로젝트를 만들 때는 뭔가 주먹구구식이었는데 타입스크립트로 만드니 체계적인 느낌이다.
체계적이기 때문에 코딩할 때 더 효육적이고 빠르게 작업이 되어야 하는데 아직 그정도 수준이 되려면 멀었다.
이런 간단한 프로젝트조차 버벅되는 순간들이 꽤 있었기 때문이다.
다음엔 타입스크립트의 리액트 버전의 프로젝트를 만들어야겠다

결과물

profile
프론트에_가까운_풀스택_개발자

0개의 댓글