Jieunny의 블로그
Section10. 모듈 및 네임스페이스 본문
✔️ 모듈 형식의 코드를 작성해서 여러 개의 파일에 코드를 나눈다.
➰ 각 파일이 관리 가능하고 유지보수 가능하게 되는 것
➰ 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 |