Skip to main content

· 6 min read

https://docs.nestjs.com/modules

module

1. initialize 함수 (엔트리포인트)

Nest 앱을 부트스트랩하면서 의존성 그래프를 스캔하고, 인스턴스를 생성하는 메인 루틴

주요 컴포넌트:

  • Injector: 인스턴스 생성기 (DI 책임)
  • InstanceLoader: 프로토타입/인스턴스를 모듈 단위로 실제로 생성
  • DependenciesScanner: 메타데이터 스캔 (@Module, @Injectable, @Controller 등 분석)

흐름:

dependenciesScanner.scan(module) → 모듈/컨트롤러/프로바이더 메타데이터를 스캔 후 컨테이너에 등록

instanceLoader.createInstancesOfDependencies() → 프로토타입 생성 후 실제 인스턴스 생성

dependenciesScanner.applyApplicationProviders() → 글로벌 프로바이더 등 애플리케이션 레벨 적용

https://github.com/cheonkyu/nest/blob/master/packages/core/nest-factory.ts#L201-L248

private async initialize(
module: any,
container: NestContainer,
graphInspector: GraphInspector,
config = new ApplicationConfig(),
options: NestApplicationContextOptions = {},
httpServer: HttpServer | null = null,
) {
UuidFactory.mode = options.snapshot
? UuidFactoryMode.Deterministic
: UuidFactoryMode.Random;

const injector = new Injector({
preview: options.preview!,
instanceDecorator: options.instrument?.instanceDecorator,
});
const instanceLoader = new InstanceLoader(
container,
injector,
graphInspector,
);
const metadataScanner = new MetadataScanner();
const dependenciesScanner = new DependenciesScanner(
container,
metadataScanner,
graphInspector,
config,
);
container.setHttpAdapter(httpServer);

const teardown = this.abortOnError === false ? rethrow : undefined;
await httpServer?.init?.();
try {
this.logger.log(MESSAGES.APPLICATION_START);

await ExceptionsZone.asyncRun(
async () => {
await dependenciesScanner.scan(module);
await instanceLoader.createInstancesOfDependencies();
dependenciesScanner.applyApplicationProviders();
},
teardown,
this.autoFlushLogs,
);
} catch (e) {
this.handleInitializationError(e);
}
}

2. Injector (주입기)

Nest의 DI 컨테이너 핵심. 역할: loadPrototype: 프로토타입 객체 생성 후 collection(Map)에 저장 loadInstance: 의존성을 해석하고, 실제 클래스 인스턴스를 생성 즉, loadPrototype → “틀”만 먼저 만든다 (클래스의 빈 객체) loadInstance → “실제 인스턴스”를 만든다 (constructor 호출, DI 주입 등 처리)

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/injector.ts#L229-L243

  public async loadController(
wrapper: InstanceWrapper<Controller>,
moduleRef: Module,
contextId = STATIC_CONTEXT,
) {
const controllers = moduleRef.controllers;
await this.loadInstance<Controller>(
wrapper,
controllers,
moduleRef,
contextId,
wrapper,
);
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
}

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/injector.ts#L109-L126

public loadPrototype<T>(
{ token }: InstanceWrapper<T>,
collection: Map<InjectionToken, InstanceWrapper<T>>,
contextId = STATIC_CONTEXT,
) {
if (!collection) {
return;
}
const target = collection.get(token)!;
const instance = target.createPrototype(contextId);
if (instance) {
const wrapper = new InstanceWrapper({
...target,
instance,
});
collection.set(token, wrapper);
}
}

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/injector.ts#L128-L205

public async loadInstance<T>(
wrapper: InstanceWrapper<T>,
collection: Map<InjectionToken, InstanceWrapper>,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
const inquirerId = this.getInquirerId(inquirer);
const instanceHost = wrapper.getInstanceByContextId(
this.getContextId(contextId, wrapper),
inquirerId,
);

if (instanceHost.isPending) {
const settlementSignal = wrapper.settlementSignal;
if (inquirer && settlementSignal?.isCycle(inquirer.id)) {
throw new CircularDependencyException(`"${wrapper.name}"`);
}

return instanceHost.donePromise!.then((err?: unknown) => {
if (err) {
throw err;
}
});
}

const settlementSignal = this.applySettlementSignal(instanceHost, wrapper);
const token = wrapper.token || wrapper.name;

const { inject } = wrapper;
const targetWrapper = collection.get(token);
if (isUndefined(targetWrapper)) {
throw new RuntimeException();
}
if (instanceHost.isResolved) {
return settlementSignal.complete();
}
try {
const t0 = this.getNowTimestamp();
const callback = async (instances: unknown[]) => {
const properties = await this.resolveProperties(
wrapper,
moduleRef,
inject as InjectionToken[],
contextId,
wrapper,
inquirer,
);
const instance = await this.instantiateClass(
instances,
wrapper,
targetWrapper,
contextId,
inquirer,
);
this.applyProperties(instance, properties);
wrapper.initTime = this.getNowTimestamp() - t0;
settlementSignal.complete();
};
await this.resolveConstructorParams<T>(
wrapper,
moduleRef,
inject as InjectionToken[],
callback,
contextId,
wrapper,
inquirer,
);
} catch (err) {
wrapper.removeInstanceByContextId(
this.getContextId(contextId, wrapper),
inquirerId,
);

settlementSignal.error(err);
throw err;
}
}

3. InstanceLoader (인스턴스 로더)

Injector를 이용해 모듈 단위로 컨트롤러, 프로바이더, 인젝터블을 실제 인스턴스로 변환 단계: createPrototypes() → 미리 모든 Provider/Controller의 프로토타입 생성 createInstances() → 실제 인스턴스 생성 (constructor 호출 + DI 적용)

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/instance-loader.ts#L25-L38

  public async createInstancesOfDependencies(
modules: Map<string, Module> = this.container.getModules(),
) {
this.createPrototypes(modules);

try {
await this.createInstances(modules);
} catch (err) {
this.graphInspector.inspectModules(modules);
this.graphInspector.registerPartial(err);
throw err;
}
this.graphInspector.inspectModules(modules);
}

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/instance-loader.ts#L40-L46

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/instance-loader.ts#L80-L85

 private createPrototypes(modules: Map<string, Module>) {
modules.forEach(moduleRef => {
this.createPrototypesOfProviders(moduleRef);
this.createPrototypesOfInjectables(moduleRef);
this.createPrototypesOfControllers(moduleRef);
});
}

