🔗 레파지토리에서 보기
📌 개요
- 네임스페이스와 파일 번들링
- 네임스페이스 : 구문 기능을 제공할 수 있는 타입
- 기본적으로 네임스페이스 아래에 코드를 그룹으로 묶은 다음, 다른 파일에 네임스페이스를 임포트해서 파일마다 네임스페이스를 사용
- 파일 번들링 : 여러 파일을 하나의 파일로 묶어서 컴파일 처리한 여러 파일에 코드를 작성.
- 파일마다 임포트되어 관리해야 하는 임포트 개수가 줄어들어 HTML 파일 하나로 다양한 임포트를 수동 관리할 필요가 없다.
- ES6 imports/exports
- 모던 자바스크립트는 대규모 프로젝트를 묶는데에 솔루션을 가지고 있다.
- 대신, 스크립트 임포트 하나는 꼭 필요하다.
📌 네임스페이스
📖 네임스페이스 작업하기
💎 drag-drop-interfaces.ts
namespace App {
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;
}
}
- 네임스페이스에 클래스, 상수 등 원하는대로 넣을 수 있는 것은 맞다. 따라서 기본적으로 갖고있는 코드가 뭐든 네임스페이스를 씌울 수 있다.
export 키워드를 인터페이스 앞에 추가하여 네임스페이스에서 기능을 내보내도록 한다.
💎 app.ts
namespace App {
enum ProjectStatus {
Active,
Finished,
}
class Project {
constructor(
public id: string,
public title: string,
public description: string,
public people: number,
public status: ProjectStatus
) {}
}
type Listener<T> = (items: T[]) => void;
class State<T> {
protected listeners: Listener<T>[] = [];
addListener(listenerFn: Listener<T>) {
this.listeners.push(listenerFn);
}
}
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.updateListerers();
}
moveProject(projectId: string, newState: ProjectStatus) {
const project = this.projects.find((prj) => prj.id === projectId);
if (project && project.status !== newState) {
project.status = newState;
this.updateListerers();
}
}
private updateListerers() {
for (const listenerFn of this.listeners) {
listenerFn(this.projects.slice());
}
}
}
const projectState = ProjectState.getInstance();
interface Validatable {
value: string | number;
required?: boolean;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
}
function validate(validatableInput: Validatable) {
let isValid = true;
if (validatableInput.required) {
isValid =
isValid && validatableInput.value.toString().trim().length !== 0;
}
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;
}
console.log(isValid, validatableInput);
return isValid;
}
function autobind(_: any, _2: string, desciptor: PropertyDescriptor) {
const originalMethod = desciptor.value;
const adjDescriptor: PropertyDescriptor = {
configurable: true,
get() {
const boundFn = originalMethod.bind(this);
return boundFn;
},
};
return adjDescriptor;
}
abstract class Component<T extends HTMLElement, U extends HTMLElement> {
templateElement: HTMLTemplateElement;
hostElement: T;
element: U;
constructor(
templateId: string,
hostId: string,
insertAtStart: boolean,
newElementId?: string
) {
this.templateElement = document.getElementById(
templateId
)! as HTMLTemplateElement;
this.hostElement = document.getElementById(hostId)! 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 configures?(): void;
abstract renderContent(): void;
}
class ProjectItem
extends Component<HTMLUListElement, HTMLLIElement>
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);
this.project = project;
this.configures();
this.renderContent();
}
@autobind
dragStartHandler(event: DragEvent) {
console.log(event);
event.dataTransfer!.setData("text/plain", this.project.id);
event.dataTransfer!.effectAllowed = "move";
}
dragEndHandler(_: DragEvent) {
console.log("DragEnd");
}
configures() {
this.element.addEventListener("dragstart", this.dragStartHandler);
this.element.addEventListener("dragend", this.dragEndHandler);
}
renderContent() {
this.element.querySelector("h2")!.textContent = this.project.title;
this.element.querySelector("h3")!.textContent =
this.persons + " assigned";
this.element.querySelector("p")!.textContent = this.project.description;
}
}
class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget
{
assignedProjects: Project[];
constructor(private type: "active" | "finished") {
super("project-list", "app", false, `${type}-projects`);
this.assignedProjects = [];
this.configures();
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
dragLeaveHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.remove("droppable");
}
@autobind
dropHandler(event: DragEvent) {
const prjId = event.dataTransfer?.getData("text/plain")!;
projectState.moveProject(
prjId,
this.type === "active" ? ProjectStatus.Active : ProjectStatus.Finished
);
}
configures() {
this.element.addEventListener("dragover", this.dragOverHandler);
this.element.addEventListener("dragleave", this.dragLeaveHandler);
this.element.addEventListener("drop", this.dropHandler);
projectState.addListener((projects: Project[]) => {
console.log(projects);
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
} else {
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);
}
}
}
class ProjectInput extends Component<HTMLDivElement, 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;
this.configures();
}
configures() {
this.element.addEventListener("submit", this.submitHandler);
}
renderContent() {}
private gatherUserInput(): [string, string, number] | undefined {
const enteredTitle = this.titleInputElement.value;
const enteredDesciption = this.descriptionInputElement.value;
const enteredPeople = this.peopleInputElement.value;
const titleValidatable: Validatable = {
value: enteredTitle,
required: true,
};
const desciptionValidatable: Validatable = {
value: enteredDesciption,
required: true,
minLength: 5,
};
const peopleValidatable: Validatable = {
value: +enteredPeople,
required: true,
min: 1,
max: 5,
};
if (
!validate(titleValidatable) ||
!validate(desciptionValidatable) ||
!validate(peopleValidatable)
) {
alert("데이터를 입력해 주세요.");
return;
} else {
return [enteredTitle, enteredDesciption, +enteredPeople];
}
}
@autobind
private submitHandler(event: Event) {
event.preventDefault();
const userInput = this.gatherUserInput();
if (Array.isArray(userInput)) {
const [title, desc, people] = userInput;
projectState.addProject(title, desc, people);
this.clearInput();
}
}
private clearInput() {
this.titleInputElement.value = "";
this.descriptionInputElement.value = "";
this.peopleInputElement.value = "";
}
}
new ProjectInput();
new ProjectList("active");
new ProjectList("finished");
}
/// 를 이용하여 네임스페이스를 import → App app.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
) {}
}
}
💎 tsconfig.json
outFile로 타입스크립트에게 네임스페이슬르 사슬처럼 이어주라고 명령 → 참조한 내용이 컴파일할 때 여러 개의 자바스크립트 파일이 아니라 단일 자바스크립트 파일로 컴파일이 된다.
// tsconfig.json
{
"compilerOptions": {
"module": "amd",
"outFile": "./dist/bundle.js"
}
}
💎 bundle.js 적용하기
- 기존에 dist 폴더 안에 있던 js 파일을 모두 삭제
tsc -w를 다시 실행함으로써 bundle.js 생성
- index.html의 script src를 bundle.js로 설정
📖 파일 및 폴더 정리하기
💎 app.ts
namespace App {
new ProjectInput();
new ProjectList("active");
new ProjectList("finished");
}
🔗 레파지토리에서 보기
📖 네임스페이스 임포트
💎 app.ts
namespace App {
new ProjectInput();
new ProjectList("active");
new ProjectList("finished");
}
namespace App {
export class ProjectInput extends Component<HTMLDivElement, 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;
this.configures();
}
configures() {
this.element.addEventListener("submit", this.submitHandler);
}
renderContent() {}
private gatherUserInput(): [string, string, number] | undefined {
const enteredTitle = this.titleInputElement.value;
const enteredDesciption = this.descriptionInputElement.value;
const enteredPeople = this.peopleInputElement.value;
const titleValidatable: Validatable = {
value: enteredTitle,
required: true,
};
const desciptionValidatable: Validatable = {
value: enteredDesciption,
required: true,
minLength: 5,
};
const peopleValidatable: Validatable = {
value: +enteredPeople,
required: true,
min: 1,
max: 5,
};
if (
!validate(titleValidatable) ||
!validate(desciptionValidatable) ||
!validate(peopleValidatable)
) {
alert("데이터를 입력해 주세요.");
return;
} else {
return [enteredTitle, enteredDesciption, +enteredPeople];
}
}
@autobind
private submitHandler(event: Event) {
event.preventDefault();
const userInput = this.gatherUserInput();
if (Array.isArray(userInput)) {
const [title, desc, people] = userInput;
projectState.addProject(title, desc, people);
this.clearInput();
}
}
private clearInput() {
this.titleInputElement.value = "";
this.descriptionInputElement.value = "";
this.peopleInputElement.value = "";
}
}
}
💎 components/project-item.ts
namespace App {
export class ProjectItem
extends Component<HTMLUListElement, HTMLLIElement>
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);
this.project = project;
this.configures();
this.renderContent();
}
@autobind
dragStartHandler(event: DragEvent) {
console.log(event);
event.dataTransfer!.setData("text/plain", this.project.id);
event.dataTransfer!.effectAllowed = "move";
}
dragEndHandler(_: DragEvent) {
console.log("DragEnd");
}
configures() {
this.element.addEventListener("dragstart", this.dragStartHandler);
this.element.addEventListener("dragend", this.dragEndHandler);
}
renderContent() {
this.element.querySelector("h2")!.textContent = this.project.title;
this.element.querySelector("h3")!.textContent =
this.persons + " assigned";
this.element.querySelector("p")!.textContent = this.project.description;
}
}
}
💎 components/project-list.ts
namespace App {
export class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget
{
assignedProjects: Project[];
constructor(private type: "active" | "finished") {
super("project-list", "app", false, `${type}-projects`);
this.assignedProjects = [];
this.configures();
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
dragLeaveHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.remove("droppable");
}
@autobind
dropHandler(event: DragEvent) {
const prjId = event.dataTransfer?.getData("text/plain")!;
projectState.moveProject(
prjId,
this.type === "active" ? ProjectStatus.Active : ProjectStatus.Finished
);
}
configures() {
this.element.addEventListener("dragover", this.dragOverHandler);
this.element.addEventListener("dragleave", this.dragLeaveHandler);
this.element.addEventListener("drop", this.dropHandler);
projectState.addListener((projects: Project[]) => {
console.log(projects);
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
} else {
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);
}
}
}
}
📌 ES 모듈 사용하기
📖 ES 모듈 사용하기
💎 app.ts
import { ProjectInput } from "./components/project-input.js";
import { ProjectList } from "./components/project-list.js";
new ProjectInput();
new ProjectList("active");
new ProjectList("finished");
💎 tsconfig.json
{
"compilerOptions": {
"module": "ES2015"
// "outFile": "./dist/bundle.js"
}
}
💎 index.html
<script type="module" src="09_WritingModularCode/dist/app.js"></script>
🔗 레파지토리에서 보기
📖 다양한 import, export 구문 이해하기
💎 components/base-component.ts
export default abstract class Component<
T extends HTMLElement,
U extends HTMLElement
> {
templateElement: HTMLTemplateElement;
hostElement: T;
element: U;
constructor(
templateId: string,
hostId: string,
insertAtStart: boolean,
newElementId?: string
) {
this.templateElement = document.getElementById(
templateId
)! as HTMLTemplateElement;
this.hostElement = document.getElementById(hostId)! 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 configures?(): void;
abstract renderContent(): void;
}
import Component from "./base-component.js";
import * as Validation from "../util/validation.js";
import { autobind as Autobind } from "../decorators/autobind.js";
import { projectState } from "../state/project-state.js";
export class ProjectInput extends Component<HTMLDivElement, 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;
this.configures();
}
configures() {
this.element.addEventListener("submit", this.submitHandler);
}
renderContent() {}
private gatherUserInput(): [string, string, number] | undefined {
const enteredTitle = this.titleInputElement.value;
const enteredDesciption = this.descriptionInputElement.value;
const enteredPeople = this.peopleInputElement.value;
const titleValidatable: Validation.Validatable = {
value: enteredTitle,
required: true,
};
const desciptionValidatable: Validation.Validatable = {
value: enteredDesciption,
required: true,
minLength: 5,
};
const peopleValidatable: Validation.Validatable = {
value: +enteredPeople,
required: true,
min: 1,
max: 5,
};
if (
!Validation.validate(titleValidatable) ||
!Validation.validate(desciptionValidatable) ||
!Validation.validate(peopleValidatable)
) {
alert("데이터를 입력해 주세요.");
return;
} else {
return [enteredTitle, enteredDesciption, +enteredPeople];
}
}
@Autobind
private submitHandler(event: Event) {
event.preventDefault();
const userInput = this.gatherUserInput();
if (Array.isArray(userInput)) {
const [title, desc, people] = userInput;
projectState.addProject(title, desc, people);
this.clearInput();
}
}
private clearInput() {
this.titleInputElement.value = "";
this.descriptionInputElement.value = "";
this.peopleInputElement.value = "";
}
}
📖 모듈의 코드 실행
- 다른 파일에 해당 파일이 최초로 임포트될 때 한 차례만 실행된다.
- 다른 파일이 동일한 파일을 또 임포트해도 다시 실행되지 않는다.
🔗 자바스크립트 모듈 개요
🔗 MDN | 자바스크립트 모듈