Jieunny의 블로그

Section10. 모듈 및 네임스페이스 본문

CodeStates/TS 스터디

Section10. 모듈 및 네임스페이스

Jieunny 2023. 3. 23. 11:47

✔️ 모듈 형식의 코드를 작성해서 여러 개의 파일에 코드를 나눈다.

➰ 각 파일이 관리 가능하고 유지보수 가능하게 되는 것

➰ import, export로 간단하게 참조 가능

 

🚨 Chrome 또는 Firefox를 사용하자.

 

𝟭. 모듈 코드 작성하기

1️⃣ 여러 개의 코드 파일, 타입스크립트 파일 작성 후 수동으로 컴파일 된 js 파일을 HTML에 import 하는 것

2️⃣ Namespaces & File Bundling

3️⃣ ES6 Imports / Exports -> 권장

 

𝟮. 네임스페이스 작업하기

1️⃣ drag-drop-interface.ts 만들기

2️⃣ Draggable, DragTarget 인터페이스 잘라서 붙여넣기

// drag-drop-interface.ts

namespace App {
  // Drag & Drop interface
  // 한 박스에서 다른 박스로 프로젝트 항목 옮기기
  export interface Draggable {
    // 드래그에 관한 이벤트 리스너를 가지고 있음.
      dragStartHandler(event: DragEvent): void;
      dragEndHandler(event: DragEvent): void;
  }

  export interface DragTarget {
    // 드래깅 가능한 인터페이스를 드래깅 가능한 요소를 렌더링 하는 모든 클래스에 더할 수 있다.
    dragOverHandler(event: DragEvent): void;
    // 드래그 앤 드롭과 자바스크립트를 실행할 때 실행
    // 브라우저와 자바스크립트에 하고자 하는 드래그가 유효한 타겟임을 알려주기 위함.
    
    dropHandler(event: DragEvent): void;
    // 실제 일어나는 드롭에 대응한다.

    dragLeaveHandler(event: DragEvent): void;
    // 사용자에게 비주얼 피드백을 준다.

  }  
}
// app.ts

/// <reference path="drag-drop-interface.ts" />
// 특수 코멘트 - 타입스크립트가 이해하고 채택함.

name space App {
	// 모든 코드
}

 

3️⃣ project-model.ts 만들기

// project-model.ts

namespace App {
  export enum ProjectStatus {
    Active,
    Finished
  }
  
  export class Project {
    // 항상 동일한 구조를 갖는 프로젝트 객체를 만들 수 있다.
    constructor(
      public id: string,
      public title: string,
      public description: string,
      public people: number, 
      public status: ProjectStatus
    ) {}
  }
}
// app.ts

/// <reference path="drag-drop-interface.ts" />
// 특수 코멘트 - 타입스크립트가 이해하고 채택함.
/// <reference path="project-model.ts" />

name space App {
	// 모든 코드
}
//tsconfig.json
"outFile" : "./dist/bundle.js", // 활성화하기
"module" : "amd" // commonjs -> amd로 수정

➰ outFile : 타입스크립트가 네임스페이스와 연결되도록 한다.

    ㄴ 여러 개의 자바스크립트 파일을 컴파일 하는 대신에 컴파일 중에 연결되는 참조들을 하나의 자바스크립트 파일로 연결한다.

 

𝟯. 파일 및 폴더 정리하기

1️⃣ project-state.ts파일 생성

namespace App {
    // Project State Management
    type Listener<T> = (items: T[]) => void;
    // 리스너는 함수!
    
    class State<T> {
      protected listeners: Listener<T>[] = [];
    
      addListener(listenerFn: Listener<T>) {
        this.listeners.push(listenerFn);
      }
    }
    
    export class ProjectState extends State<Project>{
       //앱 상태를 관리하는 클래스
      // 관리 대상이 되는 상태를 관리한다
      private projects: Project[] = [];
      private static instance: ProjectState;
    
      private constructor() {
        super();
      }
    
      static getInstance() {
        // 싱글톤 사용
        if (this.instance) {
          return this.instance;
        }
        this.instance = new ProjectState();
        return this.instance;
      }
    
      // 다수의 프로젝트를 갖게 됨.
      addProject(title: string, description: string, numOfPeople: number) {
        const newProject = new Project(
          Math.random().toString(),
          title, 
          description, 
          numOfPeople,
          ProjectStatus.Active
          );
        // 저장하고자 하는 프로젝트
        this.projects.push(newProject);
        // 프로젝트 목록 배열에 새 프로젝트 추가
        this.updateListeners();
        
      }
      moveProject(projectId: string, newStatus: ProjectStatus) {
        // 프로젝트 상태 변경(현재 있는 목록에서 다른 목록으로 옮기기)
        const project = this.projects.find(prj => prj.id === projectId);
        if (project && project.status !== newStatus) {
          // 프로젝트의 새 상태가 원래 상태와 같으면 리렌더링 하지 않음.
          project.status = newStatus;
          // 상태를 새로운 상태로 변경
          this.updateListeners();
        }
      }
    