private createPrototypesOfControllers(moduleRef: Module) {
const { controllers } = moduleRef;
controllers.forEach(wrapper =>
this.injector.loadPrototype<Controller>(wrapper, controllers),
);
}

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/instance-loader.ts#L25-L38

https://github.com/cheonkyu/nest/blob/master/packages/core/injector/instance-loader.ts#L87-L96

  private async createInstances(modules: Map<string, Module>) {
await Promise.all(
[...modules.values()].map(async moduleRef => {
await this.createInstancesOfProviders(moduleRef);
await this.createInstancesOfInjectables(moduleRef);
await this.createInstancesOfControllers(moduleRef);

const { name } = moduleRef;
this.isModuleWhitelisted(name) &&
this.logger.log(MODULE_INIT_MESSAGE`${name}`);
}),
);
}

private async createInstancesOfControllers(moduleRef: Module) {
const { controllers } = moduleRef;
const wrappers = [...controllers.values()];
await Promise.all(
wrappers.map(async item => {
await this.injector.loadController(item, moduleRef);
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
}),
);
}

4. Prototype vs Instance

NestJS는 두 단계로 객체를 생성: Prototype 단계 (loadPrototype): 실제 인스턴스는 아니고, “class의 껍데기” 같은 placeholder 이후 순환 참조(circular dependency) 감지에 활용 Instance 단계 (loadInstance): constructor 호출 + DI 주입 → 실제로 사용할 인스턴스 생성 Property 기반 주입도 여기서 처리됨

5. Controller 생성 과정 예시

createPrototypesOfControllers → injector.loadPrototype 호출 createInstancesOfControllers → injector.loadController 호출 내부적으로 loadInstance 실행 resolveConstructorParams → 생성자 의존성 주입 instantiateClass → 실제 인스턴스 생성 applyProperties → 프로퍼티 주입 (@Inject 등) 즉, Nest가 컨트롤러를 사용할 수 있는 실제 인스턴스로 만들어주기 위해 두 단계를 거침.

flowchart TD
subgraph Bootstrap["NestFactory.initialize()"]
A[DependenciesScanner] --> B[NestContainer]
B --> C[Modules: Controllers, Providers, Injectables]
A --> D[GraphInspector]
end

subgraph Instance_Creation["InstanceLoader.createInstancesOfDependencies()"]
C --> E[Injector.loadPrototype]
C --> F[Injector.loadInstance]
E --> F
end

subgraph Controller_Creation["Controller Lifecycle"]
F --> G[Injector.loadController]
G --> H[resolveConstructorParams]
H --> I[instantiateClass]
I --> J[applyProperties]
end

subgraph Circular_Dependency
F --> K[CircularDependencyException]
end

· 5 min read

https://docs.nestjs.com/first-steps

1. main.ts (entry point)

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
  • NestFactory.create(AppModule)NestApplication 인스턴스 생성
  • app.listen(3000) → 내부적으로 Express/Fastify 서버 실행

즉, Nest 애플리케이션의 진입점.


2. NestFactory

https://github.com/cheonkyu/nest/blob/master/packages/core/nest-factory.ts#L383-L396

export const NestFactory = new NestFactoryStatic();
  • NestFactory싱글톤 인스턴스
  • 개발자가 NestFactory.create()를 호출하면 내부적으로 NestFactoryStatic.create() 실행됨

3. NestFactoryStatic.create()

https://github.com/cheonkyu/nest/blob/master/packages/core/nest-factory.ts#L79-L113

주요 단계

public async create<T extends INestApplication = INestApplication>(
moduleCls: IEntryNestModule,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
// 1. HTTP 어댑터 초기화
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions!)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];

// 2. 기본 설정 객체 준비
const applicationConfig = new ApplicationConfig();
const container = new NestContainer(applicationConfig, appOptions);
const graphInspector = this.createGraphInspector(appOptions!, container);

// 3. 로거 및 에러 핸들링 설정
this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions);

// 4. 컨테이너와 모듈 초기화
await this.initialize(
moduleCls,
container,
graphInspector,
applicationConfig,
appOptions,
httpServer,
);

// 5. NestApplication 인스턴스 생성
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
graphInspector,
appOptions,
);

// 6. 프록시 생성 (메서드 위임)
const target = this.createNestInstance(instance);
return this.createAdapterProxy<T>(target, httpServer);
}

정리하면:

  1. HTTP 서버 어댑터 결정

    • 사용자가 new FastifyAdapter() 같은 걸 넘기면 그대로 사용
    • 없으면 createHttpAdapter()ExpressAdapter 기본 사용
  2. DI 컨테이너 초기화

    • NestContainer 생성 → 모듈/프로바이더 등록 및 관리
  3. 그래프 인스펙터

    • 의존성 그래프 추적/검사 도구 (--debug 옵션 등에 활용)
  4. 초기화 과정

    • initialize() → 모듈 로딩, 의존성 주입, 라이프사이클 훅 실행
  5. NestApplication 생성

    • 컨테이너 + 어댑터 조합된 앱 객체
  6. Proxy 래핑

    • 최종적으로 반환되는 app은 Proxy → Express/Fastify 메서드를 직접 호출 가능 (app.getHttpServer().get(...) 대신 app.get(...) 등)

4. createHttpAdapter

https://github.com/cheonkyu/nest/blob/master/packages/core/nest-factory.ts#L318-L325

private createHttpAdapter<T = any>(httpServer?: T): AbstractHttpAdapter {
const { ExpressAdapter } = loadAdapter(
'@nestjs/platform-express',
'HTTP',
() => require('@nestjs/platform-express'),
);
return new ExpressAdapter(httpServer);
}
  • NestJS는 플랫폼 독립적이므로, 기본은 ExpressAdapter
  • Fastify를 쓰려면 @nestjs/platform-fastify 설치 후 FastifyAdapter 넘기면 됨
  • 이렇게 해서 Nest는 어댑터 기반의 추상화된 HTTP 계층을 제공

