권한관리 문제점
- 불필요한 복잡성과 디버깅의 어려움
- 계층적 권한의 비효율성
- 데이터베이스 부하
- 여러 개의 진실 소스(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())
}
1. 왜 테스트가 고양이 목에 방울 달기인가?
- 이론적으로는 완벽한 해결책이지만, 실천은 어렵다 테스트를 통해 코드의 품질을 높이고, 버그를 줄이며, 리팩토링을 쉽게 할 수 있다는 점은 모두 동의한다. 그러나 실제로 테스트를 작성하고 유지하려면 시간, 노력, 그리고 팀의 협력이 필요하다.
- 책임 소재의 문제 마치 고양이 목에 방울을 달 사람이 필요하듯이, 테스트를 누가 작성하고, 누가 관리할지를 명확히 하지 않으면 테스트는 점점 방치된다.
2. 테스트를 도입하면서 겪는 흔한 장애물
- 시간과 비용에 대한 부담 특히 프로젝트 초기 단계에서는 빠르게 기능을 구현하는 것이 더 중요하게 여겨져 테스트 작성이 후순위로 밀리게 되고 이후 기능이 복잡해지면 테스트를 도입하기가 더 어려워진다.
- 문화적 장벽 팀 전체가 테스트의 중요성에 대해 공감하지 않으면 테스트는 단순히 “추가적인 업무”로 인식된다. 테스트가 “일반적인 코드 작성 과정의 일부”로 자리 잡지 못하면 자연스럽게 테스트 작성은 미뤄지게 되는 것 이다.
- 기술적 장벽 테스트를 작성하기 위해 필요한 기술이나 도구를 충분히 익히지 못한 팀원들이 많을 경우, 테스트 작성 자체가 어렵게 느껴질 수 있다.
3. 고양이에게 방울을 다는 방법: 테스트 도입 전략
- 작게 시작하기 처음부터 모든 코드에 테스트를 적용하려고 하기보다, 중요하거나 변화 가능성이 높은 부분부터 테스트를 작성한다.
- 자동화를 활용하기
- 테스트 작성을 개발 과정의 일부로 만들기
- 기존 코드에 테스트 추가하기
- 리팩토링과 병행하여 테스트를 작성.
- 새로운 버그가 발생한 영역에 대해 회귀 테스트를 추가.
- 테스트 유지보수 비용을 낮추기
4. 테스트의 효과는 신뢰를 기반으로 한다
5. 테스트는 방울을 단 이후가 더 중요하다
고양이 목에 방울을 다는 데 성공했다고 해서 끝이 아니다. 테스트는 “설치”가 아니라 “유지”가 핵심.
- 테스트를 지속적으로 업데이트
- 테스트를 실행 가능한 상태로 유지
- 팀 내 테스트 문화 확립
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,
}
}
취소 가능한 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
함수로 요청을 취소할 수 있음- 요청 실패 시 오류를 처리하고 예외를 발생시킴