권한관리 문제점

  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

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 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
  • 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
@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 클래스에 적용
#371

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)
  })
})
#368
  • 검색 매개변수별로 데이터 유효성 검증
  • useSearchParamsZod를 활용해 모든 검색 매개변수를 병합하고 유효성 검증
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,
  }
}
#367

취소 가능한 fetch 요청을 생성하는 createCancelableFetch 함수 구현

/**
 * 취소 가능한 fetch 요청
 *
 * @param url - 요청 URL
 * @param options - fetch 옵션
 * @returns {run, cancel} - run: 요청 실행 함수, cancel: 요청 취소 함수
 *
 * @example
 *
 * ```ts
 * const { run, cancel } = createCancelableFetch('/api/data')
 *
 * run()
 *   .then((data) => console.log('Fetched data:', data))
 *   .catch((err) => console.error('Error or canceled:', err))
 *
 * cancel()
 * ```
 */
function createCancelableFetch(url: string, options: RequestInit = {}) {
  const abortController = new AbortController()

  const run = async () => {
    const response = await fetch(url, {
      ...options,
      signal: abortController.signal,
    })

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    return response.json()
  }

  const cancel = () => abortController.abort()

  return {
    run,
    cancel,
  }
}
  • AbortController를 사용하여 요청을 중단할 수 있는 기능 제공
  • run 함수로 fetch 요청을 실행하고, cancel 함수로 요청을 취소할 수 있음
  • 요청 실패 시 오류를 처리하고 예외를 발생시킴

#360
19 중 2페이지