/**
 * 취소 가능한 fetch 요청
 * @param url - 요청 URL
 * @param options - fetch 옵션
 * @returns {run, cancel} - run: 요청 실행 함수, cancel: 요청 취소 함수
 */
const createCancelableFetch = <T>(url: string, options: RequestInit = {}) => {
  const abortController = new AbortController()

  const run = async () => {
    const response = await fetch(url, {
      ...options,
      signal: abortController.signal,
    })

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    return response.json() as Promise<T>
  }

  const cancel = () => abortController.abort()

  return {
    run,
    cancel,
  }
}

const { run, cancel } = createCancelableFetch('/api/data')

run()
  .then((data) => console.log('Fetched data:', data))
  .catch((err) => console.error('Error or canceled:', err))

cancel()
#360

https://developer.chrome.com/docs/css-ui/css-field-sizing?&hl=ko

field-sizing를 사용하면 콘텐츠를 기반으로 크기 조절을 사용 설정하는 데 CSS 한 줄이 필요합니다. 이 콘텐츠 기반 크기 조절 스타일은 textarea 외에도 다른 요소에도 적용됩니다.

#358

tuple 함수 메모

  • 목적: 비동기 호출의 결과와 오류를 튜플 형식으로 반환
  • 사용 예: const [error, data] = await tuple(someAsyncFunction());

동작 방식:

  1. 입력: maybePromise (Promise 또는 일반 값)
  2. 처리:
    • try 블록에서 await로 비동기 결과를 기다림
    • 성공 시: [null, 결과값] 반환
    • 오류 발생 시:
      • Error 인스턴스이면: [error] 반환
      • 그 외의 경우: [new TupleItError(error)] 반환

장점:

  • 오류 처리 간소화 (단일 체크로 오류 관리 가능)
#357

https://www.jameskerr.blog/posts/use-state-object/

useStateObject는 React의 useState를 확장한 가벼운 래퍼로, 객체 상태 관리를 간편하게 할 수 있도록 설계되었습니다.


export type StateObject<T extends object> = T & {
  set: React.Dispatch<React.SetStateAction<T>>
  setItem: <K extends keyof T>(key: K, value: T[K]) => void
  merge: (newState: Partial<T>) => void
  reset: () => void
}

그렇다면 MapSet도 시도해보기

function useStateMap<K, V>(init: Iterable<[K, V]> = []) {
  const [map, setMap] = useState(new Map<K, V>(init))

  const update = useCallback(
    (updater: (currentMap: Map<K, V>) => void) => {
      setMap((prev) => {
        const newMap = new Map(prev)
        updater(newMap)
        return newMap
      })
    },
    [setMap]
  )

  return {
    map,
    set: (key: K, value: V) => update((m) => m.set(key, value)),
    delete: (key: K) => update((m) => m.delete(key)),
    clear: () => setMap(new Map()),
    has: (key: K) => map.has(key),
    get: (key: K) => map.get(key),
    entries: () => Array.from(map.entries()),
    size: map.size,
  }
}
function useStateSet<T>(init: Iterable<T> = []) {
  const [set, setSet] = useState(new Set<T>(init))

  const update = useCallback(
    (updater: (currentSet: Set<T>) => void) => {
      setSet((prev) => {
        const newSet = new Set(prev)
        updater(newSet)
        return newSet
      })
    },
    [setSet]
  )

  return {
    set,
    add: (value: T) => update((s) => s.add(value)),
    delete: (value: T) => update((s) => s.delete(value)),
    has: (value: T) => set.has(value),
    clear: () => setSet(new Set()),
    entries: () => Array.from(set),
    size: set.size,
  }
}

#354

thisisunsafe

-> 로컬 환경 또는 테스트 서버에서 강제 접근

  • 웹사이트의 SSL/TLS 인증서를 신뢰할 수 없을 때, 이 사이트에 연결할 수 없음 또는 이 연결은 비공개로 설정되지 않았습니다
  • NET::ERR_CERT_AUTHORITY_INVALID, NET::ERR_CERT_COMMON_NAME_INVALID 같은 오류 코드가 나타남