      private updateListeners() {
        for(const listenerFn of this.listeners) {
          // 새로운 프로젝트를 추가할 때 뭔가 변화가 있을 때마다 모든 리스너함수를 호출
          listenerFn(this.projects.slice());
        }
      }
    }
    
    export const projectState = ProjectState.getInstance();
    // 전역 상수
    // 검증 가능한 객체 구조 정의
    // 하나의 인스턴스만 생성하기 때문에 정확히 동일한 객체로 항상 작업할 수 있다.
    
}
  
    static getInstance() {
      // 싱글톤 사용
      if (this.instance) {
        return this.instance;
      }
      this.instance = new ProjectState();
      return this.instance;
    }
  
    // 다수의 프로젝트를 갖게 됨.
    addProject(title: string, description: string, numOfPeople: number) {
      const newProject = new Project(
        Math.random().toString(),
        title, 
        description, 
        numOfPeople,
        ProjectStatus.Active
        );
      // 저장하고자 하는 프로젝트
      this.projects.push(newProject);
      // 프로젝트 목록 배열에 새 프로젝트 추가
      this.updateListeners();
      
    }
    moveProject(projectId: string, newStatus: ProjectStatus) {
      // 프로젝트 상태 변경(현재 있는 목록에서 다른 목록으로 옮기기)
      const project = this.projects.find(prj => prj.id === projectId);
      if (project && project.status !== newStatus) {
        // 프로젝트의 새 상태가 원래 상태와 같으면 리렌더링 하지 않음.
        project.status = newStatus;
        // 상태를 새로운 상태로 변경
        this.updateListeners();
      }
    }
  
    private updateListeners() {
      for(const listenerFn of this.listeners) {
        // 새로운 프로젝트를 추가할 때 뭔가 변화가 있을 때마다 모든 리스너함수를 호출
        listenerFn(this.projects.slice());
      }
    }
  }
  
  const projectState = ProjectState.getInstance();
  // 전역 상수
  // 검증 가능한 객체 구조 정의
  // 하나의 인스턴스만 생성하기 때문에 정확히 동일한 객체로 항상 작업할 수 있다.

 

2️⃣ validatoin.ts 파일 생성

// validatoin.ts

namespace App {
  // Validation
  export interface Validatable {
    value: string | number;
    required?: boolean;
    // 공란으로 둬도 되는지 안되는지 확인
    minLength?: number;
    maxLength?: number;
    // 문자열의 길이 확인
    min?: number;
    max?: number;
    // 수치 값이 특정 숫자 이상인지,특정 최댓값 이하인지 확인(number일 때)
    // 수치 외에는 모두 선택사항 이므로 물음표 추가
  }
  // 검증 가능한 객체 구조 정의
   
  export function validate(validatableInput: Validatable){
    let isValid = true;
    // 처음엔 true로 설정하고 하나라도 유효하지 않으면 false로 바꾸기
    if(validatableInput.required) {
      // 꼭 채워야 하는 칸이 비어있는지 확인
        isValid = isValid && validatableInput.value.toString().trim().length !== 0;
        // 들어온 게 문자열이어야만 trim() 메서드 사용가능
        // 비어있으면 isValid가 false가 된다.
    }
    if(validatableInput.minLength != null && typeof validatableInput.value === 'string'){
      // 들어온 값이 문자열일 때 최소 길이를 가지고 있는지 확인
      // 왜 validatableInput.minLength 이게 참인지 여부확인을 안하고 null인지 확인하냐? -> 값이 0이면 문제가 생기기 때문에
      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;
  }
}

 

3️⃣ autobind-decorator.ts 파일 생성

// autobind-decorator.js

namespace App {
   // autobind decorator
   export 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;
  }
  // app.js:9 Uncaught TypeError: Cannot read properties of undefined (reading 'value')
  // at autobind
  // 왜 에러나지..ㅠ
}

 

4️⃣ 파일 구조 정리하고 import 경로 정리하기

src

    ㄴ components

        ㄴ base-component.ts

        ㄴ project-input.ts

        ㄴ project-item.ts

        ㄴ project-list.ts

    ㄴ decorators

        ㄴ autobind.ts

    ㄴ models

        ㄴ drag-drop.ts

        ㄴ project.ts

    ㄴ state

        ㄴ project-state.ts

 ㄴ util

        ㄴ validation.ts

 

