원문 보기 : https://saul-mirone.github.io/how-to-design-a-type-friendly-context/
자바스크립트 세계에서, Koa는 하나의 이정표와 같은 역할을 하였습니다.
Koa는 애플리케이션의 핵심은 플러그인을 load하기 위한 코어로서 동작하며, 여러 기능을 제공하기 위해 다양한 플러그인을 번들할 수 있는 역할임을 되새기게 합니다. (ex, 스프링, express)
vscode의 확장 프로그램, 웹팩의 플러그인과 같은 비슷한 사례들을 발견할 수 있습니다.
자바스크립트의 컨텍스트
app.use(async (ctx, next) => {
// inject props into ctx
ctx.foo = 'bar';
const startTime = Date.now();
await next();
// do something after other ctx done.
const endTime = Date.now();
const duration = endTime - startTime;
console.log('Ctx duration:', duration);
})
정적 타입 체크
정적 타입 시스템을 사용하는 언어의 일반적인 문제: 모듈 간에 공유되는 객체를 우아하게 처리하는 방법이 있을까요?
설계
(주 : 왜 제어의 역전일까? 해당 글에는 안나와있어서 행간을 읽어야한다.
ctx에 데이터를 직접 주입하는게 아니라, createCtx와 createSlice를 만들어 코드가 ctx에 데이터를 대신 주입하게 한다.)
koa의 컨텍스트 디자인을 다시 생각해 보겠습니다. 컨텍스트가 ctx.foo와 같이 수정할 수 있는 속성이 있는 객체임을 알 수 있습니다.
이 API를 ctx.get(foo)으로 변환하면 어떨까요?
foo의 생성은 우리가 제어할 수 있는 것이기 때문에 이에 대한 정보를 작성할 수 있습니다.
const ctx = createCtx();
const numberSlice = createSlice(0);
// inject a ctx.
ctx.inject(numberSlice);
const number = ctx.get(numberSlice); // -> 0
// set value of numberSlice to 1.
ctx.set(numberSlice, number + 1);
새로운 데이터 구조인 슬라이스를 도입했습니다.
이 디자인으로 전체 ctx를 여러 조각으로 나눴습니다.
이제 ctx 및 slice의 구조를 정의할 수 있습니다.
type Ctx = Map<symbol, Slice>;
type Slice<T = unknown> = {
id: symbol;
set: (value: T) => void;
get: () => T;
}
Slice
type Metadata<T> = {
id: symbol;
(ctx: Ctx): Slice<T>;
};
const createSlice = <T>(defaultValue: T): Metadata<T> => {
const id = Symbol('Slice');
const metadata = (ctx: Ctx) => {
let inner = defaultValue;
const slice: Slice<T> = {
id,
set: (next) => {
inner = next;
},
get: () => inner
}
ctx.set(id, slice as Slice);
return slice;
}
metadata.id = id;
return metadata;
}
Ctx
const createCtx = () => {
const map: Ctx = new Map();
const getSlice = <T>(metadata: Metadata<T>): Slice<T> => {
const value = map.get(metadata.id);
if (!value) {
throw new Error('Slice not injected');
}
return value as Slice<T>;
}
return {
inject: <T>(metadata: Metadata<T>) => metadata(map), // inject slice (id, slice)
get: <T>(metadata: Metadata<T>): T => getSlice(metadata).get(), // T - default value
set: <T>(metadata: Metadata<T>, value: T): void => {
getSlice(metadata).set(value); // void (inner = next)
}
}
}
Testing
메타데이터 (자체 심벌 ID와 내부에 슬라이스 존재) 생성
메타데이터 안의 슬라이스는 get, set 메서드로 클로저를 업데이트, 및 리턴함.
메타데이터 안의 슬라이스는 주입할 때마다 새로 생김 (컨텍스트 끼리 공유 안됨)
메타데이터 자체도 get 및 set 할 수 있음.
ctx에 inject하면 createSlice가 리턴한 함수(메타데이터)가 ctx 내부에 (id,slice)형태로 주입해줌.
ctx.get하면, ctx에서 메타데이터 아이디로 해당 슬라이스를 찾아 값을 꺼내옴.
ctx.set하면 해당 메타데이터 내부의 값을 바꿈.
// 메타데이터를 만듭니다.
const num = createSlice(0);
// 컨텍스트를 만듭니다.
const ctx1 = createCtx();
const ctx2 = createCtx();
// 컨텍스트에 메타데이터를 주입합니다.
ctx1.inject(num); // (id,slice)
ctx2.inject(num); // (id,slice)
// 컨텍스트에서 해당 Default 값을 얻습니다.
const x = ctx1.get(num); // editor will know x is number
// 슬라이스 내부 값 업데이트
ctx1.set(num, x + 1);
// this line will have an error since num slice only accept number
// 타입 오류
ctx.set(num, 'string')
ctx1.get(num); // => 1
ctx2.get(num); // => still 0
Full Code (전체 코드 다시보기)
type Ctx = Map<symbol, Slice>;
type Slice<T = unknown> = {
id: symbol;
set: (value: T) => void;
get: () => T;
};
type Metadata<T> = {
id: symbol;
(ctx: Ctx): Slice<T>;
};
const createSlice = <T>(defaultValue: T): Metadata<T> => {
const id = Symbol("Slice");
const metadata = (ctx: Ctx) => {
let inner = defaultValue;
const slice: Slice<T> = {
id,
set: (next) => {
inner = next;
},
get: () => inner
};
ctx.set(id, slice as Slice);
return slice;
};
metadata.id = id;
return metadata;
};
const createCtx = () => {
const map: Ctx = new Map();
const getSlice = <T>(metadata: Metadata<T>): Slice<T> => {
const value = map.get(metadata.id);
if (!value) {
throw new Error("Slice not injected");
}
return value as Slice<T>;
};
return {
inject: <T>(metadata: Metadata<T>) => metadata(map),
get: <T>(metadata: Metadata<T>): T => getSlice(metadata).get(),
set: <T>(metadata: Metadata<T>, value: T): void => {
getSlice(metadata).set(value);
}
};
};
여담
Slice는 recoil/jotai과 매우 유사한 자료구조이다.
메타데이터와 컨텍스트를 하나로 합치면 아톰이 된다.
'FrontEnd' 카테고리의 다른 글
Recoil 배경 및 기본 알아보기 (0) | 2022.05.14 |
---|---|
XState 상태 기계로 SWR 구현하기 (0) | 2022.05.05 |
XState와 React를 활용한 기본 UI 예제 구현(7-GUIs) (0) | 2022.05.05 |
XState : 타입스크립트와 같이 활용하기 (0) | 2022.05.05 |
XState : 컨텍스트와 이벤트 모델링 (0) | 2022.05.05 |