for file in *.md; do
new_name="${file#* }"
mv "$file" "$new_name"
done
| 개념 | JavaScript | Bash |
| ----------- | --------------- | ---------------------- |
| 반복 | `for...of` | `for...in...do...done` |
| 문자열 분리 | `split(' ')[1]` | `${var#* }` |
| 문자열 치환 | `replace()` | `${var/old/new}` |
| 파일 조작 | `fs.rename()` | `mv` |
| 출력 | `console.log()` | `echo` |
Parameter Expansion
file="351 529.md"
${file#* } # 앞에서 "* " 제거 → "529.md"
${file##* } # greedy
${file% *} # 뒤에서 " *" 제거 → "351"
${file%%.*} # greedy → "351 529"
${file/old/new} # 첫 번째 치환
${file//old/new} # 전체 치환
#은 앞(키보드에서 $보다 왼쪽), %는 뒤. 하나면 가장 짧게, 둘이면 가장 길게 맞는 부분을 지운다.1
따옴표
mv $file $new_name # ❌ 공백 있으면 깨짐
mv "$file" "$new_name" # ✅
디버깅
set -x # 실행 명령어 출력
set -e # 에러 시 중단
Footnotes
-
Shell Parameter Expansion - Bash Reference Manual: “If the pattern matches the beginning of the expanded value of parameter, then the result of the expansion is the expanded value of parameter with the shortest matching pattern (the ‘#’ case) or the longest matching pattern (the ‘##’ case) deleted.” ↩
콜백 → Promise → async/await
// 콜백
fetchData(() => {
processData(() => {
displayData()
})
})
// Promise 체인
fetchData().then(processData).then(displayData)
// async/await
async function main() {
const data = await fetchData()
const processed = await processData(data)
await displayData(processed)
}
Promise는 모나드처럼 동작 — then 은 콜백이 Promise 를 돌려주면 풀어서 잇는다(flatMap). 값을 돌려주면 감싼다(map). 한 메서드가 둘 다 해서 Promise<Promise<T>> 는 만들어지지 않는다.1
Footnotes
-
Promise.prototype.then() — MDN: “The value returned from then() is resolved in the same way as Promise.resolve(). This means thenable objects are supported, and if the return value is not a promise, it’s implicitly wrapped in a Promise and then resolved.” ↩
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 {}
모나드 입문
모나드 = 복잡한 처리를 숨기면서 연속된 연산을 가능하게 하는 패턴
- 타입 래퍼 - 값을 감싸는 구조 (예:
NumberWithLogs) - 래핑 함수 (unit/return) - 값을 모나드로 감쌈
- 바인딩 함수 (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이 바인딩 함수 역할.
단 Promise 는 근사다 — thenable 을 자동으로 펼쳐서 left identity(unit(x).bind(f) === f(x))가 깨진다.1
const v = { then: (r) => r(42) }
const f = (x) => Promise.resolve([x])
f(v) // → [v]
Promise.resolve(v).then(f) // → [42]
Footnotes
-
Promise.resolve() - JavaScript | MDN: “This function flattens nested layers of promise-like objects (e.g., a promise that fulfills to a promise that fulfills to something) into a single layer — a promise that fulfills to a non-thenable value.” ↩
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
}
두 요소 스크롤 동기화
const box1 = document.getElementById('box1')
const box2 = document.getElementById('box2')
let ignore = null
function sync(source, target) {
if (ignore === source) { ignore = null; return } // 내가 일으킨 scroll 이벤트는 한 번 건너뛴다
const ratio = source.scrollLeft / (source.scrollWidth - source.clientWidth)
const next = Math.round(ratio * (target.scrollWidth - target.clientWidth))
if (target.scrollLeft === next) return // 값이 안 바뀌면 이벤트도 안 온다 — ignore 를 세우면 다음 사용자 스크롤을 삼킨다
ignore = target
target.scrollLeft = next
}
box1.addEventListener('scroll', () => sync(box1, box2))
box2.addEventListener('scroll', () => sync(box2, box1))
scroll 이벤트는 scrollLeft 를 넣는 순간이 아니라 다음 렌더링 때 온다1 — 동기 플래그(isSyncing = true … false)는 이벤트가 오기 전에 이미 풀려 있어서 막지 못한다. 그래서 “다음에 올 이벤트 하나”를 표시해 둔다.
Footnotes
-
CSSOM View — Scrolling events: “Whenever an element gets scrolled (whether in response to user interaction or by an API), the user agent must run these steps: … Append (element, “scroll”) to doc’s pending scroll events.“ — 실제 발사는 이벤트 루프의 렌더링 단계(“run the scroll steps”)에서 한다. ↩
현재 시간부터 목표 시간까지 남은 시간 계산 — Date 끼리 빼면 숫자 문맥에서 타임스탬프(ms)로 바뀌어 차가 밀리초로 나온다.1
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 }
Footnotes
-
Date — MDN: “The valueOf() and Symbol.toPrimitive (when passed “number”) methods — which are automatically called in number coercion — return the timestamp, causing Date objects to behave like their timestamps when used in number contexts.“ ↩
turf.js로 좌표가 영역 내에 있는지 확인 — GeoJSON 좌표는 [경도, 위도] 순이고, 폴리곤 고리는 첫 점으로 닫는다.1
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
Footnotes
-
RFC 7946 — GeoJSON: “The first two elements are longitude and latitude, or easting and northing, precisely in that order” / §3.1.6: “The first and last positions are equivalent, and they MUST contain identical values” ↩
간단한 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
},
}
}
react-is로 children에서 특정 컴포넌트 필터링
react-is는 React 공식 패키지로, React 요소 타입 확인 유틸리티. typeof나 instanceof로는 React.memo(), forwardRef() 등을 구분할 수 없어서 필요함. 다만 아래 코드가 쓰는 ReactIs.isElement 는 React.isValidElement 와 결과가 같다 — 이 코드에선 react-is 없이 isValidElement 로 충분하다.
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>
)
}