#352

템플릿 리터럴 타입을 활용하여 타입 정의하기

import * as React from 'react'

type RenderPropNames = 'Title' | 'Content' | 'Actions'

type RenderProps = {
  [K in RenderPropNames as `render${K}`]: () => React.ReactNode
}

type Props = RenderProps

/**
 * @example
 *
 * ```tsx
 * <DialogComponent
 *   renderTitle={() => <h2>Title</h2>}
 *   renderContent={() => <p>Content</p>}
 *   renderActions={() => (
 *     <div>
 *       <button onClick={handleClose}>Close</button>
 *       <button onClick={handleSubmit}>Submit</button>
 *     </div>
 *   )}
 * />
 * ```
 */
function Dialog({
  renderTitle,
  renderContent,
  renderActions,
}: Props) => {
  return (
    <div data-scope="root">
      <div data-part="content">
        {renderTitle()}
        {renderContent()}
      </div>
      <div data-part="actions">{renderActions()}</div>
    </div>
  )
}
#350

두 개의 오버로딩된 메서드에서 각각의 반환 타입을 명시적으로 추출하기

type Year = {
  year(): number
  year(u: string): string
}

type ReturnTo<T, R> = T extends R ? R : never

type GetYear = ReturnTo<Year['year'], () => number>
type SetYear = ReturnTo<Year['year'], (u: string) => string>
#345
  • 문제: 스프레드시트 데이터가 커서 내용 확인이 어려움.
  • 해결: 구글 드라이브에 스프레드시트 데이터를 CSV 파일로 업로드하여 확인.
/**
 * 현재 활성 스프레드시트의 첫 번째 시트 데이터를 CSV 파일로 저장하고,
 * 구글 드라이브에 업로드합니다.
 * 생성된 파일의 URL은 콘솔에 로그로 출력됩니다.
 */
function saveSpreadsheetDataAsCSV() {
  /** 현재 활성 스프레드시트 */
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]
  /** 시트의 모든 데이터의 2차원 배열 */
  const data = sheet.getDataRange().getValues()
  /** CSV 데이터 */
  const csv = data.map((row) => row.join(',')).join('\n')
  /** 파일명 */
  const fileName = 'spreadsheet_data.csv'
  /** 구글 드라이브에 업로드 완료된 CSV 파일 */
  const file = DriveApp.createFile(fileName, csv, MimeType.CSV)

  console.log('파일이 생성되었습니다: ' + file.getUrl())
}
#342
class ColorManipulator {
  private baseColor: string
  private targetColor: string

  constructor(baseColor: string, targetColor: string) {
    this.baseColor = baseColor
    this.targetColor = targetColor
  }

  private hexToRgb(hex: string) {
    const bigint = parseInt(hex.slice(1), 16)

    return {
      r: (bigint >> 16) & 255,
      g: (bigint >> 8) & 255,
      b: bigint & 255,
    }
  }

  public calculateOpacity() {
    const target = this.hexToRgb(this.targetColor)
    const baseRG = this.hexToRgb(this.baseColor)
    const opacities = [
      (target.r - baseRG.r) / (255 - baseRG.r),
      (target.g - baseRG.g) / (255 - baseRG.g),
      (target.b - baseRG.b) / (255 - baseRG.b),
    ]
    const averageOpacity =
      opacities.reduce((sum, value) => sum + value, 0) / opacities.length

    return averageOpacity
  }

  public getCssRGBA() {
    const opacity = this.calculateOpacity()

    return `rgba(0, 0, 0, ${opacity.toFixed(2)})`
  }
}

const manipulator = new ColorManipulator('#000000', '#D1D7DE')
const opacity = manipulator.calculateOpacity()
const cssRGBA = manipulator.getCssRGBA()
#341
17 중 1페이지