5️⃣ base-component.ts 파일 생성하기

namespace App {
  export abstract class Component<T extends HTMLElement, U extends HTMLElement > {
    // 추상화로 만들어서 인스턴스화 할 수 없게 한다.
    templateElement: HTMLTemplateElement;
    hostElement: T;
    element: U;
  
    constructor(templateId: string, hostElementId: string, insertAtStart: boolean, newElementId?: string) {
      //id를 알아야 그것을 어떻게 선택할 지 알 수 있음
      //newElementId 는 새롭게 렌더링 된 요소에 할당된다.
      this.templateElement = document.getElementById(templateId)! as HTMLTemplateElement;
      // 템플릿에 대한 접근성을 제공 
      this.hostElement = document.getElementById(hostElementId)! as T;
  
      const importedNode = document.importNode(this.templateElement.content, true);
      this.element = importedNode.firstElementChild as U;  
      if(newElementId) {
        this.element.id = newElementId;
      }
      this.attach(insertAtStart);
    }
  
    private attach(insertAtBeginning: boolean) {
      this.hostElement.insertAdjacentElement(insertAtBeginning ? 'afterbegin' : 'beforeend', this.element);
      // 어디다 새로운 노드를 추가할 지 정해준다.
    }
  
    abstract configure?(): void;
    abstract renderContent(): void;
    // 이 컴포넌트를 상속받는 모든 클래스에 두 메소드를 추가해서 구현할 수 있다.
  }
}

 

6️⃣ project.item.ts 파일 생성하기

// project-item.ts
/// <reference path="base-component.ts" />

namespace App {
  // 프로젝트 항목 클래스를 인스턴스화 하기
  // ProjectItem Class
  export class ProjectItem extends Component<HTMLUListElement, HTMLElement> implements Draggable {
    // 프로젝트 클래스의 이 프로젝트 항목에 속하는 프로젝트 저장하기
    private project: Project;
  
    get persons() {
      // 사람 수에 맞에 뒤에 문장 바꿔주기 
      if(this.project.people === 1){
        return '1 person';
      } else {
        return `${this.project.people} persons`;
      }
    }
    
    constructor(hostId: string, project: Project) {
      super('single-project', hostId, false, project.id);
      // super에 프로젝트 항목이 렌더링될 요소 id가 어디에 있는지 알려주어야 한다.
      this.project = project;
  
      this.configure();
      this.renderContent();
    }
  
    @autobind
    dragStartHandler(event: DragEvent) {
      event.dataTransfer!.setData('text/plain', this.project.id);
      // 모든 드래그 관련 이벤트가 데이터 전송 객체가 있는 이벤트 객체를 만들어내지는 않으므로 !추가 해준다.
      event.dataTransfer!.effectAllowed = 'move';
      // 드래그 항목을 원래 위치에서 지우고 새로운 위치에 등록한다는 의미.
    }
  
    dragEndHandler(_: DragEvent) {
      console.log('dg')
    }
    // 드래깅 가능한 클래스를 추가하면 항상 위의 메소드를 얻게 된다.
  
    configure() {
      this.element.addEventListener('dragstart', this.dragStartHandler);
      this.element.addEventListener('dragend', this.dragEndHandler);
    }
  
    renderContent() {
      // li요소 렌더링
      this.element.querySelector('h2')!.textContent = this.project.title;
      this.element.querySelector('h3')!.textContent = this.persons + ' assigned';
      this.element.querySelector('p')!.textContent = this.project.description;
    }
  }
}

 

7️⃣ project-list.ts 파일 생성하기

// project-list.ts
/// <reference path="base-component.ts" />

namespace App {
  // ProjectList Class
  class ProjectList extends Component<HTMLDivElement, HTMLElement> implements DragTarget {
    // 프로젝트 리스트 만들 class
    assignedProjects: Project[];
    
    constructor(private type: 'active' | 'finished')  {
      super('project-list', 'app', false, `${type}-projects`);
      // ????????????
      // 앞이 아니라 뒤애 넣어야 하므로 false 전달
      this.assignedProjects = [];
      
      this.configure();
      this.renderContent();
      // 생성
    }
  