5. createAdapterProxy

private createAdapterProxy<T>(app: NestApplication, adapter: HttpServer): T {
const proxy = new Proxy(app, {
get: (receiver: Record<string, any>, prop: string) => {
const mapToProxy = (result: unknown) => {
return result instanceof Promise
? result.then(mapToProxy)
: result instanceof NestApplication
? proxy
: result;
};

// (1) app에 없는 속성인데 adapter에 있으면 → adapter 메서드 호출
if (!(prop in receiver) && prop in adapter) {
return (...args: unknown[]) => {
const result = this.createExceptionZone(adapter, prop)(...args);
return mapToProxy(result);
};
}

// (2) app의 메서드라면 실행 후 proxy 매핑
if (isFunction(receiver[prop])) {
return (...args: unknown[]) => {
const result = receiver[prop](...args);
return mapToProxy(result);
};
}

// (3) 일반 프로퍼티는 그냥 반환
return receiver[prop];
},
});
return proxy as unknown as T;
}

동작 방식

  • Proxy 객체로 app을 감쌈

  • NestApplication에 없는 속성을 호출하면 → Express/Fastify 어댑터의 메서드로 위임

    • 예: app.get() (NestApplication에는 없음 → Express get() 실행됨)
  • Nest API와 HTTP 서버 API를 자연스럽게 합쳐줌

즉, NestApplicationExpress/Fastify API를 하나의 객체처럼 보이게 함.


최종 정리

NestJS 부트스트랩 과정은 크게 6단계:

  1. NestFactory.create(AppModule) 호출
  2. HTTP 어댑터(Express/Fastify) 결정
  3. NestContainer 초기화 (DI 컨테이너, 모듈 등록)
  4. 모듈 초기화 및 의존성 주입 완료
  5. NestApplication 생성
  6. Proxy로 Nest와 Express/Fastify API 연결 후 반환

이후 app.listen(3000) 하면 내부적으로 Express/Fastify 서버가 실행되고, Nest 컨테이너에 등록된 컨트롤러들이 라우팅에 연결됨.

· One min read

페이지 이동 시 스크롤 top 처리

import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'

export default function ScrollToTop() {
const { pathname } = useLocation()

useEffect(() => {
window.scrollTo(0, 0)
}, [pathname])

return null
}
export default function App() {
return (
<div className="flex min-h-screen flex-col">
<ScrollToTop />
<Header transparentBg={transparentBg} />
<main className="flex flex-1 flex-col">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/brand" element={<BrandStory />} />
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
</Routes>
</main>
<Footer />
</div>
)
}

· 3 min read

프록시 컴포넌트 작성하기

Q. 표준 HTML 컴포넌트를 많이 사용하는데, 이때 필요한 모든 프로퍼티를 항상 설정하는 작업이 번거로움 A. 프록시 컴포넌트를 만들고 적용한다.

ㅅㄷㄴㅅ

특정 속성 재정의 못하게 하기

type SubmitButtonProps = Omit<JSX.IntrinsicElements["button"], "type">;

function SubmitButton(props: SubmitButtonProps) {
return <button type="submit" {...props} />;
}
type StyleButton = Omit<
JSX.IntrinsicElements["button"],
"type" | "className" | "style"
> & { type: "primary" | "secondary" };

function StyledButton({ type, ...allProps }: StyledButton) {
return <Button type="button" className={`btn-${type}`} {...allProps} />;
}

특정 속성만 필수 값으로 정의하기

type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
type ImgProps = MakeRequired<JSX.IntrinsicElements["img"], "alt" | "src">;

export function Img(prps: ImgProps) {
return <img {...props} />;
}

const anImage = <Img />;
// alt, src 속성만 필수여서 ts 에러가 남

제어 컴포넌트 구현하기

Q. 입력과 같은 form 요소의 상태를 브라우저와 리엑트 중 어디에서 관리할지 결정해야 하므로 상화이 복잡해짐 A. 런타임에 ㅣㅂ제어와 제어로 전환되지 않도록 구별된 유니온과 선택형 never 기법을 사용하는 프록시 컴포넌트를 구현

import React, { useState } from "react";

type OnlyRequired<T, K extends keyof T = keyof T> = Required<Pick<T, K>> &
Partial<Omit<T, K>>;

type ControlledProps = OnlyRequired<
JSX.IntrinsicElements["input"],
"value" | "onChange"
> & {
defaultValue?: never;
};

type UnControlledProps = OnlyRequired<
JSX.IntrinsicElements["input"],
"value" | "onChange"
> & {
defaultValue: string;
value?: never;
onChange?: never;
};

type InputProps = ControlledProps | UnControlledProps;

function Input({ ...allProps }: InputProps) {
return <input {...allProps} />;
}

function Controlled() {
const [val, setVal] = useState("");
return <Input value={val} onChange={(el) => setVal(e.target.value)} />;
}

function UnControlled() {
return <Input defaultValue="Hello" />;
}

사용자 정의 훅 형식 정의하기

Q. 커스텀 훅을 정의하고 적절한 형식을 얻으려 한다. A. 튜플 형식이나 const 컨텍스트를 사용한다.

export const useToggle = (initialValue: boolean) => {
const [value, setValue] = useState(initialValue);
const toggleValue = () => setValue(!value);
return [value, toggleValue] as const;
};

리엑트의 합성 이벤트 시스템에서 콜백 형식화하기

import React from "react";

type WithChildren<T = {}> = T & { children?: React.ReactNode };

type ButtonProps = {
onClick: (event: React.MouseEvent) => void;
} & WithChildren;

function Button({ onClick, children }: ButtonProps) {
return <button onClick={onClick}></button>;
}

· 6 min read

함수 시그니처 일반화하기

Q. 같은 기능을 수행하지만 서로 다른 형식(호환되지 않음)을 취급하는 두 함수가 있음 A. 제네릭으로 동작을 일반화

type Languages = {
en: URL;
kr: URL;
};

