TypeScript Maybe Monad 구현

type Maybe<T> = Just<T> | Nothing

class Just<T> {
  constructor(public value: T) {}
  bind<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
    return fn(this.value)
  }
}

class Nothing {
  bind<U>(fn: (value: any) => Maybe<U>): Maybe<U> {
    return this
  }
}

const nothing = new Nothing()

function safeDivide(x: number, y: number): Maybe<number> {
  return y === 0 ? nothing : new Just(x / y)
}

new Just(10).bind((x) => safeDivide(x, 2)) // Just { value: 5 }
new Just(10).bind((x) => safeDivide(x, 0)) // Nothing {}
#535

모나드 입문

모나드 = 복잡한 처리를 숨기면서 연속된 연산을 가능하게 하는 패턴

  1. 타입 래퍼 - 값을 감싸는 구조 (예: NumberWithLogs)
  2. 래핑 함수 (unit/return) - 값을 모나드로 감쌈
  3. 바인딩 함수 (bind/flatMap) - 래핑된 값에 함수 적용
interface NumberWithLogs {
  result: number
  logs: string[]
}

function wrapWithLogs(n: number): NumberWithLogs {
  return { result: n, logs: [] }
}

function runWithLogs(
  input: NumberWithLogs,
  transform: (n: number) => NumberWithLogs
): NumberWithLogs {
  const next = transform(input.result)
  return { result: next.result, logs: [...input.logs, ...next.logs] }
}

Option, Promise도 모나드. then이 바인딩 함수 역할.

#534

Maybe Monad로 null 체크 체이닝

type Maybe<T> = T | null

const Maybe = {
  of: <T>(value: T): Maybe<T> => (value != null ? value : null),
  map: <T, U>(m: Maybe<T>, fn: (v: T) => U): Maybe<U> =>
    m != null ? Maybe.of(fn(m)) : null,
}

// 사용 예: DOM 요소 찾아서 스크롤
Maybe.of(scrollElement.current)
  .map((root) => root.querySelector(`#${id}`))
  .map((target) => target.getClientRects()[0])
  .map((rect) => {
    root.scrollLeft = rect.left
  })

Generator로 early return 패턴도 가능

function* handleScroll(id) {
  const root = scrollElement.current
  if (!root) return
  const target = root.querySelector(`#${id}`)
  if (!target) return
  yield target.getClientRects()[0]?.left ?? 0
}
#533

두 요소 스크롤 동기화

const box1 = document.getElementById('box1')
const box2 = document.getElementById('box2')

let isSyncing = false

function syncScroll(source, target, sourceWidth, targetWidth) {
  const ratio = source.scrollLeft / (source.scrollWidth - sourceWidth)
  target.scrollLeft = ratio * (target.scrollWidth - targetWidth)
}

box1.addEventListener('scroll', () => {
  if (!isSyncing) {
    isSyncing = true
    syncScroll(box1, box2, 1280, 640)
    isSyncing = false
  }
})

box2.addEventListener('scroll', () => {
  if (!isSyncing) {
    isSyncing = true
    syncScroll(box2, box1, 640, 1280)
    isSyncing = false
  }
})

isSyncing 플래그로 무한 이벤트 루프 방지

#532

현재 시간부터 목표 시간까지 남은 시간 계산

function calculateRemainingTime(targetTime) {
  const now = new Date()
  const [h, m, s] = targetTime.split(':').map(Number)

  const target = new Date()
  target.setHours(h, m, s)

  const diff = target - now
  if (diff < 0) return null

  return {
    hours: Math.floor(diff / 3600000) % 24,
    minutes: Math.floor(diff / 60000) % 60,
    seconds: Math.floor(diff / 1000) % 60,
  }
}

calculateRemainingTime('18:30:00')
// { hours: 2, minutes: 15, seconds: 30 }
#531

turf.js로 좌표가 영역 내에 있는지 확인

const turf = require('@turf/turf')