    @autobind
    dragOverHandler(event: DragEvent) {
    // 박스의 모습이나 정리되지 않은 목록을 바꾼다.
      if(event.dataTransfer && event.dataTransfer.types[0] === 'text/plain'){
        event.preventDefault();
        // 자바스크립트의 드래그 앤 드롭 이벤트의 디폴트는 드로핑을 허용하지 않으므로 디폴트를 막아줘야한다.
        const listEl = this.element.querySelector('ul')!;
        listEl.classList.add('droppable');
        // 드롭 하려고 클릭해서 움직이면 클래스 추가하기.
      }
    }
  
    @autobind
    dropHandler(event: DragEvent) {
      const prjId = event.dataTransfer!.getData('text/plain');
      projectState.moveProject(
        prjId,
        this.type === 'active' ? ProjectStatus.Active : ProjectStatus.Finished
      );
    }
  
    @autobind
    dragLeaveHandler(_: DragEvent) {
      const listEl = this.element.querySelector('ul')!;
      listEl.classList.remove('droppable');
    }
  
    configure() {
      this.element.addEventListener('dragover', this.dragOverHandler);
      this.element.addEventListener('dragleave', this.dragLeaveHandler);
      this.element.addEventListener('drop', this.dropHandler);
      projectState.addListener((projects: Project[]) => {
        const relevantProjects = projects.filter(prj => {
          if (this.type === 'active') {
            return prj.status === ProjectStatus.Active;
          }
          return prj.status === ProjectStatus.Finished;
        });
        this.assignedProjects = relevantProjects;
        this.renderProjects();
      });
    }
  
    renderContent() {
      // 화면에 프로젝트 리스트 그려주는 함수
      const listId = `${this.type}-projects-list`;
      this.element.querySelector('ul')!.id = listId;
      this.element.querySelector('h2')!.textContent = this.type.toUpperCase() + ' PROJECTS';
    }
  
    private renderProjects() {
      // ????????
      const listEl = document.getElementById(`${this.type}-projects-list`)! as HTMLUListElement;
      listEl.innerHTML = ''; 
      // 모든 목록 항목을 없애고
      for (const prjItem of this.assignedProjects) {
        // 재생성한다 => 추가버튼 눌렀을 때 같은 목록이 두번 나오는 것 방지
        new ProjectItem(this.element.querySelector('ul')!.id, prjItem); 
        // 적절한 위치에 렌더링 해서 list-style 안나오게 하기
      }
    }
  }
}

 

8️⃣ project-input.ts 파일 생성하기

 

// project-input.ts
/// <reference path="base-component.ts" />

namespace App {
  // ProjectInput Class
  export class ProjectInput extends Component<HTMLDListElement, HTMLFormElement> {
    titleInputElement: HTMLInputElement;
    descriptionInputElement: HTMLInputElement;
    peopleInputElement: HTMLInputElement;
  
