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])
면접관의 역할은 지원자의 역량과 가능성을 공정하게 평가하고, 그 과정에서 지원자가 가진 강점을 발견해 내는 데 있다. 그러나 때때로 면접 과정에서 면접관의 태도나 질문 방식이 본래의 목적을 벗어나, 단순히 우월감을 드러내거나 지원자를 압박하는 데 집중되는 경우가 있다. 이러한 태도는 지원자에게 불필요한 좌절감을 안길 뿐만 아니라, 회사의 이미지에도 부정적인 영향을 미친다.
특히, 자신의 “대기업 출신” 경력을 강조하며 현재의 면접 과정을 깎아내리거나, 지원자의 자질을 평가하기보다 시스템의 문제로 돌리는 발언은 비판받아 마땅하다. “대기업에서는 인사팀이 필터링을 잘해줘서 면접이 원활하다”는 식의 발언은, 지원자가 면접 자리에 앉아 있는 것조차 실수였다는 뉘앙스를 풍긴다. 이러한 발언은 면접관 본인의 권위를 세우기 위해 지원자를 깎아내리는 행동에 불과하다.
또한, 면접관이 질문을 통해 지원자의 논리적 사고나 문제 해결 능력을 파악하려는 의도는 충분히 이해할 수 있다. 그러나 압박 면접이라는 명목으로 지식을 과시하거나, 지원자가 답하지 못할 법한 영역까지 집요하게 물고 늘어지는 태도는 건설적이지 않다. 이는 지원자의 성장을 돕는 피드백을 제공하기보다, 단순히 “나는 이런 것도 알고 있다”는 걸 과시하는 데 초점이 맞춰져 있는 경우가 많다.
더구나, 이런 사례가 특화된 도메인을 가진 스타트업에서 빈번히 발생한다는 점도 우려가 커지고 있는 부분이다. 스타트업이라면 일반적으로 지원자가 새로운 환경에 빠르게 적응하고, 문제 해결 능력을 통해 성장 가능성을 보여줄 수 있는지가 핵심이다. 그러나 면접관의 태도가 이런 유연함을 고려하기보다, 다른 쪽에 더 무게를 두는 경우 해당 스타트업이 이런 원칙을 고수하면서 얼마나 빠르게 성장할 수 있을지는 의문이다.
진정한 면접관은, 지원자가 모르는 부분을 통해 그들의 사고 과정을 살펴보고, 모르는 것을 배우려는 자세와 문제 해결 능력을 평가하는 사람이어야 한다. 면접은 지원자의 잠재력을 발견하고, 회사와의 적합성을 함께 탐색하는 과정이지, 면접관 개인의 지식 자랑 대회가 아니다.
회사는 면접관 교육을 통해 이러한 문제점을 개선하고, 면접 과정이 지원자에게도 배움과 성장의 기회가 될 수 있도록 노력해야 한다. 좋은 개발자가 좋은 면접관이 되는 것은 아니다. 훌륭한 면접관이란, 지원자가 가진 가능성을 긍정적으로 이끌어내고, 공정한 기준으로 평가할 줄 아는 사람이다.
모든 면접은 양방향의 과정이다. 면접관도 면접을 본다는 마음가짐으로, 스스로의 태도를 돌아볼 필요가 있다.
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
export function parseNativeEmoji(unified: string): string {
return unified
.split('-')
.map((hex) => String.fromCodePoint(parseInt(hex, 16)))
.join('')
}
unified
문자열을-
로 분리하여 개별 유니코드 코드 포인트를 얻는다.- 각 16진수 코드 포인트를 정수로 변환하고, 그에 해당하는 캐릭터를 반환한다.
- 최종적으로 변환된 캐릭터들을 연결하여 하나의 문자열로 생성한다.
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 테스트 및 디폴트 값 설정에 활용될 수 있다.
브라우저에서 PDF 및 이미지 파일에 대한 OCR 실행
- PDF.js를 사용하여 PDF에서 이미지를 추출
- Tesseract OCR로 추출된 이미지에서 텍스트를 인식
- 직접 브라우저 상에서 OCR 작업을 실행
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를 렌더링함. - 이 구조는 테스트를 쉽게 하고, 컴포넌트의 역할을 명확하게 분리함.
/**
* - 인자 수 확인: `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)
- 함수 조합(Composition): 작은 함수들을 조합하여 새로운 함수를 만들며, 예를 들어
compose(f, g, h)
는f(g(h(x)))
로 실행 compose
: 오른쪽에서 왼쪽으로 함수를 실행하며,compose(square, double)(3)
은 36을 반환pipe
: 왼쪽에서 오른쪽으로 함수를 실행하여,pipe(double, square)(3)
의 결과는 36- 커링(Currying):
f(a, b, c)
를f(a)(b)(c)
형태로 변환하고, 예를 들어add(1)(2)(3)
의 결과는 6 - 부분 적용(Partial Application): 일부 인자만 미리 적용하여 새로운 함수를 만들 수 있으며,
const double = multiply(2, _)
로 정의 - 포인트-프리 스타일: 변수를 사용하지 않고 함수를 조합하여 작성하며, 예를 들면
compose(square, double)
와 같은 형태 - 데이터 마지막 원칙(Data Last): 데이터를 마지막 인자로 배치하여 조합성을 높이는데, 예를 들어
const halve = divideDataLast(2)
와 같이 사용
- 프런트엔드(클라이언트)에서 MP4 파일의 오디오 존재 여부를 확인하기
- 브라우저 API 사용 시 호환성 문제 발생 (Chrome, Safari, Firefox 각각 다른 API 사용)
- FileReader API를 사용하여 서버에서 응답받은 바이너리 데이터를 읽고, 이를 분석해 오디오 여부 확인.
- 오디오 정보가 발견되지 않으면 추가 데이터 요청하여 확인 범위를 증가시켜 반복.
await ffmpeg.writeFile('input.mp4', await fetchFile(file))
await ffmpeg.ffprobe(['-i', 'input.mp4', '-show_streams', '-o', 'output.txt'])
const data = await ffmpeg.readFile('output.txt')
…하지만 ffmpeg의 중요성을 깨달았다.