type AllowedElements = {
video: string;
};

// in 키워드를 이용하기 위해 extends Object 해줌
function isAvailable<T extends Object>(
target: T,
key: string | number | symbol
): key is keyof T {
return key in target;
}

console.log(isAvailable({ en: "stet", kr: "st" }, "kr"));
console.log(isAvailable({ video: "test" }, "kr"));

관련된 함수 인수 만들기

Q. 첫번째 매개변수에 의존하는 두 번째 함수 매개변수를 구현하기 A. 제네릭 제약(generic constraint)를 이용

type Languages = {
en: URL;
kr: URL;
};

const languages = {
en: "https://google.com",
kr: "https://naver.com",
};

type URLList = {
[x: string]: URL;
};

function fetchFile<List>(urls: List, key: keyof List) {
// TODO
}

function fetchFiles<List>(urls: List, keys: (keyof List)[]) {
// TODO
}
fetchFile(languages, "kr");
fetchFiles(languages, ["kr", "en"]);

형식 맵을 이용한 매핑 형식 사용하기

Q. 문자열 식별자에 기반한 특정 하위 형식의 객체를 만드는 팩토리 함수를 구현하는데, 다양한 하위 형식이 존재할 수 있는 상황 A. 모든 하위 형식에 형식 맵에 저장하고 인덱스로 접근할 수 있게 한 다음 Partial<T> 같은 매핑된 형식을 사용

// interface HTMLElementTagNameMap {
// // 선언합치기
// [x: string]: HTMLUnknownElement
// }

type AllElements = HTMLElementTagNameMap & {
[x: `${string}-${string}`]: HTMLElement;
};

// function createElement<T extends keyof AllElements>(key: T, props?: Partial<AllElements[T]>) : AllElements[T] {
// const el = document.createElement(key as string) as AllElements[T]
// return Object.assign(el, props);
// }

// 별도로 정의한 타입 오버로딩
function createElement<T extends keyof AllElements>(
key: T,
props?: Partial<AllElements[T]>
): AllElements[T];
function createElement(tag: string, props?: Partial<HTMLElement>): HTMLElement {
const el = document.createElement(tag as string) as HTMLElement;
return Object.assign(el, props);
}

const a = createElement("a", { href: "test" });
const b = createElement("my-element");

새 객체 형식 생성하기

Q. 모델과 관련 있는 형식이 애플리케이션이 존재한다. 모델이 바뀔 때마다 형식도 바꿔야 한다. A. 제네릭 매핑된 형식을 이용해 원래 형식에 기반한 새 객체 형식을 만듬

type ToyBase = {
name: string;
description: string;
minimumAge: number;
};

type BoardGame = ToyBase & {
kind: "boardgame";
players: number;
};

type Doll = ToyBase & {
kind: "doll";
material: "plastic";
};

type Toy = Doll | BoardGame;

// type GroupedToys = {
// boardgame: Toy[];
// puzzle: Toy[];
// doll: Toy[];
// }

type GroupedToys = {
[k in Toy["kind"]]?: Toy[];
};

function groupToys(toys: Toy[]): GroupedToys {
const groups: GroupedToys = {};
for (let toy of toys) {
groups[toy.kind] = groups[toy.kind] ?? [];
groups[toy.kind]?.push(toy);
}
return groups;
}
type ToyBase = {
name: string;
description: string;
minimumAge: number;
};

type BoardGame = ToyBase & {
kind: "boardgame";
players: number;
};

type Doll = ToyBase & {
kind: "doll";
material: "plastic";
};

type Toy = Doll | BoardGame;

// type GroupedToys = {
// boardgame: Toy[];
// puzzle: Toy[];
// doll: Toy[];
// }

// Group 타입 정의하기
type Group<Collection, Selector extends keyof Collection> = {
[k in Collection[Selector] extends string // Selector가 Collection의 유효한 키인지 판단
? Collection[Selector]
: never]?: Collection[];
};
type GroupedToys = Partial<Group<Toy, "kind">>;

function groupToys(toys: Toy[]): GroupedToys {
const groups: GroupedToys = {};
for (let toy of toys) {
groups[toy.kind] = groups[toy.kind] ?? [];
groups[toy.kind]?.push(toy);
}
return groups;
}

ThisType으로 객체의 this 정의하기

Q. 앱에서 여러 메서드를 포함하는 복잡한 설정 객체들을 처리하는데, 사용처에 따라 this의 컨텍스트가 달라짐 A. 내장 제네릭 ThisType<T>를 이용해 this를 올바르게 정의

Vuejs 같은 프레임워크는 많은 팩토리 함수에 의존

  • data 함수 : 인스턴스 초기 데이터 반환
  • computed 프로퍼티 : 계산된 프로퍼티 저장. 이들 함수는 일반 프로퍼티에 접근하듯이 초기 데이터에 접근 가능
  • methods 프로퍼티 : 메서드는 호출할 수 있는 대상이며, 초기 데이터와 계산된 프로퍼티 모두 접근 가능
// Record<string, () => any>
// type FnObj = {
// [name: string]: () => any;
// };
type FnObj = Record<string, () => any>;

// ReturnType<FunctionObj[K]>
// FunctionObj[K]가 반환하는 타입
type MapFnToProp<FunctionObj extends FnObj> = {
[K in keyof FunctionObj]: ReturnType<FunctionObj[K]>;
};

type Options<Data, Computed extends FnObj, Methods> = {
data(this: {}): Data;
computed?: Computed & ThisType<Data>;
methods?: Methods & ThisType<Data & MapFnToProp<Computed> & Methods>;
};

declare function create<Data, Computed extends FnObj, Methods>(
options: Options<Data, Computed, Methods>
): any;

const instance = create({
data() {
return {
firstName: "cheonkyu",
lastName: "kim",
};
},
computed: {
fullName() {
return `${this.firstName} + ${this.lastName}`;
},
},
methods: {
hello() {
console.log(this.fullName);
},
},
});

· 5 min read

타입스크립트의 문자열 템플릿 리터럴 형식을 이용한 프로젝트

사용자 정의 이벤트 시스템 정의하기

