문제 상황

  • 개인 프로젝트를 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

면접관의 역할은 지원자의 역량과 가능성을 공정하게 평가하고, 그 과정에서 지원자가 가진 강점을 발견해 내는 데 있다. 그러나 때때로 면접 과정에서 면접관의 태도나 질문 방식이 본래의 목적을 벗어나, 단순히 우월감을 드러내거나 지원자를 압박하는 데 집중되는 경우가 있다. 이러한 태도는 지원자에게 불필요한 좌절감을 안길 뿐만 아니라, 회사의 이미지에도 부정적인 영향을 미친다.

특히, 자신의 “대기업 출신” 경력을 강조하며 현재의 면접 과정을 깎아내리거나, 지원자의 자질을 평가하기보다 시스템의 문제로 돌리는 발언은 비판받아 마땅하다. “대기업에서는 인사팀이 필터링을 잘해줘서 면접이 원활하다”는 식의 발언은, 지원자가 면접 자리에 앉아 있는 것조차 실수였다는 뉘앙스를 풍긴다. 이러한 발언은 면접관 본인의 권위를 세우기 위해 지원자를 깎아내리는 행동에 불과하다.

또한, 면접관이 질문을 통해 지원자의 논리적 사고나 문제 해결 능력을 파악하려는 의도는 충분히 이해할 수 있다. 그러나 압박 면접이라는 명목으로 지식을 과시하거나, 지원자가 답하지 못할 법한 영역까지 집요하게 물고 늘어지는 태도는 건설적이지 않다. 이는 지원자의 성장을 돕는 피드백을 제공하기보다, 단순히 “나는 이런 것도 알고 있다”는 걸 과시하는 데 초점이 맞춰져 있는 경우가 많다.

더구나, 이런 사례가 특화된 도메인을 가진 스타트업에서 빈번히 발생한다는 점도 우려가 커지고 있는 부분이다. 스타트업이라면 일반적으로 지원자가 새로운 환경에 빠르게 적응하고, 문제 해결 능력을 통해 성장 가능성을 보여줄 수 있는지가 핵심이다. 그러나 면접관의 태도가 이런 유연함을 고려하기보다, 다른 쪽에 더 무게를 두는 경우 해당 스타트업이 이런 원칙을 고수하면서 얼마나 빠르게 성장할 수 있을지는 의문이다.

진정한 면접관은, 지원자가 모르는 부분을 통해 그들의 사고 과정을 살펴보고, 모르는 것을 배우려는 자세와 문제 해결 능력을 평가하는 사람이어야 한다. 면접은 지원자의 잠재력을 발견하고, 회사와의 적합성을 함께 탐색하는 과정이지, 면접관 개인의 지식 자랑 대회가 아니다.

회사는 면접관 교육을 통해 이러한 문제점을 개선하고, 면접 과정이 지원자에게도 배움과 성장의 기회가 될 수 있도록 노력해야 한다. 좋은 개발자가 좋은 면접관이 되는 것은 아니다. 훌륭한 면접관이란, 지원자가 가진 가능성을 긍정적으로 이끌어내고, 공정한 기준으로 평가할 줄 아는 사람이다.

모든 면접은 양방향의 과정이다. 면접관도 면접을 본다는 마음가짐으로, 스스로의 태도를 돌아볼 필요가 있다.

#489
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
19 중 1페이지