Serverless 환경에서 SQLite 문제 및 대안 정리

문제 상황

  • 개인 프로젝트를 Vercel에 배포한 후, SQLite 관련 에러가 발생
  • 읽기 전용으로 데이터 파일을 올려 사용하는 방식이 기존에는 잘 작동했으나, 최근 환경에서는 에러 발생
  • Vercel 같은 플랫폼에서는 SQLite 사용이 공식적으로 지원되지 않음

문제 해결 접근

대안 라이브러리 탐색 (JSON 기반)

  • 정적인 데이터로 쓸 경우 JSON 변환을 고려
  • 후보 라이브러리:
    • JSONata: JSON에 대한 선언적 질의
    • jsonpath: XPath 스타일 질의
    • AlaSQL: SQL 문법으로 JSON 쿼리 가능

해결: Turso 도입

  • Turso: LibSQL 기반의 SQLite-compatible serverless DB
  • 기존 SQLite 쿼리 그대로 사용 가능하며, Vercel에서도 정상 작동
  • serverless 환경에 특화된 구조로 신뢰성 및 확장성 확보
#492

llms.txt는 웹사이트나 애플리케이션이 자신이 사용하는 LLM(대규모 언어 모델) 및 관련 설정에 대해 명시적으로 문서화할 수 있는 포맷이다.

아래 링크들은 llms.txt 포맷이 실제로 어떻게 사용되고 있는지 참고한 자료들. 각 사이트는 자신들의 문서를 llms.txt에 구조적으로 명시하고 있다.


#491
const isVisible = document.visibilityState === 'visible'
const isHidden = document.visibilityState === 'hidden'

document.addEventListener('visibilitychange', onChange)

/**
 * `visibilitychange` 이벤트는 정상적인 탭 전환 시에는 잘 작동하지만, 시스템 슬립, 화면 잠금, 또는 브라우저가 백그라운드에서 복귀할 때는 누락될 수 있다.
 * 그래서 수동으로 처리되는 부분이 필요
 */
document.addEventListener('mousemove', setVisible)
document.addEventListener('keydown', setVisible)

const [isVisible, setIsVisible] = useState(true)

useEffect(() => {
  const onChange = () => {
    const newState = document.visibilityState !== 'hidden'

    if (newState !== isVisible) {
      setIsVisible(newState)
    }
  }

  // addEventListener

  return () => {
    // removeEventListener
  }
}, [isVisible])

#490
type Task = (callback: (result: string) => void) => void

type TasksCallback = (results: string[]) => void

class TaskRunner {
  protected tasks: Task[] = []

  protected results: Set<string> = new Set()

  addTask(task: Task) {
    this.tasks.push(task)
  }
}

class ParallelTaskRunner extends TaskRunner {
  run(callback: TasksCallback) {
    const totalTasks = this.tasks.length

    this.tasks.forEach((task) => {
      task((result) => {
        this.results.add(result)

        if (this.results.size === totalTasks) {
          callback([...this.results])
        }
      })
    })
  }
}

class SerialTaskRunner extends TaskRunner {
  index = 0

  run(callback: TasksCallback) {
    const executeTask = () => {
      if (this.index >= this.tasks.length) {
        callback([...this.results])
        return
      }

      this.tasks[this.index]((result) => {
        this.results.add(result)
        this.index++

        executeTask()
      })
    }

    executeTask()
  }
}

Footnotes

  1. concurrently

#485
export function parseNativeEmoji(unified: string): string {
  return unified
    .split('-')
    .map((hex) => String.fromCodePoint(parseInt(hex, 16)))
    .join('')
}
  • unified 문자열을 -로 분리하여 개별 유니코드 코드 포인트를 얻는다.
  • 각 16진수 코드 포인트를 정수로 변환하고, 그에 해당하는 캐릭터를 반환한다.
  • 최종적으로 변환된 캐릭터들을 연결하여 하나의 문자열로 생성한다.

#477
const emptyCsvFile = new File([''], 'default.csv', { type: 'text/csv' })
const emptyPngFile = new File([''], 'default.png', { type: 'image/png' })

formData.append('product_coupons', emptyCsvFile)
formData.append('product_imgs', emptyPngFile)

console.assert(emptyCsvFile.size === 0)

빈 파일을 FormData에 첨부하여 전송하는 방법

  • File 객체를 사용하여 빈 파일을 생성할 수 있다.
  • 파일의 MIME 타입을 지정하여 타입에 맞는 빈 파일을 만들 수 있다.
  • 생성된 빈 파일을 FormData 객체에 추가하여 전송할 수 있다.

