본문 바로가기
TYPESCRIPT

연습 프로젝트 1

by 일태찡 2023. 3. 30.

 

기본 HTML 파일은 다음과 같습니다.

 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>ProjectManager</title>
    <link rel="stylesheet" href="app.css" />
    <script src="dist/app.js" defer></script>
  </head>
  <body>
    <template id="project-input">
      <form>
        <div class="form-control">
          <label for="title">Title</label>
          <input type="text" id="title" />
        </div>
        <div class="form-control">
          <label for="description">Description</label>
          <textarea id="description" rows="3"></textarea>
        </div>
        <div class="form-control">
          <label for="people">People</label>
          <input type="number" id="people" step="1" min="0" max="10" />
        </div>
        <button type="submit">ADD PROJECT</button>
      </form>
    </template>
    <template id="single-project">
      <li></li>
    </template>
    <template id="project-list">
      <section class="projects">
        <header>
          <h2></h2>
        </header>
        <ul></ul>
      </section>
    </template>
    <div id="app"></div>
  </body>
</html>

 

 

DOM 요소 선택 및 OOP 렌더링

 

class ProjectInput {
  // 1
  templateElement: HTMLTemplateElement;
  hostElement: HTMLDivElement;
  element: HTMLFormElement;

  constructor() {
    this.templateElement = document.getElementById(
      "project-input"
  // 2  3
    )! as HTMLTemplateElement;
    this.hostElement = document.getElementById("app")! as HTMLDivElement;
    
    // 4
    const importedNode = document.importNode(
      this.templateElement.content,
      true
    );
    
    // 5
    this.element = importedNode.firstElementChild as HTMLFormElement;
    
    this.attach();
  }

  // 6
  private attach() {
    this.hostElement.insertAdjacentElement("afterbegin", this.element);
  }
}

const prjInput = new ProjectInput();

 

  1. 저장될 타입을 명확히 합니다.
  2. 느낌표를 붙여서 타입스크립트에게 모든 것이 괜찮고 그 ID로 엘리멘트에 접근할 것이라고 말합니다.
  3. getElementById는 어떤 엘리멘트가 마지막에 반환될지 모르기에 타입 캐스팅을 이용해 알려줍니다.
  4. 탬플릿 코드 사이의 html 코드를 참조하고 깊은 복사를 위해 true로 설정합니다.
  5. template 안에 있는 form을 지정합니다.
  6. hostElement 여는 태그가 시작되는 곳에(afterbegin) 삽입합니다.

 

 

 

DOM 요소와 상호 작용

 

class ProjectInput {
  templateElement: HTMLTemplateElement;
  hostElement: HTMLDivElement;
  element: HTMLFormElement;
  titleInputElement: HTMLInputElement;
  descriptionInputElement: HTMLInputElement;
  peopleInputElement: HTMLInputElement;

  constructor() {
    this.templateElement = document.getElementById(
      "project-input"
    )! as HTMLTemplateElement;
    this.hostElement = document.getElementById("app")! as HTMLDivElement;

    const importedNode = document.importNode(
      this.templateElement.content,
      true
    );
    this.element = importedNode.firstElementChild as HTMLFormElement;
    // 1
    this.element.id = "user-input";

    this.titleInputElement = this.element.querySelector(
      "#title"
    ) as HTMLInputElement;
    this.descriptionInputElement = this.element.querySelector(
      "#description"
    ) as HTMLInputElement;
    this.peopleInputElement = this.element.querySelector(
      "#people"
    ) as HTMLInputElement;

    this.configure();
    this.attach();
  }

  // 3
  private submitHandler(event: Event) {
    event.preventDefault();
    console.log(this.titleInputElement.value);
  }

  // 2
  private configure() {
    this.element.addEventListener("submit", this.submitHandler.bind(this));
  }

  private attach() {
    this.hostElement.insertAdjacentElement("afterbegin", this.element);
  }
}

const prjInput = new ProjectInput();

 

  1. element에 스타일링을 위해 ID를 연결합니다.
  2. bind를 통해 this가 submitHandler가 아닌 클래스를 참조하게 합니다.
  3. 유효성 검사를 위한 메서드이고 preventDefault를 호출하여 기본 양식 제출을 방지합니다.

 

 

 

Autobind 데코레이터 생성 및 사용하기

 