Q. 사용자 정의 이벤트 시스템을 만들 때 모든 이벤트명은 "on"으로 시작하도록 이름 규칙을 정하려고 함 A. 문자열 템플릿 리터럴 형식을 이용해 문자열 패턴을 기술

type EventName = `on${string}`; // "on"으로 시작하는 문자열 타입
type EventObject<T> = {
val: T;
};

type Callback<T = any> = (ev: EventObject<T>) => void;
type Events = {
[x: EventName]: Callback[] | undefined;
};

class EventSystem {
events: Events;
constructor() {
this.events = {};
}

defineEventHandler(name: EventName, callback: Callback): void {
this.events[name] = this.events[name] ?? [];
this.events[name]?.push(callback);
}

trigger(name: EventName, value: any) {
const callbacks = this.events[name];
if (callbacks) {
callbacks.forEach((cb) => {
cb({ val: value });
});
}
}
}

const system = new EventSystem();
system.defineEventHandler("onClick", (value) => {
console.log("onClick", value);
});
system.trigger("onClick", "test");

문자열 조작 형식과 키 매핑으로 이벤트 콜백 만들기

Q. 아무 객체를 받아 각 프로퍼티에 watch 함수를 제공해서 이벤트 콜백을 정의하도록 허용하려 한다. A. 키 매핑을 이용해 새 문자열 프로퍼티 키를 만듬. 와쳐 함수가 적절한 camel case 를 사용하도록 문자열 조작

다음 코드처럼 프로퍼티가 변경이 되면 감지할 수 있게 구현하고 싶음

const system = new EventSystem();

let person = {
name: "kim",
age: 90,
};
const watchedPerson = system.watch(person);
watchedPerson.onAgeChanged((event) => {
console.log(event.val, "changed");
});

watchedPerson.age = 91;
type EventName = `on${string}`; // "on"으로 시작하는 문자열 타입
type EventObject<T> = {
val: T;
};

type Callback<T = any> = (ev: EventObject<T>) => void;
type Events = {
[x: EventName]: Callback[] | undefined;
};

function handleName(name: string): EventName {
function capitalize(inp: string) {
return inp.charAt(0).toUpperCase() + inp.slice(1);
}
return `on${capitalize(name)}Changed` as EventName;
}

class EventSystem {
events: Events;
constructor() {
this.events = {};
}

defineEventHandler(name: EventName, callback: Callback): void {
this.events[name] = this.events[name] ?? [];
this.events[name]?.push(callback);
}

trigger(name: EventName, value: any) {
const callbacks = this.events[name];
if (callbacks) {
callbacks.forEach((cb) => {
cb({ val: value });
});
}
}

watch<T extends object>(obj: T): T & WatchedObject<T> {
const self = this;
return new Proxy(obj, {
get(target, property) {
if (
typeof property === "string" &&
property.startsWith("on") &&
property.endsWith("Changed")
) {
return (callback: Callback) => {
self.defineEventHandler(property as EventName, callback);
};
}
return target[property as keyof T];
},
set(target, property, value) {
if (property in target && typeof property === "string") {
target[property as keyof T] = value;
self.trigger(handleName(property), value);
return true;
}
return false;
},
}) as T & WatchedObject<T>;
}
}

type WatchedObject<T> = {
[K in string & keyof T as `on${Capitalize<K>}Changed`]: (
ev: Callback<T[K]>
) => void;
};
type Person = {
name: string;
age: number;
};

// type WatchedPerson = {
// onNameChanged: (ev: Callback<string>) => void;
// onAgeChanged: (ev: Callback<number>) => void;
// }
type WatchedPerson = WatchedObject<Person>;

const system = new EventSystem();

let person = {
name: "kim",
age: 90,
};
const watchedPerson = system.watch(person);
watchedPerson.onAgeChanged((event) => {
console.log(event.val, "changed");
});

watchedPerson.age = 91;

템플릿 리터럴을 구분자로 사용하기

Q. 벡엔드 요청 상태를 pending에서 error나 success로 변경하는 상태 머신을 모델링, 다양한 벡엔드 요청에 이들 상태를 적용하면서 내부 형식은 하나로 유지해야함 A. 문자열 템플릿 리터럴을 구별된 유니온의 구별자로 사용

type User = {};
type Order = {};
type Data = {
user: User | null;
order: Order | null;
};
type RequestConstants = keyof Data;

type Pending = {
state: `${Uppercase<RequestConstants>}_PENDING`;
};

type Err = {
state: `${Uppercase<RequestConstants>}_ERROR`;
message: string;
};

// type Success = {
// state: `${Uppercase<RequestConstants>}_SUCCESS`,
// data: NonNullable<Data[RequestConstants]>
// }
type Success = {
[K in RequestConstants]: {
state: `${Uppercase<K>}_SUCCESS`;
data: NonNullable<Data[K]>;
};
}[RequestConstants];

type BackendRequest = Pending | Err | Success;

· 7 min read

빠진 프로퍼티와 undefined 값 구별하기

  • 선택형 프로퍼티를 더 엄격하게 처리하도록 tsconfig에서 exactOptionalPropertyTypes 활성화하기 (Interpret optional property types as written, rather than adding undefined.)
type Settings = {
language: "en" | "kr";
theme?: "github";
};

function getTheme(settings: Settings) {
if ("theme" in settings) {
return settings.theme;
}
return "default";
}

// 빠진 프로퍼티
const settings: Settings = {
language: "en",
};

// undefined 값
const settings1: Settings = {
language: "en",
theme: undefined,
};

console.log(getTheme(settings)); // 'default'
console.log(getTheme(settings1)); // undefined

열거형 사용하기

Q. 타입스크립트 enum(열거형)은 다른 형식 시스템과 다르게 동작하는 거 같다.

A. enum 보다는 const enum을 사용한다. enum의 문제가 무엇인지 이해하고 유니온 형식 같은 대안을 활용한다.

다음 타입스크립트 코드는 js 코드를 만들어낸다.