이는 서버에 빈 형태의 파일을 전송할 때 유용하며, API 테스트 및 디폴트 값 설정에 활용될 수 있다.

#461

브라우저에서 PDF 및 이미지 파일에 대한 OCR 실행

  • PDF.js를 사용하여 PDF에서 이미지를 추출
  • Tesseract OCR로 추출된 이미지에서 텍스트를 인식
  • 직접 브라우저 상에서 OCR 작업을 실행
#431
import * as React from 'react'

type ContainerProps = {
  children: typeof Body
}

type BodyProps = {
  id: string
}

function Container({ children }: ContainerProps) {
  const id = '1234'

  return <>{children({ id })}</>
}

export function Body({ id }: BodyProps) {
  return <>{id}</>
}

export function Page() {
  return <Container>{Body}</Container>
}

컨테이너/프레젠테이션 패턴을 활용한 React 컴포넌트 구조.

  • 컨테이너 컴포넌트(Container)는 상태를 관리하고 데이터를 하위 컴포넌트에 전달함.
  • 프레젠테이션 컴포넌트(Body)는 데이터를 받아서 UI를 렌더링함.
  • 이 구조는 테스트를 쉽게 하고, 컴포넌트의 역할을 명확하게 분리함.
#386
/**
 * - 인자 수 확인: `curry` 함수는 전달된 함수 `fn`의 인자 수를 확인
 * - 인자 수가 충분한 경우: 만약 `args`의 길이가 `fn`의 인자 수 이상이면, `fn`을 호출하고 결과를 반환
 * - 인자 수가 부족한 경우: 그렇지 않으면 추가 인자(`moreArgs`)를 받을 수 있는 새 함수를 반환. 이 함수는 기존 인자(`args`)와 새로운 인자(`moreArgs`)를 합쳐 다시 `curry(fn)`을 호출하여 최종적으로 인자가 충분할 때까지 이 과정을 반복
 */
const curry =
  (fn: Function) =>
  (...args: any[]) =>
    args.length >= fn.length
      ? fn(...args)
      : (...moreArgs: any[]) => curry(fn)(...args, ...moreArgs)

/**
 * - `...fns: Function[]` 여러 개의 함수를 인자로 받는다
 * - 내부에서 `reduce` 메서드를 사용하여 함수 배열을 순회 초기값으로 `x`를 사용하며, 각 함수 `fn`을 차례로 호출하여 결과를 다음 함수로 전달
 */
const pipe =
  (...fns: Function[]) =>
  (x: any) =>
    fns.reduce((acc, fn) => fn(acc), x)

/**
 * - `...fns`: 여러 개의 함수를 매개변수로 받음
 * - `(x)`: 초기값 `x`를 인자로 받아 결과를 리턴
 * - `fns.reduceRight(...)`: 배열의 오른쪽부터 왼쪽으로 각 함수를 적용합니다. `y`는 이전 함수의 결과이며, `f`는 현재 함수
 * - `f(y)`: 현재 함수 `f`를 이전 함수의 결과 `y`에 적용
 */
const compose =
  (...fns) =>
  (x) =>
    fns.reduceRight((y, f) => f(y), x)

  1. 함수 조합(Composition): 작은 함수들을 조합하여 새로운 함수를 만들며, 예를 들어 compose(f, g, h)f(g(h(x)))로 실행
  2. compose: 오른쪽에서 왼쪽으로 함수를 실행하며, compose(square, double)(3)은 36을 반환
  3. pipe: 왼쪽에서 오른쪽으로 함수를 실행하여, pipe(double, square)(3)의 결과는 36
  4. 커링(Currying): f(a, b, c)f(a)(b)(c) 형태로 변환하고, 예를 들어 add(1)(2)(3)의 결과는 6
  5. 부분 적용(Partial Application): 일부 인자만 미리 적용하여 새로운 함수를 만들 수 있으며, const double = multiply(2, _)로 정의
  6. 포인트-프리 스타일: 변수를 사용하지 않고 함수를 조합하여 작성하며, 예를 들면 compose(square, double)와 같은 형태
  7. 데이터 마지막 원칙(Data Last): 데이터를 마지막 인자로 배치하여 조합성을 높이는데, 예를 들어 const halve = divideDataLast(2)와 같이 사용

#385
19 중 1페이지