위 2번인 bind 이용을 데코레이터를 통해 구현해 봅니다.

 

// 1                  1-5
function autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value; // 1-1
  const adjDescriptor: PropertyDescriptor = { // 1-2
    configurable: true, // 1-3
    get() { // 1-4
      const boundFn = originalMethod.bind(this);
      return boundFn;
    },
  };
  return adjDescriptor;
}

class ProjectInput {
  templateElement: HTMLTemplateElement;
  hostElement: HTMLDivElement;
  element: HTMLFormElement;
  titleInputElement: HTMLInputElement;
  descriptionInputElement: HTMLInputElement;
  peopleInputElement: HTMLInputElement;

  constructor() {
    this.templateElement = document.getElementById(
      "project-input"
    )! as HTMLTemplateElement;
    this.hostElement = document.getElementById("app")! as HTMLDivElement;

    const importedNode = document.importNode(
      this.templateElement.content,
      true
    );
    this.element = importedNode.firstElementChild as HTMLFormElement;
    this.element.id = "user-input";

    this.titleInputElement = this.element.querySelector(
      "#title"
    ) as HTMLInputElement;
    this.descriptionInputElement = this.element.querySelector(
      "#description"
    ) as HTMLInputElement;
    this.peopleInputElement = this.element.querySelector(
      "#people"
    ) as HTMLInputElement;

    this.configure();
    this.attach();
  }
  
  // 2
  @autobind
  private submitHandler(event: Event) {
    event.preventDefault();
    console.log(this.titleInputElement.value);
  }

  private configure() {
    this.element.addEventListener("submit", this.submitHandler);
  }

  private attach() {
    this.hostElement.insertAdjacentElement("afterbegin", this.element);
  }
}

const prjInput = new ProjectInput();

 

  1. autobind 데코레이터 함수를 생성합니다.
    1. descriptor.value를 통해 원래 메서드에 접근합니다.
    2. adjDescriptor를 생성하여 PropertyDescriptor를 수정합니다.
    3. configurable을 true로 설정하여 언제든 수정할 수 있게 합니다.
    4. 게터 함수를 통해 바인드를 설정합니다.
    5. _를 이용하여 이 값들을 사용하지 않을 것을 알고 있지만 다음 인자가 필요하기에 이 값들을 수용하게 할 수 있습니다.
  2. 데코레이터를 연결합니다.

 

 

사용자 입력 가져오기

 

function autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const adjDescriptor: PropertyDescriptor = {
    configurable: true,
    get() {
      const boundFn = originalMethod.bind(this);
      return boundFn;
    },
  };
  return adjDescriptor;
}

class ProjectInput {
  templateElement: HTMLTemplateElement;
  hostElement: HTMLDivElement;
  element: HTMLFormElement;
  titleInputElement: HTMLInputElement;
  descriptionInputElement: HTMLInputElement;
  peopleInputElement: HTMLInputElement;

  constructor() {
    this.templateElement = document.getElementById(
      "project-input"
    )! as HTMLTemplateElement;
    this.hostElement = document.getElementById("app")! as HTMLDivElement;

    const importedNode = document.importNode(
      this.templateElement.content,
      true
    );
    this.element = importedNode.firstElementChild as HTMLFormElement;
    this.element.id = "user-input";

    this.titleInputElement = this.element.querySelector(
      "#title"
    ) as HTMLInputElement;
    this.descriptionInputElement = this.element.querySelector(
      "#description"
    ) as HTMLInputElement;
    this.peopleInputElement = this.element.querySelector(
      "#people"
    ) as HTMLInputElement;

    this.configure();
    this.attach();
  }

  // 1                                          2
  private gatherUserInput(): [string, string, number] | undefined {
    const enteredTitle = this.titleInputElement.value;
    const enteredDescription = this.descriptionInputElement.value;
    const enteredPeople = this.peopleInputElement.value;

    if (
      enteredTitle.trim().length === 0 ||
      enteredDescription.trim().length === 0 ||
      enteredPeople.trim().length === 0
    ) {
      alert("Invalid input, please try again!");
      return;
    } else {
      return [enteredTitle, enteredDescription, +enteredPeople]; // 3
    }
  }

  // 5
  private clearInputs() {
    this.titleInputElement.value = '';
    this.descriptionInputElement.value = '';
    this.peopleInputElement.value = '';
  }

