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>
}
테스트를 좀 더 쉽게 하려면 컨테이너/프레젠테이션 패턴이 여전히 유효하다고 생각한다.
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)
- 함수 조합(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)
와 같이 사용
https://techblog.lycorp.co.jp/ko/check-mp4-file-has-audio-using-filereader-in-front-end
- 프런트엔드(클라이언트)에서 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의 중요성을 깨달았다.
https://www.figma.com/blog/how-we-rolled-out-our-own-permissions-dsl-at-figma/
권한관리 문제점
- 불필요한 복잡성과 디버깅의 어려움
- 계층적 권한의 비효율성
- 데이터베이스 부하
- 여러 개의 진실 소스(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
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
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 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 요청과 네트워크 오류를 안정적으로 처리
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로 구성
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>
)
}