@howmuchhome-web/utils-ts
프레임워크에 의존하지 않는 순수 TypeScript 유틸리티 모음입니다. 색상·날짜·숫자·문자열·객체 변환, 타입 유틸, AES 암호화 등을 제공합니다.
- 의존성:
date-fns,uuid,react(일부 파일에서 JSX 반환용). 별도 빌드 없이 소스(.ts/.tsx)를 직접 소비합니다(main: index.ts). - 사용처:
@howmuchhome-web/api,@howmuchhome-web/ui,@howmuchhome-web/domains,@howmuchhome-web/funnel,@howmuchhome-web/editor,@howmuchhome-web/common-components,@howmuchhome-web/bridge-protocol,apps/app,apps/dashboard등. - ⚠️ 전부 native/SSR-safe 는 아닙니다. 타입·순수 함수 위주지만
dom.ts/media.ts/device.ts(브라우저 전역)와aes-encryption.ts(Nodecrypto) 같은 export 는 native·SSR 에서 실패합니다 → native 주의.
import {clamp, phoneFormatter, objectKeySnakeToCamel} from '@howmuchhome-web/utils-ts';배럴(index.ts) 구조
index.ts는 다음 파일들을 re-export 합니다.
color · date · device · dom · function · media
number · object · string · type · types · array · address · aes-encryption주의: crypto.ts(NumberCodec)와 promise.ts(withMinimumDelay)는 배럴에 포함되어 있지 않습니다. 이 심볼들은 필요 시 파일 경로로 직접 import 해야 합니다.
// 배럴에 없는 심볼은 경로로 직접 import
import {NumberCodec} from '@howmuchhome-web/utils-ts/crypto';
import {withMinimumDelay} from '@howmuchhome-web/utils-ts/promise';유틸 인벤토리
색상 (color.ts)
| 심볼 | 시그니처 | 설명 |
|---|---|---|
hexToHSL | (hex: string) => {h, s, l} | HEX → HSL. 파싱 실패 시 {h:0, s:0, l:0} 반환. |
hslToHex | (h: number, s: number, l: number) => string | HSL → #rrggbb. |
generateRandomColors | (count: number) => string[] | 골든 앵글(137°) 기반으로 시각적으로 구분되는 색상 배열 생성. |
날짜 (date.ts)
date-fns(format, isSameDay, isSameYear)와 date-fns/locale의 ko를 사용합니다.
| 심볼 | 시그니처 | 설명 |
|---|---|---|
dateObjectToEngDate | (date: Date) => string | MMM D, YYYY 형식(예: JAN 3, 2026). |
getDateTimeFromDateObject | (data: Date) => string | YYYY.MM.DD 오전/오후 H시 M분. |
getDateFromDateObject | (date: Date) => string | KST(+9h) 기준 YYYY.MM.DD. |
getTimeFromDateObject | (date: Date, second = false) => string | KST 기준 HH:mm(또는 HH:mm:ss). |
getDDay | (from: Date, to: Date) => number | 두 날짜 간 D-day(일수). to는 당일 23:59:59 기준. |
getDDayFromNow | (to: Date) => number | getDDay(new Date(), to). |
displayedDatetime | (date: Date) => string | 오늘이면 HH:mm, 아니면 YY.MM.DD. |
getDisplayedCreatedAt | (date: Date) => string | 같은 날 HH:mm, 그 외 yyyy.MM.dd(date-fns). |
displayTimeLapse | (date: Date) => string | 당일이면 N시간/N분 전·방금 전, 그 외 YY.MM.DD. |
isValidBirthDate | (dateString: string) => boolean | 6자리/8자리 생년월일 유효성. 길이 검증은 별도 필요. |
convertToLocaleDate | (date: Date) => Date | 로컬 타임존 오프셋을 적용한 Date. |
참고: 파일 내부에 export 되지 않은 getDifferenceStringFromNow(상대 시간 문자열) 헬퍼가 존재하지만 외부로 노출되지 않습니다.
숫자 (number.ts)
| 심볼 | 시그니처 | 설명 |
|---|---|---|
clamp | (value, min, max) => number | 값을 [min, max]로 제한. |
absClamp | (value, max) => number | 절댓값을 max로 제한하되 부호 유지. |
hashNumber | (target: string) => number | 문자열 → 32bit 양수 해시. |
findNearestNumber | (from: number, to: number[]) => number | 배열에서 from에 가장 가까운 값. |
formatPercentage | (value: number) => string | 정수면 그대로, 소수면 소수점 2자리. NaN이면 '0'. |
문자열 (string.tsx)
.tsx 확장자로, 일부 함수가 React 엘리먼트(ReactElement/Fragment)를 반환합니다. uuid의 v4를 사용합니다.
| 심볼 | 시그니처 | 설명 |
|---|---|---|
phoneFormatter | (phone: string | null | undefined) => string | 010-1234-5678 형식으로 하이픈 삽입. |
createRandomString | (length: number) => string | 영숫자 랜덤 문자열. |
matchDigits | (str: string | number, digits: number, item = '0') => string | 자릿수 맞춰 앞을 패딩. |
splitByKeyword | (str: string, keyword: string) => string[] | 키워드 기준으로 조각·키워드를 번갈아 분할. |
textReplacer | (text, regex, callback) => ReactNode[] | 텍스트 특정 부분을 ReactNode로 래핑(하이라이트 등). |
applyLineBreak | (node) => ReactNode[] | \n을 <br/>로 치환. |
extractUrls | (text?: string) => string[] | 텍스트에서 URL 추출. |
getUUID4 | () => string | uuid v4 (재-export). |
convertNumberToKoreanString | (value: number) => string | 예: 123456789 → 1억 2345만 6789. |
getRootDomain | (hostname: string) => string | 루트 도메인 추출(localhost·IP 예외 처리). |
getFileSizeText | (size: number) => string | 바이트 → 1.5MB 형식. |
standardSortString | (a, b) => number | 숫자 우선, 그 외 localeCompare 정렬 비교기. |
isUUID4 | (str?: string) => boolean | UUID v4 형식 검증. |
정규식 상수: simpleUrlRegExp, phoneValidationRegExp, emailValidationRegExp, nameValidationRegExp(한글만), noSpecialCharacterRegExp(영숫자+한글).
문자열 정렬 보조 (array.ts)
| 심볼 | 시그니처 | 설명 |
|---|---|---|
compareMixedStringsAndNumbers | (a: string, b: string) => number | 둘 다 숫자면 수치 비교, 아니면 localeCompare. |
파일명은
array.ts지만 배열 유틸이 아니라 문자열/숫자 혼합 정렬 비교 함수 하나만 담고 있습니다. 일반적으로는 배럴에서 import 하세요.
타입 유틸 (types.ts)
react의 Dispatch를 참조합니다.
| 심볼 | 설명 |
|---|---|
DateString, UUID | string 별칭(의미 명시용). |
MaybeAsync<T> | T | Promise<T>. |
KeysMatching<T, V> | T에서 값 타입이 V인 키의 유니온. |
DeepPartial<T> | 재귀적 Partial. |
ArrayFromUnion<T> | 유니온 각 항목을 배열 유니온으로(string | number → string[] | number[]). |
OmitFromUnion<T, K> | 유니온 분배 Omit. |
ElementOfArray<T> | 배열 요소 타입 추출. |
UnionDispatch<T> | 유니온 분배 Dispatch. |
NonNullableProperties<O> / NullableProperties<O> | 각 프로퍼티를 non-null / nullable로. |
객체 (object.ts)
| 심볼 | 시그니처 | 설명 |
|---|---|---|
objectKeySnakeToCamel | (obj: any) => any | 키를 snake_case → camelCase로 in-place 변환(중첩 포함). |
objectKeyCamelToSnake | <T>(obj: T) => T | camelCase → snake_case(불변, 새 객체 반환). two_way_info는 하위를 변환하지 않음(PRESERVE_CAMEL_CASE_KEYS). |
excludeUndefinedFromObject | (obj) => obj | undefined 값 프로퍼티 제거. |
groupBy | <T>(data: T[], key: string) => {[k]: T[]} | 지정 키로 그룹핑. |
deepEqual | <T>(obj1: T, obj2: T) => boolean | 얕지 않은 재귀 동등 비교. |
objectKeySnakeToCamel은 API fetcher(packages/api/fetcher/fetch.ts)에서 서버 응답(snake_case)을 camelCase로 정규화하는 데 쓰입니다. E2E 목 핸들러에서도 요청 body 비교에 사용됩니다.
주소 (address.ts)
| 심볼 | 시그니처 | 설명 |
|---|---|---|
splitStreetAddress | (fullAddress: string | null) => {address, detailAddress} | 도로명 주소와 상세주소 분리. |
함수·비동기 (function.ts, promise.ts)
| 심볼 | 파일 | 시그니처 | 설명 |
|---|---|---|---|
waitForTimeout | function.ts | (ms: number) => Promise<void> | setTimeout Promise 래퍼. |
waitForFunction | function.ts | (callback, options?, ...args) => Promise<void> | 콜백이 true 될 때까지 폴링(timeout 기본 3000ms, period 기본 100ms). |
retryOnError | function.ts | (callback, tryLimit, delay = 1000) => Promise<T> | 실패 시 재시도. |
assertUnreachable | function.ts | (x: never) => never | 완전성(exhaustiveness) 체크용. |
withMinimumDelay | promise.ts | (promise, delay) => Promise<T> | 최소 지속시간 보장(스켈레톤 깜빡임 방지 등). 배럴 미포함, 경로 import 필요. |
브라우저 DOM·미디어·디바이스 (dom.ts, media.ts, device.ts)
브라우저 전용 API(document, window, Image, FileReader, navigator.vibrate)에 의존하므로 SSR/native 환경에서는 사용 불가합니다.
| 심볼 | 파일 | 설명 |
|---|---|---|
getElementPosition | dom.ts | 요소의 top/bottom 오프셋. |
placeCaretAtEnd | dom.ts | contenteditable 캐럿을 끝으로. |
locateCaret / locateCaretByX | dom.ts | 캐럿 위치 지정(오프셋/X좌표 기준). |
getTextFromChildNodes | dom.ts | 자식 노드에서 줄바꿈 보존 텍스트 추출. |
getImageSize / getVideoSize / getMediaSize / getMediaType | media.ts | Blob의 크기·타입 추출. |
vibrate | device.ts | vibrate.weak() / .medium() / .strong() 진동(지원 시). |
인코딩 (crypto.ts)
| 심볼 | 시그니처 | 설명 |
|---|---|---|
NumberCodec | class | 숫자 ↔ 짧은 코드 문자열 양방향 인코더. |
NumberCodec은 이름과 달리 Node crypto 모듈을 사용하지 않습니다. 62진수 charset(0-9A-Za-z)과 XOR·비트 회전으로 32bit 정수를 난독화된 고정 길이 코드로 변환합니다(암호학적 보안 목적 아님).
import {NumberCodec} from '@howmuchhome-web/utils-ts/crypto';
const codec = new NumberCodec({secretKey: [/* number[] */], codeLength: 6, numRounds: 5});
const code = codec.encode(12345); // 고정 길이 문자열
const value = codec.decode(code); // 12345AES 암호화 (aes-encryption.ts)
이 파일은 Node의 crypto 모듈(import crypto from 'crypto')을 사용합니다. 배럴(index.ts)에서 re-export 되므로, @howmuchhome-web/utils-ts를 import 하면 이 파일의 crypto 의존성이 함께 딸려옵니다. native 함정은 아래 native 주의를 참고하세요.
| 심볼 | 시그니처 | 설명 |
|---|---|---|
AES128 | class | AES-128-CBC/PKCS5. Java 샘플과 호환되도록 구현. iv는 userId 기반 고정. |
encryptAES128CBCSimple | (text: string) => string | AES128 기본 인스턴스로 암호화(base64). |
decryptAES128CBCSimple | (encryptedText: string) => string | 위의 복호화. |
encryptField | (value: string) => string | encryptAES128CBCSimple 래퍼, 실패 시 ''. |
createAES128 | (key: string, userId = 'hyphen') => AES128 | 커스텀 키/userId로 인스턴스 생성. |
encryptCertificateField | (value: string) => string | 전자증명서 v2용 AES-256-CBC. 랜덤 IV + base64(IV‖암호문). |
AES128 / encryptAES128CBCSimple은 키·userId를 환경변수(ENCRYPTION_KEY/NEXT_PUBLIC_ENCRYPTION_KEY, ENCRYPTION_USER_ID/NEXT_PUBLIC_ENCRYPTION_USER_ID)에서 읽으며 fallback 기본값을 가집니다.
encryptCertificateField는 NEXT_PUBLIC_CERTIFICATE_AES_KEY를 사용하며, 없으면 예외를 던집니다. 프론트에서 민감 데이터(주민번호 뒷자리 등)를 암호화해 서버로 보내는 증명서 발급 플로우에서 쓰입니다.
import {encryptCertificateField} from '@howmuchhome-web/utils-ts';
const rrn2Enc = encryptCertificateField(birthPassword); // base64(IV‖암호문)native 주의
aes-encryption.ts가 Node crypto를 import 하고 이 파일이 배럴에서 re-export 되므로, native(Expo/Hermes) 번들에서는 crypto가 해석되지 않습니다. 이를 위해 metro가 crypto를 빈 모듈로 stub 합니다.
- metro
EMPTY_STUBS에crypto가 등록되어 있어Unable to resolve 'crypto'없이 빌드는 통과합니다. - 그러나 stub 이므로 native 에서 AES 함수들을 실제로 호출하면 동작하지 않습니다. native 에서는 이 유틸들을 사용하지 마세요.
- 증명서 발급의 경우, native 는 web
encryptCertificateField와 동일 알고리즘(AES-256-CBC · 랜덤 IV ·base64(IV‖암호문))을 순수 JS(node-forge)로 재구현한 별도 구현을 씁니다:apps/native-app/src/features/electronic-certificate/lib/encryptCertificateField.ts. 이 native 구현의 출력은 서버(Nodecrypto)와 양방향 호환됩니다.
자세한 stub·resolver 규칙은 native 앱 · Metro 와 의존성을 참고하세요.
또한 dom.ts / media.ts / device.ts는 브라우저 전용 전역(document, window, Image, FileReader, navigator.vibrate)에 의존하므로 native·SSR 환경에서 호출하면 실패합니다.