  @autobind
  private submitHandler(event: Event) {
    event.preventDefault();
    // 4
    const userInput = this.gatherUserInput();
    if (Array.isArray(userInput)) {
        const [title, desc, people] = userInput;
        console.log(title, desc, people);
        this.clearInputs();
    }
  }

  private configure() {
    this.element.addEventListener("submit", this.submitHandler);
  }

  private attach() {
    this.hostElement.insertAdjacentElement("afterbegin", this.element);
  }
}

const prjInput = new ProjectInput();

 

  1. 입력 정보를 저장할 메서드를 생성합니다.
  2. 기본적으로 튜플 형태로 반환하나 유효성 검증을 통과하지 못할 경우 반환 값이 없기에 undefined를 유니언 타입으로 설정해 줍니다.
  3. 입력 요소에서 추출한 값은 사실상 디폴트가 텍스트이기 때문에 +를 붙여 숫자로 바꿔줍니다.
  4. 튜플은 기본적으로 자바스크립트에서 배열이기 때문에 userInput이 배열일 경우 즉, 유효성 검증을 통과한 경우로 조건을 설정합니다.
  5. 입력 시 입력 창을 초기화하는 메서드를 만들고 연결합니다.

 

 

 

재사용 가능한 검증 기능 생성

 

// 1
interface Validatable {
  value: string | number;
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  min?: number;
  max?: number;
}

function validate(validatableInput: Validatable) {
  let isValid = true;

  // 2
  if (validatableInput.required) {
    isValid = isValid && validatableInput.value.toString().trim().length !== 0;
  }

  // 3
  if (
    validatableInput.minLength != null &&
    typeof validatableInput.value === "string"
  ) {
    isValid =
      isValid && validatableInput.value.length >= validatableInput.minLength;
  }

  if (
    validatableInput.maxLength != null &&
    typeof validatableInput.value === "string"
  ) {
    isValid =
      isValid && validatableInput.value.length <= validatableInput.maxLength;
  }

  if (
    validatableInput.min != null &&
    typeof validatableInput.value === "number"
  ) {
    isValid == isValid && validatableInput.value >= validatableInput.min;
  }

  if (
    validatableInput.max != null &&
    typeof validatableInput.value === "number"
  ) {
    isValid == isValid && validatableInput.value <= validatableInput.max;
  }

  return isValid;
}

...

class ProjectInput {
  ...
  
  private gatherUserInput(): [string, string, number] | undefined {
    const enteredTitle = this.titleInputElement.value;
    const enteredDescription = this.descriptionInputElement.value;
    const enteredPeople = this.peopleInputElement.value;

    // 4
    const titleValidatable: Validatable = {
      value: enteredTitle,
      required: true,
    };

    const descriptionValidatable: Validatable = {
      value: enteredDescription,
      required: true,
      minLength: 5,
    };

    const peopleValidatable: Validatable = {
      value: +enteredPeople,
      required: true,
      min: 1,
      max: 5,
    };

    if (
      !validate(titleValidatable) ||
      !validate(descriptionValidatable) ||
      !validate(peopleValidatable)
    ) {
      alert("Invalid input, please try again!");
      return;
    } else {
      return [enteredTitle, enteredDescription, +enteredPeople];
    }
  }
...
}

 

재사용 가능한 검증을 만들기 위해 클래스 위에 다음과 같이 작성합니다.

 

  1. 객체의 기본 구조를 인터페이스를 통해 만듭니다.
  2. 필수 조건일 경우 길이가 0이면 false를 반환하게 합니다.
  3. 문자열일 경우 즉, 프로젝트에서 title과 description에서 최소 길이, 최대 길이를 설정할 수 있습니다. minLength가 0으로 설정되면 falsy 한 값이 되기에 위 설정에서 꼬일 수 있기에 != null을 추가해 null과 undefined과 아닐 경우를 추가합니다.
  4. 각 입력 데이터마다 검증 조건 객체를 설정합니다.

 

'TYPESCRIPT' 카테고리의 다른 글

연습 프로젝트 3  (6) 2023.04.03
연습 프로젝트 2  (13) 2023.03.31
데코레이터  (8) 2023.03.28
제네릭  (6) 2023.03.27
고급 타입  (6) 2023.03.24