import * as React from 'react'

type ContainerProps = {
  children: typeof Body
}

type BodyProps = {
  id: string
}

function Container({ children }: ContainerProps) {
  const id = '1234'

  return <>{children({ id })}</>
}

export function Body({ id }: BodyProps) {
  return <>{id}</>
}

export function Page() {
  return <Container>{Body}</Container>
}

테스트를 좀 더 쉽게 하려면 컨테이너/프레젠테이션 패턴이 여전히 유효하다고 생각한다.

#386

https://janhesters.com/blog/unleash-javascripts-potential-with-functional-programming

/**
 * - 인자 수 확인: `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)

  1. 함수 조합(Composition): 작은 함수들을 조합하여 새로운 함수를 만들며, 예를 들어 compose(f, g, h)f(g(h(x)))로 실행
  2. compose: 오른쪽에서 왼쪽으로 함수를 실행하며, compose(square, double)(3)은 36을 반환
  3. pipe: 왼쪽에서 오른쪽으로 함수를 실행하여, pipe(double, square)(3)의 결과는 36
  4. 커링(Currying): f(a, b, c)f(a)(b)(c) 형태로 변환하고, 예를 들어 add(1)(2)(3)의 결과는 6
  5. 부분 적용(Partial Application): 일부 인자만 미리 적용하여 새로운 함수를 만들 수 있으며, const double = multiply(2, _)로 정의
  6. 포인트-프리 스타일: 변수를 사용하지 않고 함수를 조합하여 작성하며, 예를 들면 compose(square, double)와 같은 형태
  7. 데이터 마지막 원칙(Data Last): 데이터를 마지막 인자로 배치하여 조합성을 높이는데, 예를 들어 const halve = divideDataLast(2)와 같이 사용

#385

https://techblog.lycorp.co.jp/ko/check-mp4-file-has-audio-using-filereader-in-front-end

  • 프런트엔드(클라이언트)에서 MP4 파일의 오디오 존재 여부를 확인하기
  • 브라우저 API 사용 시 호환성 문제 발생 (Chrome, Safari, Firefox 각각 다른 API 사용)
    • MP4 파일의 구조를 분석하는 방법으로 방향 전환
      • hdlr 아톰에서 오디오 존재 여부를 나타내는 Component subtype 필드가 soun일 경우 오디오가 존재할 것임.
      • MP4 파일의 특정 바이트를 선택적으로 요청하기 위해 Range 필드 사용.
      • 다양한 파일 크기에 따라 ‘soun’의 예상 위치를 조사하고, 적절한 범위 값 설정.
  • 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의 중요성을 깨달았다.

#383

https://www.figma.com/blog/how-we-rolled-out-our-own-permissions-dsl-at-figma/

권한관리 문제점

  1. 불필요한 복잡성과 디버깅의 어려움
  2. 계층적 권한의 비효율성
  3. 데이터베이스 부하
  4. 여러 개의 진실 소스(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
#382

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
#381

https://blog.jez.io/intro-elim/

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

  • interfaceclass를 통해 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())
}
#380

1. 왜 테스트가 고양이 목에 방울 달기인가?

  • 이론적으로는 완벽한 해결책이지만, 실천은 어렵다 테스트를 통해 코드의 품질을 높이고, 버그를 줄이며, 리팩토링을 쉽게 할 수 있다는 점은 모두 동의한다. 그러나 실제로 테스트를 작성하고 유지하려면 시간, 노력, 그리고 팀의 협력이 필요하다.
  • 책임 소재의 문제 마치 고양이 목에 방울을 달 사람이 필요하듯이, 테스트를 누가 작성하고, 누가 관리할지를 명확히 하지 않으면 테스트는 점점 방치된다.

2. 테스트를 도입하면서 겪는 흔한 장애물

  • 시간과 비용에 대한 부담 특히 프로젝트 초기 단계에서는 빠르게 기능을 구현하는 것이 더 중요하게 여겨져 테스트 작성이 후순위로 밀리게 되고 이후 기능이 복잡해지면 테스트를 도입하기가 더 어려워진다.
  • 문화적 장벽 팀 전체가 테스트의 중요성에 대해 공감하지 않으면 테스트는 단순히 “추가적인 업무”로 인식된다. 테스트가 “일반적인 코드 작성 과정의 일부”로 자리 잡지 못하면 자연스럽게 테스트 작성은 미뤄지게 되는 것 이다.
  • 기술적 장벽 테스트를 작성하기 위해 필요한 기술이나 도구를 충분히 익히지 못한 팀원들이 많을 경우, 테스트 작성 자체가 어렵게 느껴질 수 있다.

3. 고양이에게 방울을 다는 방법: 테스트 도입 전략

  • 작게 시작하기 처음부터 모든 코드에 테스트를 적용하려고 하기보다, 중요하거나 변화 가능성이 높은 부분부터 테스트를 작성한다.
  • 자동화를 활용하기
  • 테스트 작성을 개발 과정의 일부로 만들기
  • 기존 코드에 테스트 추가하기
    • 리팩토링과 병행하여 테스트를 작성.
    • 새로운 버그가 발생한 영역에 대해 회귀 테스트를 추가.
  • 테스트 유지보수 비용을 낮추기

4. 테스트의 효과는 신뢰를 기반으로 한다


5. 테스트는 방울을 단 이후가 더 중요하다

고양이 목에 방울을 다는 데 성공했다고 해서 끝이 아니다. 테스트는 “설치”가 아니라 “유지”가 핵심.

  • 테스트를 지속적으로 업데이트
  • 테스트를 실행 가능한 상태로 유지
  • 팀 내 테스트 문화 확립
#379
type FetchWithAbortResult =
  | { response: Response; abort: () => void }
  | { error: NetworkError | HttpError }

class NetworkError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'NetworkError'
  }
}

class HttpError extends Error {
  constructor(public status: number, message?: string) {
    super(message || `HTTP error: ${status}`)
    this.name = 'HttpError'
  }
}

async function fetchWithAbortController(
  url: string,
  options: RequestInit = {}
): Promise<FetchWithAbortResult> {
  const controller = new AbortController()
  const { signal } = controller

  try {
    const response = await fetch(url, { ...options, signal })

    if (!response.ok) {
      throw new HttpError(response.status)
    }

    return {
      response,
      abort: () => controller.abort(),
    }
  } catch (error) {
    if (error.name === 'AbortError') {
      return {
        error: new NetworkError(),
      }
    }

    const e =
      error instanceof HttpError ? error : new NetworkError(error.message)

    return {
      error: e,
    }
  }
}
  • API 요청 시 발생할 수 있는 오류 문제의 필요성
  • AbortController 사용과 사용자 정의 에러 클래스를 통한 에러 관리
  • HTTP 요청과 네트워크 오류를 안정적으로 처리
#378

https://github.com/sindresorhus/type-fest/blob/49605b9770a344bac28ce8504045541dc0bdbd4b/source/basic.d.ts#L38-L68

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로 구성
#376

https://programmingarehard.com/2025/01/13/maybe-dont-navigate-1.html/

  • 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>
  )
}
#373
18 중 1페이지