// 기본
@primary: #3498db;
.button { color: @primary; }

// 보간 (선택자, 속성, URL에서)
@component: button; // 따옴표를 넣으면 .'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 파일 리스트 디버깅

// gulpfile.mjs — gulp 5 · gulp-debug 5 · glob 14 (gulp-debug 5 는 ESM 전용)
import gulp from 'gulp'
import debug from 'gulp-debug'
import through2 from 'through2'
import gulpif from 'gulp-if'
import { globSync } from 'glob'

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

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

// 3. glob 패턴 사전 확인
console.log(globSync('src/**/*.js'))

// 조건부 디버깅: `NODE_ENV=debug gulp build`
gulp.src('src/**/*.js').pipe(gulpif(process.env.NODE_ENV === 'debug', debug()))
#525

TypeScript Discriminated Union 문서화 딜레마

// 개발: 타입 안전, IDE 자동완성, 명확한 의도
type Button =
  | { type: 'submit'; color: string }
  | { type: 'reset'; text: string }
<!-- 문서화: 조건부 속성 설명 어려움, 테이블 복잡, 예시 다수 필요 -->

| 속성  | 타입                | 조건                    |
| ----- | ------------------- | ----------------------- |
| type  | 'submit' \| 'reset' | 필수                    |
| color | string              | type='submit'일 때 필수 |
| text  | string              | type='reset'일 때 필수  |

  • 자세한 타입 정의는 TypeScript 정의 파일 참고
  • typedoc, api-extractor로 자동 생성
#524

react-spring

  • onRest 애니메이션이 물리적으로 정지했을 때 → UI 상태 업데이트용1
  • onResolve Promise가 resolve될 때 → 비동기 플로우 제어용
const [springs, api] = useSpring(() => ({
  x: 0,
  onRest: () => setStatus('stopped'), // UI 상태
  onResolve: () => console.log('done'), // Promise 기반 로직
}))

// Promise 방식도 가능
await api.start({ x: 100 })

Footnotes

  1. Events - react-spring: onRest — “Called when the animation comes to a stand-still.” / onResolve — “Called when the promise for the update is resolved.” ↩

#523

npm install 실패 시 플래그 선택

# 1. Node 버전 불일치 (engines 필드) — npm 은 기본이 경고(EBADENGINE)뿐
#    막혔다면 engine-strict 가 켜진 것. --ignore-engines 는 yarn 플래그라 npm 은 무시한다
npm install --engine-strict=false

# 2. peer dependencies 충돌 (가장 흔함)
npm install --legacy-peer-deps

# 3. 마지막 수단 (권장 안함)
npm install --force

--legacy-peer-deps가 필요한 상황

{
  "react": "^18.2.0",
  // 라이브러리가 아직 React 18 공식 지원 안 하지만 실제로는 동작함.
  "react-beautiful-dnd": "^13.1.1" // peer: react@^16.8 || ^17
}
# 프로젝트 전체 적용
echo "legacy-peer-deps=true" > .npmrc

  • package.json engines - npm: “Unless the user has set the engine-strict config flag, this field is advisory only and will only produce warnings when your package is installed as a dependency.”
  • legacy-peer-deps - npm: “Causes npm to completely ignore peerDependencies when building a package tree, as in npm versions 3 through 6.”
#522

RTL에서 overflow scroll 상태 테스트 — 실제 브라우저(Vitest browser mode·Playwright)에서만 돈다. jsdom 은 레이아웃을 하지 않아 scrollWidth·clientWidth 가 0 이라 아래에서 true 를 기대하는 단언이 둘 다 실패한다.1

// scrollWidth > clientWidth면 가로 스크롤 필요
const isOverflowScrollable = (el) => ({
  horizontal: el.scrollWidth > el.clientWidth,
  vertical: el.scrollHeight > el.clientHeight,
})

test('overflow 확인', () => {
  render(<ScrollableComponent parentWidth={300} childWidth={500} />)

  const parent = screen.getByTestId('parent-container')

  expect(isOverflowScrollable(parent).horizontal).toBe(true)
})

test('동적 크기 변경', () => {
  const { rerender } = render(<Comp parentWidth={400} childWidth={300} />)

  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(
    false
  )

  rerender(<Comp parentWidth={400} childWidth={600} />)

  expect(isOverflowScrollable(screen.getByTestId('parent')).horizontal).toBe(
    true
  )
})