enum Direction {
Up,
Down,
Left,
Right,
}
"use strict";
var Direction;
(function (Direction) {
Direction[(Direction["Up"] = 0)] = "Up";
Direction[(Direction["Down"] = 1)] = "Down";
Direction[(Direction["Left"] = 2)] = "Left";
Direction[(Direction["Right"] = 3)] = "Right";
})(Direction || (Direction = {}));

const enum를 이용하면 코드 생성을 안함

const enum Direction {
Up,
Down,
Left,
Right,
}
"use strict";

ts 5.0 이후부터는 열거형 해석이 엄격해졌다. 그 이전에는 의도대로 동작안할 여지가 있다.

구조적 형식 시스템에 명목상 형식 정의하기

Q. 애플리케이션에 같은 기본 형식을 가리키는 별칭이지만 의미가 완전히 다른 여러 형식이 있다. 구조적 형식 시스템에서는 이들을 동일하게 취급하지만 그러면 안된다.

A. (브랜딩) 타입마다 구분할 수 있는 속성을 정의한다.

다음 코드는 동작한다.

자바스크립트는 주로 객체 리터럴에 의존하며 타입스크립트는 형식이나 이들 리터리의 모양을 추론하려 노력

type Person = { name: string };
type Student = { name: string };

function isPersion(person: Person) {
console.log(person);
}

const student: Student = {
name: "a",
};

isPersion(student);

kind 속성을 추가

type Person = {
name: string;
_kind: "person";
};
type Student = {
name: string;
_kind: "student";
};

function isPersion(person: Person) {
console.log(person);
}

const student: Student = {
name: "a",
_kind: "student",
};

// isPersion(student)
// Argument of type 'Student' is not assignable to parameter of type 'Person'.
// Types of property '_kind' are incompatible.
// Type '"student"' is not assignable to type '"person"'.(2345)
type Credits = number & { _kind: "credits" };
type AccountNumber = number & { _kind: "accountNumber" };

const account = 12345678 as AccountNumber;
let balance = 1000 as Credits;
const amount = 1000 as Credits;

function increase(balance: Credits, amount: Credits) {
// 결과가 number라 Credits 타입으로 `형식 어서션` 해줌
return (balance + amount) as Credits;
}

balance = increase(balance, amount);
// balance = increase(balance, account)
// Argument of type 'AccountNumber' is not assignable to parameter of type 'Credits'.
// Type 'AccountNumber' is not assignable to type '{ _kind: "credits"; }'.
// Types of property '_kind' are incompatible.
// Type '"accountNumber"' is not assignable to type '"credits"'.(2345)

문자열 하위 집합의 느슨한 자동 완성 활성화하기

Q. API는 모든 문자열을 전달할 수 있어야 하는데, 그중에서도 몇 가지 문자열값을 자동 완성으로 보여주려 한다. A. 문자열 리터럴 유니온 형식에 string & 를 추가

유즈케이스

// 자동완성됨
type ContentType = "post" | "page" | "asset" | (string & {});
// 자동완성 안됨
// type ContentType = "post" | "page" | "asset" | string;

function retrieve(content: ContentType) {}
retrieve("");

선택형 never로 배타적 논리합 모델 만들기

Q. 유니온에서 서로 겹치지 않도록 모델을 만들어야하는데, 이를 구별할 kind 프로퍼티를 api에서 사용할 수 없는 상황 A. 선택형 never 기법으로 특정 프로퍼티를 제외 (구별해야하는 프로퍼티가 적을때 활용 가능)

type SelectBase = {
options: string[];
};

type SingleSelect = SelectBase & {
value: string;
};

type MultipleSelect = SelectBase & {
values: string[];
};

type SelectProperties = SingleSelect | MultipleSelect;

function select(param: SelectProperties) {
if ("value" in param) {
// 단건일때
} else if ("value" in param) {
// 여러 건일때
}
}

select({
options: ["a"],
value: "1",
});
select({
options: ["a"],
values: ["1"],
});

// 이거는 무슨 동작???
select({
options: ["a"],
values: ["1"],
value: "1",
});

선택적 never를 적용

type SelectBase = {
options: string[];
};

type SingleSelect = SelectBase & {
value: string;
values?: never;
};

type MultipleSelect = SelectBase & {
values: string[];
value?: never;
};

assertNever 함수를 이용해 완전 검사하기

Q. 시간이 지나면서 유니온 형식에 새 구성요소를 추가하는 상황이 발생. 관련된 부분의 모든 코드에서 고쳐야함 A. assertNever 함수로 모든 남은 케이스에 어서션을 적용해 모든 상황을 확인하도록 완전 검사를 수행

type Circle = {
radius: number;
kind: "circle";
};

type Square = {
x: number;
kind: "square";
};

// 신규 타입이 나오고
type Rectangle = {
x: number;
y: number;
kind: "rectangle";
};

// type Shape = Circle | Square;
// 유니온 타입이 새로 확장된다면
type Shape = Circle | Square | Rectangle;

function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI;
case "square":
return shape.x * shape.x;
default:
// 이 케이스에서 shape는 never가 아닌 shape 타입이기에
// ts에서 에러를 얄려줌
throw assertNever(shape);
}
}

function assertNever(value: never) {
console.error(value);
throw Error("not posible");
}

· 3 min read
  • Parial
  • Required
  • Readonly
  • Pick
  • Record
  • Exclude
  • Extract
  • Omit
  • NonNullable
  • Parameters
  • ConstructorParameters
  • ReturnType
  • InstanceType
  • ThisType

Parial

Partial: 기존 객체의 속성을 전부 옵셔널로 만드는 함수

type MyPartial<T> = {
[P in keyof T]?: T[P];
};

type Result = MyPartial<{ a: string; b: number }>;

Required

Required: 기본 객체의 속성을 전부 필수로 만드는 함수

type MyRequired<T> = {
[P in keyof T]-?: T[P];
};

type Result = MyRequired<{ a?: string; b?: number }>;

Readonly

기본 객체의 속성을 전부 읽기 전용으로 만드는 함수

type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};

type Result = MyReadonly<{ a: string; b: number }>;

Pick

기본 객체에서 지정한 속성만 추리는 함수

type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};

type Result = MyPick<{ a: string; b: number; c: number }, "a" | "b">;

Record

type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};