const polygon = turf.polygon([
  [
    [-73.981, 40.768],
    [-73.981, 40.764],
    [-73.975, 40.764],
    [-73.975, 40.768],
    [-73.981, 40.768], // 닫기
  ],
])

const point = turf.point([-73.978, 40.766])

turf.booleanPointInPolygon(point, polygon) // true/false

npm install @turf/turf

#530

간단한 Redux 스타일 Store 구현

type Reducer<S, A> = (state: S, action: A) => S
type Listener<S> = (state: S) => void

interface Store<S, A> {
  getState: () => S
  subscribe: (listener: Listener<S>) => () => void
  dispatch: (action: A) => A
}

function createStore<S, A>(
  reducer: Reducer<S, A>,
  preloadedState: S
): Store<S, A> {
  let currentState = preloadedState
  let listeners: Listener<S>[] = []

  return {
    getState: () => currentState,
    subscribe: (listener) => {
      listener(currentState)
      listeners.push(listener)
      return () => {
        listeners = listeners.filter((l) => l !== listener)
      }
    },
    dispatch: (action) => {
      currentState = reducer(currentState, action)
      listeners.forEach((l) => l(currentState))
      return action
    },
  }
}
#529

react-is로 children에서 특정 컴포넌트 필터링

react-is는 React 공식 패키지로, React 요소 타입 확인 유틸리티. typeofinstanceof로는 React.memo(), forwardRef() 등을 구분할 수 없어서 필요함. 라이브러리 개발, children 타입 검사에 필수.

import * as ReactIs from 'react-is'

// 특정 컴포넌트 타입인지 확인
function isComponentType<T>(element: ReactNode, Type: ComponentType<T>) {
  return ReactIs.isElement(element) && element.type === Type
}

// children에서 특정 컴포넌트만 필터링
function filterChildren<T>(children: ReactNode, Type: ComponentType<T>) {
  return Children.toArray(children).filter(
    (child): child is ReactElement<T> =>
      ReactIs.isElement(child) && child.type === Type
  )
}

// 사용: Layout에서 Header, Content, Footer 분리
const Layout = ({ children }) => {
  const headers = filterChildren(children, Header)
  const contents = filterChildren(children, Content)

  return (
    <div>
      <div className="header">{headers}</div>
      <div className="content">{contents}</div>
    </div>
  )
}
#528
// 기본
@primary: #3498db;
.button { color: @primary; }

// 보간 (선택자, 속성, URL에서)
@component: 'button';
.@{component} { ... }           // .button
background-@{property}: blue;   // background-color: blue
url('@{path}/icon.svg')

// 변수를 이용한 변수
@theme: 'primary';
color: @@theme;  // @primary 값

// 맵 (LESS 3.5+)
@colors: { primary: #3498db; danger: #e74c3c; };
color: @colors[primary];

// CSS 변수 조합
:root { --primary: @primary; }
.el { color: var(~'--@{prefix}-color'); }

  • ~"" 이스케이프 (문자열 그대로 출력)
  • @{} 보간 (변수를 문자열로 치환)
  • @@var 변수를 이용한 변수 참조

https://lesscss.org/features/#variables-feature

#526

gulp.src 파일 리스트 디버깅

// 1. gulp-debug (가장 간단)
gulp.src('src/**/*.js').pipe(require('gulp-debug')({ title: 'Files:' }))

// 2. through2 (상세 정보)
const through2 = require('through2')

gulp.src('src/**/*.js').pipe(
  through2.obj((file, enc, cb) => {
    console.log(file.relative)
    cb(null, file)
  })
)

// 3. glob 패턴 사전 확인
require('glob')('src/**/*.js', (err, files) => console.log(files))

// 조건부 디버깅: `NODE_ENV=debug gulp build`
const gulpif = require('gulp-if')

gulp.src('src/**/*.js').pipe(gulpif(process.env.NODE_ENV === 'debug', debug()))
#525
31 중 6페이지