jsdom 에서 돌려야 하면 515 처럼 Object.defineProperty 로 scrollWidth·clientWidth 를 모킹한다. getBoundingClientRect() 도 jsdom 에선 전부 0이다.


Footnotes

  1. jsdom README — Unimplemented parts of the web platform: “Layout: the ability to calculate where elements will be visually laid out as a result of CSS, which impacts methods like getBoundingClientRects() or properties like offsetTop.” / “Currently jsdom has dummy behaviors for some aspects of these features, such as … returning zeros for many layout-related properties.” ↩

#521

Node.js에서 윈도우즈 외부 경로 파일 읽기 — 윈도우즈에선 / 도 구분자로 받지만 path 메서드는 \ 로 만든다.1

const path = require('path')
const fs = require('fs').promises

// 슬래시 사용 (윈도우즈는 / 와 \ 를 둘 다 구분자로 받는다)
// ('C:/Users/username/Documents/file.txt')

// 또는 백슬래시 이스케이프
// ('C:\\Users\\username\\Documents\\file.txt')

// 권장: path.join 사용 (크로스 플랫폼)
path.join(process.env.USERPROFILE, 'Documents', 'file.txt')

// path.resolve로 절대 경로 생성
path.resolve('../../Documents/file.txt')

path.normalize()는 디렉토리 트래버설을 막지 못한다 — .. 를 풀기만 해서 normalize('../../etc/passwd') 는 그대로 '../../etc/passwd' 다.2 사용자 입력이면 path.resolve(base, input) 결과가 base + path.sep 로 시작하는지 확인한다.


Footnotes

  1. Path - Node.js: “On Windows, both the forward slash (/) and backward slash (\) are accepted as path segment separators; however, the path methods only add backward slashes (\).” ↩

  2. path.normalize - Node.js: “The path.normalize() method normalizes the given path, resolving '..' and '.' segments.” ↩

#520

Date.getDay() - 로컬 시간 기준 요일 반환 (0=일요일, 6=토요일)1

new Date().getDay() // 0~6
new Date(2024, 11, 25).getDay() // 3 (수요일) — 월은 0부터 시작
new Date('2024-12-25').getDay() // ⚠ 날짜만 쓴 문자열은 UTC 자정 → UTC보다 느린 시간대(미국 등)에선 2

const days = ['일', '월', '화', '수', '목', '금', '토']
days[new Date().getDay()] // 오늘 요일

Footnotes

  1. Date.prototype.getDay() - MDN: “returns the day of the week for this date according to local time, where 0 represents Sunday.” / Date time string format - MDN: “When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.” ↩

#519

검색어 필터링 UI → Debounce (300-500ms)

타이핑 완료 후 검색이 더 자연스러움. “자바스크립트” 입력 시 매 글자마다 검색하면 비효율적. Debounce 는 호출이 멈출 때까지 기다리고, Throttle 은 계속 호출돼도 정해진 최대 빈도로 실행한다.1

const handleSearch = debounce((term) => performSearch(term), 300)
input.addEventListener('input', (e) => handleSearch(e.target.value))

Throttle은 실시간 검색 결과를 보여주면서 요청 제한할 때 사용 (1초마다 최대 1회 등).


Footnotes

  1. Debounce - MDN Glossary: “The key difference is that throttling enforces limits on continuous operations, while debouncing waits for invocations to stop for a specific time to consolidate many noisy invocations into one single invocation.” ↩

#518

TypeScript 제네릭 기본값 - <T extends Type = DefaultValue>. 기본값은 제약을 만족해야 하고, 생략한 인자는 기본값으로 정해진다.1

type SelectProps<T extends 'single' | 'multiple' = 'multiple'> = {
  mode: T
  value: T extends 'single' ? string : string[]
}

const a: SelectProps = { mode: 'multiple', value: ['a', 'b'] } // 기본값 사용
const b: SelectProps<'single'> = { mode: 'single', value: 'a' } // 명시적 지정

// 여러 매개변수에 각각 기본값
type ApiResponse<
  TData = any,
  TStatus extends 'loading' | 'success' | 'error' = 'loading'
> = {
  data: TStatus extends 'success' ? TData : null
  status: TStatus
}

가장 자주 사용되는 케이스를 기본값으로 설정하면 제네릭을 항상 명시하는 번거로움 줄어듦.


Footnotes

  1. TypeScript 2.3 — Generic parameter defaults: “Default types for a type parameter must satisfy the constraint for the type parameter, if it exists.” / “Unspecified type parameters will resolve to their default types.” ↩

#516
33 중 5페이지