type Result = MyRecord<"a" | "b", string>;

Exclude

Exclude: 어떤 타입에서 지정한 타입을 제거하는 타입

컨디셔널 타입 활용

type MyExclude<T, U> = T extends U ? never : T;

type Result = MyExclude<1 | "2" | 3, string>;
// type Result = 1 | 3

Extract

Extract: 어떤 타입에서 지정한 타입을 추출하는 타입

컨디셔널 타입 활용

type MyExtract<T, U> = T extends U ? T : never;

type Result = MyExtract<1 | "2" | 3, string>;

Omit

Omit: 특정 객체에서 지정된 속성을 제거하는 타입

Pick, Exclude

type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Result = MyOmit<{ a: "1"; b: 2; c: true }, "a" | "c">;

NonNullable

null과 undefined를 제거하는 nullable 타입

type MyNonNullable<T> = T extends null | undefined ? never : T;
type Result = MyNonNullable<string | null | undefined>;
type MyNonNullable<T> = T & {};
type Result = MyNonNullable<string | null | undefined>;

Parameters

ConstructorParameters

ReturnType

InstanceType

ThisType

메서드들에게 this를 한 방에 주입하는 타입

type Data = { money: number };

type Methods = {
addMoney(amount: number): void;
};

type Obj = {
data: Data;
methods: Methods & ThisType<Data & Methods>;
};

const obj: Obj = {
data: {
money: 1,
},
methods: {
addMoney(amount) {
this.money += amount;
},
},
};

· 10 min read

타입 추론을 적극 활용하자

타입스크립트가 타입을 제대로 추론하면 그대로 쓰고, 틀리게 추론할때만 타입 명시 타입스트립트 추론이 일반적으로는 보다 정확함

타입스크립트에만 있는 타입

  • any
  • unknown
  • void : 반환값이 없음
  • , Object : null과 undefined를 제외한 모든 값을 의미

네임스페이스 : 네임스페이스 별로 인터페이스를 분리 가능

좁은 타입은 넓은 타입에 대입할 수 있지만, 넣은 타입은 좁은 타입에 대입할 수 없다

객체의 속성과 메서드에 적용되는 특징

객체 리터럴을 대입했냐, 변수를 대입했냐에 따라 타입 검사 방식이 달라짐

객체 리터럴 대입 시 에러

interface Example {
hello: string;
}

const ex: Example = {
hello: "hi",
why: "sdfsd", // error: Object literal may only specify known properties, and 'why' does not exist in type 'Example'.(2353)
};

const obj = {
hello: "hi",
why: "123",
};
const ex1 = obj;

함수 파라미터 대입 시 에러

interface Money {
amount: number;
unit: string;
}

const money = { amount: 100, unit: "won", error: "에러 안남" };

function addMoney(money1: Money, money2: Money) {
return {
amount: money1.amount + money2.amount,
unit: "won",
};
}
addMoney(money, { amount: 3000, unit: "money", erorr: "엘러" }); // Object literal may only specify known properties, and 'erorr' does not exist in type 'Money'.(2353)

인덱스 접근 타입 (Indexed Access Type)

type Animal = { name: string };

type N1 = Animal["name"]; // N1 타입은 string
const obj = {
hello: "world",
name: "zero",
age: 28,
};
type Keys = keyof typeof obj; // type Keys = "hello" | "name" | "age"
type Values = (typeof obj)[Keys]; // type Values = string | number

매핑된 객체 타입 (Mapped Object Type)

type HelloAndHi = {
// [key: 'hello' | 'hi'] : string //에러
[key in "hello" | "hi"]: string;
};
  1. 기존 객체 타입을 복사하는 코드 (in, keyof)
interface Original {
name: string;
age: number;
married: boolean;
}
type Copy = {
[key in keyof Original]: Original[key];
};
  1. 다른 타입으로부터 값을 가져오면서 수식어를 붙일 수 있음
interface Original {
name: string;
age: number;
married: boolean;
}
type Copy = {
readonly [key in keyof Original]?: Original[key];
};
// type Copy = {
// readonly name?: string | undefined;
// readonly age?: number | undefined;
// readonly married?: boolean | undefined;
// }
  1. 다른 타입으로부터 값을 가져오면서 수식어 제거 가능
interface Original {
readonly name?: string;
readonly age?: number;
readonly married?: boolean;
}
type Copy = {
-readonly [key in keyof Original]-?: Original[key];
};
// type Copy = {
// name: string;
// age: number;
// married: boolean;
// }
  1. 속성 이름을 바꿀 수 있음

속성 명칭 대문자로 변환

interface Original {
readonly name?: string;
readonly age?: number;
readonly married?: boolean;
}
type Copy = {
[key in keyof Original as Capitalize<key>]: Original[key];
};
// type Copy = {
// readonly Name?: string | undefined;
// readonly Age?: number | undefined;
// readonly Married?: boolean | undefined;
// }

유니언(|), 인터섹션(&)

type A = string | boolean;
type B = boolean | number;
type C = A & B; // type C = boolean

type D = {} & (string | null); // type D = string
type E = string & boolean; // type E = never
type F = unknown | {}; // type F = unknown
type G = never & {}; // type G = never

타입 상속

인터페이스를 이용한 상속

interface Animal {
name: string;
}

interface Dog extends Animal {
bark(): void;
}

interface Cat extends Animal {
meow(): void;
}

타입 별칭을 이용한 상속

type Animal = {
name: string;
};

type Dog = Animal & {
bark(): void;
};
type Cat = Animal & {
meow(): void;
};
type Name = Cat["name"];

객체 간에 대입할 수 있는지 확인

타입스크립트에서는 모든 속성이 동일하면 객체 타입의 이름이 다르더라도 동일한 타입으로 취급 Money와 Liter가 동일한 타입으로 인식

interface Money {
amount: number;
unit: string;
}

interface Liter {
amount: number;
unit: string;
}

const liter: Liter = { amount: 1, unit: "liter" };
const circle: Money = liter;

브랜드(brand) 속성을 추가 (브랜딩)해서 타입 간 구별

interface Money {
__type: "money";
amount: number;
unit: string;
}