    constructor() {
      super('project-input', 'app', true, '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;
      // DOM으로 요소 불러오기
      this.configure();
    }
  
    configure() {
      // 이벤트 리스너 설정
  
      this.element.addEventListener('submit', this.submitHandler);
      // (2) submitHendler 함수를 호출할 때 bind를 줘서 this가 클래스를 바인딩 하게 한다.
      // this.element.addEventListener('submit', this.submitHandler.bind(this));
      // 여기서는 데코레이터를 이용해서 bind를 구현.
    } 
  
    renderContent(){
    }
  
    private gatherUserInput(): [string, string, number] | void {
      const enteredTitle = this.titleInputElement.value;
      const enteredDescription = this.descriptionInputElement.value;
      const enteredPeople = this.peopleInputElement.value;
      // value로 불러온건 기본적으로 text로 인식된다(Number가 아니라는 뜻)
  
      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(
        // enteredTitle.trim().length === 0 || 
        // enteredDescription.trim().length === 0 ||
        // enteredPeople.trim().length === 0
        // trim()으로 공백 없애기
        !validate(titleValidatable) ||
        !validate(descriptionValidatable) ||
        !validate(peopleValidatable)
        // input 값들이 유효한지 확인하는 validate 함수 사용 
        // 하나라도 false를 반환하면 alert를 띄운다.
      ){
        alert('Invalid input, please try again!');
        return; // 여기서 반환되는 건 튜플이 아니라 정의되지 않은 것 
        // -> 위에 undefined 추가해줘야 하는데 부정형은 함수 반환타임으로 못 쓰니까 void로 추가
      } else {
        // 유효한 입력이 있는 경우
        return [enteredTitle, enteredDescription, +enteredPeople];
        // people은 number 형이므로 + 붙여서 숫자로 바꿔준다.
      }
    }
  
    private clearInputs() {
      this.titleInputElement.value = '';
      this.descriptionInputElement.value = '';
      this.peopleInputElement.value = '';
      // 제출 하고 나면 input칸 비우기
    }
  
    @autobind
    private submitHandler(event: Event) {
      event.preventDefault();
      const userInput = this.gatherUserInput();
       // (1) this 키워드가 submitHandler에서 저 클래스를 가리키지 않아서 값을 제대로 불러올 수 없다.
      // 현재 this는 이벤트 대상에 바인딩 되고 있다.   
      if(Array.isArray(userInput)){
        // userInput이 tuple인 것을 확인 할 수 없으므로(JS에 없음) tuple도 일단 배열이니까 배열인지 확인해준다.
        const [title, desc, people] = userInput;
        projectState.addProject(title, desc, people);
        // 누르면 새 프로젝트의 정보가 추가된다.
        this.clearInputs();
      }
    }
    // 꼭 있어야 하는 건 아니지만 기본적으로 별도 메서드에서 해주는 게 좋다.
  }
}

 

9️⃣ app.ts 파일에 import 하기

 

𝟰. 네임스페이스 가져오기 문제

✔️ app.ts에 모든 파일을 import 해오는 게 좋은 방법은 아니다.

➰ 필요한 파일에 필요한 것들을 import 해오는 식으로 사용하면 어떨까...

// project-input.ts

/// <reference path="base-component.ts" />
/// <reference path="../decorators/autobind.ts" />
/// <reference path="../util/validation.ts" />
/// <reference path="../state/project-state.ts" />
// project-list.ts

/// <reference path="base-component.ts" />
/// <reference path="../decorators/autobind.ts" />
/// <reference path="../state/project-state.ts" />
/// <reference path="../models/project.ts" />
/// <reference path="../models/drag-drop.ts" />

 

𝟱. ES 모듈 사용하기

✔️ 모든 파일에서 namespace를 지우고, ///로 참조해왔던 것도 지운다.

✔️ tsconfig.json 에서 module : "es2015" 로 수정

✔️ outFile도 다시 주석처리

✔️ index.html 에서 <script type="module" src="dist/app.js"></script> 로 수정

➰ 이미 컴파일 된 js 파일로 임포트하기
➰ 이러면 위에 ///로 가져왔던 거 다 삭제해도 된다.

// project-input.ts

import { Component } from './base-component.js';
import { Validatable, validate } from '../util/validation.js';
import { autobind } from '../decorators/autobind.js';
import { projectState } from '../state/project-state.js';
// project-item.js

import { Draggable } from '../models/drag-drop.js';
import { Project } from '../models/project.js';
import { Component } from './base-component.js';
import { autobind } from '../decorators/autobind.js';
// project-list.ts

import { DragTarget } from '../models/drag-drop.js';
import { Project, ProjectStatus } from '../models/project.js';
import { Component } from './base-component.js';
import { autobind } from '../decorators/autobind.js';
import { projectState } from '../state/project-state.js';
import { ProjectItem } from './project-item.js';

 

// project-state.ts

import { Project, ProjectStatus } from '../models/project.ts';
// app.ts

import { ProjectInput } from './components/project-input.js';
import { ProjectList } from './components/project-list.js';

 

𝟲. 다양한 가져오기 및 내보내기 구문 이해하기

1️⃣ 그룹화하기

import * as Validation from '../util/validation.js';

// 사용할 때
Validation.Validatable 이런 식으로 사용

➰ 그룹화된 파일에서 export되는 모든 것들에 . 표기법으로 접근할 수 있다.

 

2️⃣ Alias 사용

import { autobind as Autobind } from '../decorators/autobind.js';

➰ 이 파일 내에서만 다른 이름으로 import 된다.

➰ 이름 충돌을 방지할 수 있다.

 

3️⃣ export default 사용

export default ...

import Cmp from '../...';

➰ 파일 당 하나의 default export가 가능하며 중괄호 없이 import 할 수 있다.

 

𝟳. 모듈의 코드는 어떻게 실행되는가?

✔️ 한 파일을 여러 번 임포트 하면, 임포트 된 것은 얼마나 빈번하게 실행되는가?

➰ 처음으로 임포트 됬을 때 1회 실행된다.

➰ 다른 파일이 그 같은 파일을 다시 임포트 하는 경우 다시 실행되지 않는다.

'CodeStates > TS 스터디' 카테고리의 다른 글

Section14. React.js 및 TypeScript  (0) 2023.03.27
Section11. TS와 함께 Webpack 사용하기  (0) 2023.03.24
Section9. Drag & Drop 프로젝트 만들기  (2) 2023.03.20
Section8. 데코레이터  (0) 2023.03.03
Section 7. 제네릭  (0) 2023.02.22