/**
* - 인자 수 확인: `curry` 함수는 전달된 함수 `fn`의 인자 수를 확인
* - 인자 수가 충분한 경우: 만약 `args`의 길이가 `fn`의 인자 수 이상이면, `fn`을 호출하고 결과를 반환
* - 인자 수가 부족한 경우: 그렇지 않으면 추가 인자(`moreArgs`)를 받을 수 있는 새 함수를 반환. 이 함수는 기존 인자(`args`)와 새로운 인자(`moreArgs`)를 합쳐 다시 `curry(fn)`을 호출하여 최종적으로 인자가 충분할 때까지 이 과정을 반복
*/
const curry =
(fn: Function) =>
(...args: any[]) =>
args.length >= fn.length
? fn(...args)
: (...moreArgs: any[]) => curry(fn)(...args, ...moreArgs)
/**
* - `...fns: Function[]` 여러 개의 함수를 인자로 받는다
* - 내부에서 `reduce` 메서드를 사용하여 함수 배열을 순회 초기값으로 `x`를 사용하며, 각 함수 `fn`을 차례로 호출하여 결과를 다음 함수로 전달
*/
const pipe =
(...fns: Function[]) =>
(x: any) =>
fns.reduce((acc, fn) => fn(acc), x)
/**
* - `...fns`: 여러 개의 함수를 매개변수로 받음
* - `(x)`: 초기값 `x`를 인자로 받아 결과를 리턴
* - `fns.reduceRight(...)`: 배열의 오른쪽부터 왼쪽으로 각 함수를 적용합니다. `y`는 이전 함수의 결과이며, `f`는 현재 함수
* - `f(y)`: 현재 함수 `f`를 이전 함수의 결과 `y`에 적용
*/
const compose =
(...fns) =>
(x) =>
fns.reduceRight((y, f) => f(y), x)
- 함수 조합(Composition): 작은 함수들을 조합하여 새로운 함수를 만들며, 예를 들어
compose(f, g, h)
는f(g(h(x)))
로 실행 compose
: 오른쪽에서 왼쪽으로 함수를 실행하며,compose(square, double)(3)
은 36을 반환pipe
: 왼쪽에서 오른쪽으로 함수를 실행하여,pipe(double, square)(3)
의 결과는 36- 커링(Currying):
f(a, b, c)
를f(a)(b)(c)
형태로 변환하고, 예를 들어add(1)(2)(3)
의 결과는 6 - 부분 적용(Partial Application): 일부 인자만 미리 적용하여 새로운 함수를 만들 수 있으며,
const double = multiply(2, _)
로 정의 - 포인트-프리 스타일: 변수를 사용하지 않고 함수를 조합하여 작성하며, 예를 들면
compose(square, double)
와 같은 형태 - 데이터 마지막 원칙(Data Last): 데이터를 마지막 인자로 배치하여 조합성을 높이는데, 예를 들어
const halve = divideDataLast(2)
와 같이 사용
- 프런트엔드(클라이언트)에서 MP4 파일의 오디오 존재 여부를 확인하기
- 브라우저 API 사용 시 호환성 문제 발생 (Chrome, Safari, Firefox 각각 다른 API 사용)
- FileReader API를 사용하여 서버에서 응답받은 바이너리 데이터를 읽고, 이를 분석해 오디오 여부 확인.
- 오디오 정보가 발견되지 않으면 추가 데이터 요청하여 확인 범위를 증가시켜 반복.
await ffmpeg.writeFile('input.mp4', await fetchFile(file))
await ffmpeg.ffprobe(['-i', 'input.mp4', '-show_streams', '-o', 'output.txt'])
const data = await ffmpeg.readFile('output.txt')
…하지만 ffmpeg의 중요성을 깨달았다.
권한관리 문제점
- 불필요한 복잡성과 디버깅의 어려움
- 계층적 권한의 비효율성
- 데이터베이스 부하
- 여러 개의 진실 소스(Sources of Truth)
해결 방법
- JSON 직렬화 가능한 DSL을 정의하여 정책을 표현.
- TypeScript 기반 평가 엔진을 구현하여 데이터를 기반으로 정책을 평가.
- 기존 AST 기반보다 단순한 구조를 가진다.
type FieldName = string
type Value = string | boolean | number | Date | null
type ExpressionArgumentRef = {
type: 'field'
ref: FieldName
}
type BinaryExpressionDef = [
FieldName,
'=' | '<>' | '>' | '<' | '>=' | '<=',
Value | ExpressionArgumentRef
]
type OrExpressionDef = {
or: ExpressionDef[]
}
type AndExpressionDef = {
and: ExpressionDef[]
}
type ExpressionDef = BinaryExpressionDef | OrExpressionDef | AndExpressionDef
const binaryExpression = ['file.id', '<>', null] satisfies ExpressionDef
const andExpression = {
and: [
['file.id', '<>', null],
['team.permission', '=', 'open'],
['project.deleted_at', '<>', null],
],
} satisfies ExpressionDef
const orExpression = {
or: [
['team.id', '<>', null],
['file.id', '=', { type: 'field', ref: 'team.id' }],
],
} satisfies ExpressionDef
static create
패턴이 유용한 상황
대부분의 경우 생성자를 사용하는 것이 더 직관적이고 단순하지만, 다음과 같은 상황에서는 static create
가 더 적합
- 생성 로직이 복잡하거나 조건부 처리가 필요할 때
- 팩토리 패턴을 간소화하고 싶을 때
- 생성 과정에서의 불변성을 강화하고 싶을 때
- 다른 타입을 반환하거나, 실패 가능성을 명시적으로 처리하고 싶을 때
1. 생성 로직 커스터마이징
- 생성자가 단순히 필드 초기화만 수행하는 경우는 일반 생성자로 충분하지만, 생성 과정에서 유효성 검사, 값 변환, 혹은 복잡한 비즈니스 로직이 필요하다면
static create
패턴이 더 적합 - 예를 들어, 유효성 검사를 통과하지 못하면 객체를 반환하지 않거나, 에러를 던질 수 있음
- 생성 과정에서의 제약 조건을 명시적으로 처리할 수 있음
class User {
private constructor(private readonly name: string) {}
static create(name: string): User | null {
if (!name || name.length < 3) {
console.error('Invalid name!')
return null
}
return new User(name)
}
}
const validUser = User.create('John') // 성공
const invalidUser = User.create('Jo') // 실패, null 반환
2. 팩토리 패턴의 간소화
static create
를 간단한 팩토리 메서드로 활용해서 객체 생성의 복잡성을 숨기기- 객체의 생성 방식이 호출자의 관점에서는 중요하지 않다면 내부 로직을 캡슐화할 수 있다
- 이런 방식은 타입에 따라 다른 구현체를 생성해야 하는 경우
class Shape {
static create(type: 'circle' | 'rectangle', size: number): Shape {
if (type === 'circle') {
return new Circle(size)
} else {
return new Rectangle(size)
}
}
}
3. 불변성 및 제약 강화
- 생성자를
private
로 설정하고,static
메서드만을 통해 객체를 생성하도록 제한하면 객체의 불변성을 더 강하게 유지할 수 있다 - 특정 조건에 따라 객체의 생성을 제한하거나 실패하도록 설계할 수 있다
class CardNumber {
private constructor(private readonly number: string) {}
static create(number: string): CardNumber | null {
if (!CardNumber.isValid(number)) {
return null
}
return new CardNumber(number)
}
private static isValid(number: string): boolean {
return number.length === 16
}
}
const card = CardNumber.create('1234567812345678') // 유효한 카드
const invalidCard = CardNumber.create('123') // null
4. 다른 타입 반환
생성자는 항상 해당 클래스의 인스턴스만 반환할 수 있다. 반면, static create
는 경우에 따라 다른 타입의 값을 반환할 수 있다.
class Result<T> {
private constructor(
public readonly value: T | null,
public readonly error: string | null
) {}
static success<T>(value: T): Result<T> {
return new Result(value, null)
}
static failure<T>(error: string): Result<T> {
return new Result(null, error)
}
}
class User {
private constructor(private readonly name: string) {}
static create(name: string): Result<User> {
if (!name || name.length < 3) {
return Result.failure('Name must be at least 3 characters long')
}
return Result.success(new User(name))
}
}
const result = User.create('Jo') // 실패 결과 반환
5. 객체 재사용
동일한 인스턴스를 재사용하고 싶을 때도 static create
패턴이 적합하다. 예를 들어, 싱글톤(Singleton) 패턴처럼 동작하거나 캐싱된 객체를 반환할 수 있다.
class Configuration {
private static instance: Configuration
private constructor(public readonly settings: Record<string, string>) {}
static create(): Configuration {
if (!Configuration.instance) {
Configuration.instance = new Configuration({ mode: 'production' })
}
return Configuration.instance
}
}
const config1 = Configuration.create()
const config2 = Configuration.create()
console.log(config1 === config2) // true
Every type is defined by its intro and elim forms
- Intro forms: 타입의 인스턴스를 어떻게 “생성”하는지 정의.
- Elim forms: 생성된 타입 인스턴스를 어떻게 “사용”하거나 “해체”할지 정의.
타입을 정의할 때, 생성과 사용의 명확한 경계를 설정해서 Intro/Elim 설계를 명시적으로 표현하기
class Rectangle {
private constructor(public width: number, public height: number) {}
static create(width: number, height: number) {
if (width <= 0 || height <= 0) {
return null
}
return new Rectangle(width, height)
}
getArea() {
return this.width * this.height
}
}
const rect = Rectangle.create(10, 20)
if (rect) {
console.log(rect.getArea())
}
Types are not their elim forms
interface
와class
를 통해 Intro/Elim 모두를 명시적으로 정의- 팩토리 메서드 같은 패턴을 활용해 생성 방식을 추상화
interface Shape {
getArea(): number
}
class Circle implements Shape {
constructor(private radius: number) {}
getArea() {
return Math.PI * this.radius ** 2
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getArea() {
return this.width * this.height
}
}
function createShape(type: 'circle' | 'rectangle', ...args: number[]) {
if (type === 'circle' && args.length === 1) {
return new Circle(args[0])
}
if (type === 'rectangle' && args.length === 2) {
return new Rectangle(args[0], args[1])
}
return null
}
const shape = createShape('circle', 10)
if (shape) {
console.log(shape.getArea())
}
type JsonPrimitive = string | number | boolean | null
type JsonObject = { [Key in string]: JsonValue } & {
[Key in string]?: JsonValue | undefined
}
type JsonArray = JsonValue[] | readonly JsonValue[]
type JsonValue = JsonPrimitive | JsonObject | JsonArray
예전에 JSON 타입 정의가 필요해서 찾아봤던 내용
JsonObject
: 문자열 키와 JsonValue 타입의 값을 가진 JSON 객체를 정의JsonArray
: JsonValue 타입의 요소를 포함하는 JSON 배열을 정의JsonPrimitive
: 문자열, 숫자, 불린, 또는 null과 같은 유효한 JSON 기본 값을 정의JsonValue
: 유효한 JSON 값을 나타내며, JsonPrimitive, JsonObject, 또는 JsonArray로 구성
navigate(-1)
의 위험성: 브라우저 히스토리에서 이전 위치로 이동, 앱 내부 네비게이션과 혼란을 초래할 수 있음- 대신
Link
컴포넌트의state
속성을 활용하여 안전하게 앱 내에서의 “Back” 네비게이션 구현 가능 - 사용자에게 현재 URL을 반환하는 커스텀 훅
useCurrentURL
구현 및 재사용의 용이성을 제공하는useBackNavigation
훅 정의
function PreserveStateLink(props) {
const location = useLocation()
const currentURL = location.pathname + location.search
return (
<Link state={{ back: currentURL }} {...props}>
{children}
</Link>
)
}
function BackLink() {
const navigate = useNavigate()
const location = useLocation()
const handleClick: LinkProps['onClick'] = (e) => {
const back = location.state?.back
if (back) {
e.preventDefault()
navigate(back)
}
}
return (
<Link to="/todos" onClick={handleBack}>
Back
</Link>
)
}
@value b from "./b.module.css";
.root {
color: aquamarine;
}
.root :global(.b) {
text-decoration: line-through;
}
CSS 모듈에서 변수를 값으로 내보내고 사용하는 방법
- PostCSS와
postcss-modules-values
플러그인을 사용하여 CSS 모듈 내에서 변수 값 내보내기 지원 - 색상 변수를 정의하는 파일 생성
- 변수 선언:
@value
구문 사용
- 변수 선언:
- 다른 CSS 모듈 파일에서 해당 변수를 가져와서 사용
- 변수 가져오기 및 CSS 클래스에 적용
URL.parse() 메서드를 활용하여 URL 객체를 생성하고 처리하는 방법
URL.parse(url)
메서드는 주어진 URL에 따라 새 URL 객체를 생성- 유효하지 않은 URL 값이 주어질 경우
null
을 반환 - 두 번째 파라미터 base는 상대 URL을 해석하기 위한 기준 URL로 사용되며, 이를 통해 URL의 경로가 올바르게 조정됨
- URL 객체나 다른 문자열을 파라미터로 사용할 수 있으며, 내부에서 문자열로 변환됨
describe('null 입력에 대한 URL API 동작 테스트', () => {
test('new URL(null)은 TypeError를 발생시킨다', () => {
expect(() => new URL(null)).toThrow(TypeError)
})
test('URL.canParse(null)은 false를 반환한다', () => {
expect(URL.canParse(null)).toBe(false)
})
test('URL.parse(null)은 null을 반환한다', () => {
expect(() => URL.parse(null)).toBe(null)
})
})
- 검색 매개변수별로 데이터 유효성 검증
useSearchParams
와Zod
를 활용해 모든 검색 매개변수를 병합하고 유효성 검증
import { useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import { z } from 'zod'
const CombinedSchema = PaginationSchema.merge(FilterSchema)
.merge(SortSchema)
.merge(SearchSchema)
.partial()
export function useSearchParamsWithSchema() {
const [searchParams, setSearchParams] = useSearchParams()
const parsedParams = useMemo(() => {
// searchParams를 객체로 변환
const paramsObject = Object.fromEntries(searchParams.entries())
// 결합된 스키마로 유효성 검사 및 파싱
const result = CombinedSchema.safeParse(paramsObject)
if (result.success) {
return result.data
}
console.error(result.error)
return {}
}, [searchParams])
/**
* 새로운 검색 매개변수로 업데이트하는 함수입니다.
*
* 1. 현재 검색 매개변수의 복사본을 생성합니다.
* 2. 새로운 매개변수를 순회하며 값을 설정하거나 삭제합니다.
* 3. 최종적으로 업데이트된 매개변수를 state에 설정합니다.
*/
const updateSearchParams = (newParams: Record<string, string>) => {
const updatedParams = new URLSearchParams(searchParams)
Object.entries(newParams).forEach(([key, value]) => {
if (value) {
// 업데이트된 매개변수로 상태 갱신
updatedParams.set(key, value)
} else {
// 값이 없으면 삭제
updatedParams.delete(key)
}
})
// 업데이트된 매개변수로 상태 갱신
setSearchParams(updatedParams)
}
return {
parsedParams,
updateSearchParams,
}
}