interface Liter {
__type: "liter";
amount: number;
unit: string;
}

const liter: Liter = { amount: 1, unit: "liter" };
const circle: Money = liter;
// type 'Liter' is not assignable to type 'Money'.
// Types of property '__type' are incompatible.
// Type '"liter"' is not assignable to type '"money"'.(2322)

제네릭

인터페이스에 제네릭을 이용해서 타입 간 상속

// interface Zero {
// type: 'human',
// race: 'yellow',
// name: 'zero',
// age: 28
// }
// interface Nero {
// type: 'human',
// race: 'yellow',
// name: 'nero',
// age: 32
// }
interface Person<N, A> {
type: "human";
race: "yellow";
name: N;
age: A;
}
interface Zero extends Person<"zero", 28> {}
interface Nero extends Person<"nero", 32> {}

타입에 제네릭 이용

type Person<N, A> = {
type: "human";
race: "yellow";
name: N;
age: A;
};
type Zero = Person<"zero", 28>;
type Nero = Person<"nero", 32>;

클래스에 제네릭 이용

class Person<N, A> {
name: N;
age: A;
constructor(name: N, age: A) {
this.name = name;
this.age = age;
}
}
class Zero extends Person<"zero", 28> {}
class Nero extends Person<"nero", 28> {}

조건문과 비슷한 컨디셔널 타입

type A1 = string;
type B1 = A1 extends string ? number : boolean;

콜백 함수의 매개변수는 생략 가능하다.

function example(callback: (error: Error, result: string) => void) {}

example((e, r) => {});
example(() => {});
example((e, r) => true);

공변성과 반공변성을 알아야 함수끼리 대입할 수 있다.

  • 공변성: A->B일때 T(A) -> T(B)인 경우
  • 반공변성: A->B일때 T(B) -> T(A)인 경우
  • 이변성: A->B일때 T(A) -> T(B), T(B) -> T(A) 인 경우
  • 무공변성: A->B일때 T(A) -> T(B)도 안되고, T(B) -> T(A)도 안되는 경우

타입스크립트는 기본적으로 공변성이다.

함수의 매개변수는 반공변성을 갖고 있음(strictFunctionTypes 옵션이 체크되어야함)

Enum 타입

enum 타입을 브랜딩에 사용

enum Money {
WON,
DOLLAR,
}

interface Won {
__type: Money.WON;
}

interface Dollar {
__type: Money.DOLLAR;
}

function moneyOrDollar(param: Won | Dollar) {
if (param.__type === Money.WON) {
} else {
}
}

재귀 타입

type Recursive = {
name: string;
children: Recursive[];
};

const recur: Recursive = {
name: "tes",
children: [],
};

템플릿 리터럴 타입을 사용

type Template = `template ${string}`;
let str: Template = "template ";
let str1: Template = "template 123";

문자열 조합을 표현

type City = "seoul" | "suwon" | "busan";
type Vehicle = "car" | "bike" | "walk";
type ID = `${City}:${Vehicle}`;
const id: ID = "seoul:walk";

추가적인 타입 검사에는 satisfies 연산자

타입스크립트 4.9 버전에 satisfies 연산자가 추가됨

// Object literal may only specify known properties, but 'sriius' does not exist in type '{ sun: string | { type: string; parent: string; }; sirius: string | { type: string; parent: string; }; earth: string | { type: string; parent: string; }; }'. Did you mean to write 'sirius'?(2561)
const universe = {
sun: "star",
sriius: "star",
earth: { type: "planet", parent: "sum" },
} satisfies {
[key in "sun" | "sirius" | "earth"]:
| { type: string; parent: string }
| string;
};
universe.earth.type;

원시 자료형에도 브랜딩 기법을 사용할 수 있다.

원시 자료형 타입에 브랜드 속성을 추가하는 기법 (타입스크립트만의 기법)

type Brand<T, B> = T & { __brand: B };
type KM = Brand<number, "km">;
type Mile = Brand<number, "mile">;

function kmToMile(km: KM) {
return (km * 0.62) as Mile;
}

const km = 3 as KM;
const mile = kmToMile(km);
// kmToMile(mile)

유용한 타입을 만들자.

판단하는 타입

데코레이터

  • ClassDecoratorContext: 클래스 자체를 장식할때
  • ClassMethodDecoratorContext: 클래스 메서드를 장식할 때
  • ClassGetterDecoratorContext: 클래스 getter를 장식할 때
  • ClassSetterDecoratorContext: 클래스 setter를 장식할 때
  • ClassMemberDecoratorContext: 클래스 멤버를 장식할 때
  • ClassAccessorDecoratorContext: 클래스 accessor를 장식할 때
  • ClassFieldDecoratorContext: 클래스 필드를 장식할 때
function startAndEnd(originalMethod: any, context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("start");
const result = originalMethod.call(this, ...args);
console.log("end");
return result;
}
return replacementMethod;
}

class A {
@startAndEnd
eat() {
console.log("eat");
}
}

const a = new A();
a.eat();

· One min read
문제1.
작업 클러스터 : k8s
Deployment를 이용해 nginx 파드를 3개 배포한 다음 컨테이너 이미지 버전을 rolling update하고
update record를 기록합니다. 마지막으로 컨테이너 이미지를 previous version으로 roll back 합니다.
•name: eshop-payment
•Image : nginx
•Image version: 1.16
•update image version: 1.17
•label: app=payment, environment=production
% kubectl create deployment eshop-payment --image=nginx:1.17 --dry-run=client -o wide > eshop-payment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: payment
environment: production
name: eshop-payment
spec:
replicas: 3
selector:
matchLabels:
app: payment
environment: production
template:
metadata:
labels:
app: payment
environment: production
spec:
containers:
- image: nginx:1.16
name: nginx
%  kubectl rollout history deployment eshop-payment
deployment.apps/eshop-payment
REVISION CHANGE-CAUSE
1 kubectl apply --filename=eshop-payment.yaml --record=true
2 kubectl set image deployment eshop-payment nginx=nginx:1.17 --record=true
% kubectl rollout undo deployment eshop-payment
deployment.apps/eshop-payment rolled back