Footnotes

  1. 삼항연산자(ternary)의 어두운면과 우려를 나타내는 글

  2. forEach 루프 break하는 트릭. 참조하는 배열의 length값을 0으로.

  3. 조건문에서 label을 지정해서 해당 블록으로 break가 가능하다는걸 보여주는 내용. 실제 적용할만한 사례는 거의 없고 글쓴이는 while+switch문에서 break를 좀 더 효율적으로 사용한 예를 보여주고 있다.

  4. optional chaining 문법으로 리팩토링 하면서…변경 가능한 부분(삼항연산자, 배열, 기능탐지)과 주의사항(값 할당, 잘못된 위치에 표기), 조심할 부분(null/undefined, 연산 순서와 순위, return 항상 호출됨)으로 구분해서 정리한 글. 커밋을 보면 실제로 어떻게 작업했는지 확인도 가능하다.

  5. <script /> 태그와 속성값에 대한 정리

  6. rust로 작성하고 wasm으로 컴파일해서 클라이언트 프로그램을 작성하는 방법을 자세하게 설명하고 있다. rust에 관심이 있거나 하다면 볼만한 글.

  7. 종속성 구조를 파악해서 최적의 병렬화로 빠르게 데이터 가져오기

  8. structuredClone()

  9. pipe, flow 제안(초안)사항을 검토하는 관점에서 보는 시각

#1

Footnotes

  1. 체이닝이 아닌 pipe가 필요한 이유에 대해서 설명해주고 있다.

  2. ows라는 라이브러리를 만든 이유(스트림으로 작성 시 간단하게 표현가능하도록 만들기 위함)와 옵저버블과 스트림의 동작 차이를 설명. 그리고 실제 어떤 부분에서 스트림으로 구현했는지 예제로 간략하게 알려주고 있다.

#10
#106

인라인 요소에 bold 스타일이 적용될 경우 레이아웃 시프팅 현상이 발생하기 때문에 해당 이슈를 해결하는 방법들.


Footnotes

  1. text-shadow로 우회하는 방법이 주 해결방법으로 올라왔다.

  2. content 속성과, grid 레이아웃을 이용한 방법.

#107

Footnotes

  1. flexbox | 파일명에 ellipsis 효과 적용, 단 파일 확장자는 제외한다.

  2. :truncated 개념이 없기 때문에 스크립트로 ResizeObserver와 영역값을 체크해서 구현한 내용.

#108

Footnotes

  1. 유토피아는 디자이너와 개발자가 반응형 디자인의 유동성에 대한 체계적인 접근 방식을 공유할 때 나타납니다. x 개의 임의 중단점에 대해 설계하는 대신 요소가 비례하고 유동적으로 확장되는 시스템을 설계할 수 있습니다.

  2. viewport 기반의 글꼴 크기를 clamp로 어떻게 구현하는지 설명해주고 있다.

#109

Footnotes

  1. 날짜 및 시간 작업을 위한 Temporal API 공식 문서

  2. 날짜 및 시간을 사용하여 작업할 때의 문제를 논의하고 솔루션으로 Temporal API를 제안하는 블로그 게시물

  3. Date 개체, 날짜 형식, 시간대 및 날짜 라이브러리를 포함하여 JavaScript에서 날짜 작업에 대한 포괄적인 설명

  4. 타임존 작업의 기본 사항을 설명

#11
# 마지막 커밋 해시
git log --pretty=format:'%h' -n 1
# 유저정보
git config user.name
git config user.email
#101

discard1

git checkout -- ./index.js

// 마지막 커밋 시의 상태로 되돌립니다.
git reset --hard

리모트 브랜치 가져오기

git remote update

Footnotes

  1. 명령어로 discard를 하려면

#105

Footnotes

  1. z-index값을 체계적으로 관리하기 위한 방법(변수로 설정해서 상대적으로 값을 설정)을 제시하고 있다.

#111

offset-path값을 지정해서 keyframes로 애니메이션을 적용하는 방법과 예제들


Footnotes

  1. element가 따라갈 모션 경로를 지정하고 상위 컨테이너 또는 SVG 좌표 시스템 내에서 요소의 위치를 ​​정의

#112

Footnotes

  1. all: unset;을 적용해서 reset 하는 방법을 제시하고 있다.

  2. box-sizing, 기본 margin 제거, percent 기반 높이, line-height 조정, font-smoothing, 미디어 타입 기본값, form 요소 글꼴 상속, 줄 바꿈, 루트 스태킹 컨텍스트로 구성된 초기화

  3. text-size-adjust 관련한 상황들

#113

Footnotes

  1. css 3d 작업시 perspective에 따른 변화와 원리를 설명해준 글

  2. translate 값을 얻기 위해서 getComputedStyle로 matrix값을 얻은 다음 2d, 3d에 따라서 해당 값을 체크

#114

Footnotes

  1. clip-path: inset(0 0 0 0); 방향에 따라서 버튼 배경색이 확장되는 모션. 방향은 삼각형(polygon()) 4개를 각 방향에 맞춰 위치시키면 된다.

  2. 화살표 모양 그리기와 filter: drop-shadow로 그림자 효과 생성하기

#116

Footnotes

  1. 초기로딩 속도를 개선하기 위한 content-visibility: auto 적용에 관한 글.

  2. 접근성관련 스크린리더에서 혼란스러울수도 있는 부분에 대한 설명이 추가되어 있다.

  3. 성능관련 css 속성을 언제(계속 권장인지, 문제가 있을때인지) 써야 맞는건가에 대한 의문이 담긴 의견.

#117

Footnotes

  1. 디자인 토큰 개념으로 사용

  2. 변수에 유효한 값을 설정하는게 아닌 계산을 설정해서 curry형식으로 사용할 수 있는 아이디어

  3. transform 같은 다중값 할당이 필요할 경우 커스텀 프로퍼티를 활용해서 적용 가능한 트릭 설명

#118

Footnotes

  1. ripple 효과를 어떻게 만들 수 있는지 설명해주고 있다. 기본 애니메이션은 css로 작성하고 이펙트가 발생하는 위치는 js로 체크해서 완성. 간단하게 ripple효과만 필요하다면 참고.

  2. abutton을 기능적으로 리스트해서 비교해주고 있다.

#119

Footnotes

  1. ECMAScript에 패턴 매칭 구문을 추가하는 제안

  2. 스마트 유형 추론을 포함한 TypeScript용 패턴 매칭 기능을 제공하는 라이브러리

  3. 타입스크립트용 패턴 매칭을 제공하는 TS-Pattern 라이브러리를 소개하고 예제와 함께 사용 방법을 설명하는 블로그 게시물

#12

Footnotes

  1. 미디어쿼리로 hover 기능이 탐지하는 방법을 소개하고 있다.

#120

Footnotes

  1. alt

  2. 최신 이미지 형식(AVIF 또는 WebP)이 압축을 최대 50%까지 개선하고 시각적으로 매력적으로 보이면서도 바이트당 더 나은 품질을 제공할 수 있는 방법을 강조

  3. 이미지 사용 및 최적화는 복잡하다. 그래서 개발자가 성능과 품질사이의 균형을 잘 조절해야 한다. 그래서 매번 자체적으로 개발하는 문제 대신 프레임웤 레밸에서 모범사례를 만드는게 중요하다.

  4. css에서 사용가능한 image-set(). type 기능은 아직 진행중.

  5. webp 기능탐지 방법을 가이드하고 있다

#121

Footnotes

  1. Defiant는 XPath 표현식을 사용하여 빠른 검색을 가능하게 하는 방법으로 전역 JSON 개체를 확장합니다.

  2. JSONata는 JSON 데이터를 위한 경량 쿼리 및 변환 언어입니다. XPath 3.1의 ‘위치 경로’ 의미에서 영감을 받아 정교한 쿼리를 간결하고 직관적인 표기법으로 표현할 수 있습니다.

  3. JS Search는 JavaScript 및 JSON 객체를 위한 효율적인 클라이언트 측 검색 라이브러리입니다.

  4. GROQ는 Sanity의 오픈소스 쿼리 언어입니다. 배우기 쉬운 강력하고 직관적인 언어입니다. GROQ를 사용하면 애플리케이션에 필요한 정보를 정확히 설명하고, 여러 문서 세트의 정보를 결합하고, 필요한 필드만으로 매우 구체적인 응답을 결합할 수 있습니다.

  5. Query JSON documents in the Terminal with GROQ | CSS-Tricks

  6. JSON과 유사한 구조에서 계산을 수행하기 위한 소형 내장형 언어

#123


Footnotes

  1. 구조화된 데이터의 직렬화 및 역직렬화를 허용하기 위해 Google에서 개발한 프로토콜입니다. Google은 시스템이 통신할 수 있도록 XML에 비해 더 나은 방법을 제공하는 것을 목표로 개발했습니다. 그래서 그들은 XML보다 더 간단하고, 더 작고, 더 빠르고, 유지 관리하기 쉽게 만드는 데 초점을 맞췄습니다.

#125

Footnotes

  1. 새롭게 문서사이트가 개편되면서 현실적인 도움이 된다.

  2. v3로 이전하기 위한 대략적인 가이드

  3. service_worker로 변경되면서 추가적으로 가이드 문서가 새로 생겼다.

  4. mdn 스타일이 좋다면 이쪽 문서도 괜찮다.

#127

매니페스트 V3로 마이그레이션1

  • host_permissions으로 분리
  • action으로 통합(두군데 수정이 필요하다)

Footnotes

  1. https://github.com/cbcruk/webext/commit/cbb36355871fb36d5f6c21752aae3400d2a97abf#diff-0f354ef5fd807996fa3f5a7a83ceb74025aa8f85862e321b8938988031e99711L3-L25

#128
function getTiltedHeight(angle) {
  const a = 100;
  const A = 90 - angle;
  
  const c = a / Math.sin(Math.PI * A / 180);
  const b = Math.sqrt(Math.pow(c, 2) - Math.pow(a, 2));
  
  return `${Math.abs(b)}%`;
}
#122

TIL — The power of JSON.stringify replacer parameter | pawelgrzybek.com

JSON.stringify(dude, (key, value) =>
  value instanceof Set ? [...value] : value
);
JSON.stringify(dude, null, "🍆");
#124

디자인 된 <select />placeholder 개념이 있어서 어떻게 하면 좋을지 찾아봤다. 간략하게 설명하자면 선택이 불가능하게 disabled 추가하고 hidden으로 숨김 마지막으로 selected로 디폴트값을 처리하면 완성.

function Select({ placeholder, children }) {
  return (
    <select>
      <option value="" disabled hidden selected>
        {placeholder}
      </option>
      {children}
    </select>
  )
}

#132

Under-Engineered Select Menus | Adrian Roselli

  • font, letter-spacing, word-spacing 상속
  • appearance 화살표 수정
  • 상태(focus, required, invalid)에 따른 스타일 추가

#134

Footnotes

  1. WebVTT에 적용 가능한 스타일에 대해 설명하고 있다

  2. SSML을 사용하면 대화의 응답을 자연스러운 말처럼 보이게 할 수 있습니다.

  3. 이벤트

  4. monitorEvents를 활용한 디버깅

#135
#136

Footnotes

  1. Chrome DevTools 및 Puppeteer를 TypeScript로 마이그레이션한 경험에 대한 설명

  2. 대규모 JavaScript 코드베이스를 TypeScript로 자동 마이그레이션하는 도구인 “ts-migrate”를 소개하는 Airbnb 엔지니어링 팀의 블로그 게시물

  3. Flow에서 TypeScript로 마이그레이션한 경험을 설명하고 오픈소스 flow-to-typescript-codemod 도구에 대한 링크를 포함하는 Stripe 엔지니어링 팀의 블로그 게시물

  4. Etsy 엔지니어링 팀이 대규모 웹 애플리케이션에서 TypeScript를 채택한 방법, 직면한 문제 및 얻은 이점을 설명

#14

Building a multi-select component

다중 선택 UI를 구현하기위해서 checkbox, select 두가지 방법으로 작업하는 방식을 소개하고 있다. 그외 선택된 상태값을 얻기위한 counter() 함수, 모바일 체크를 위한 미디어쿼리도 알려주고 있다.

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

  &::after {
    content: counter(filters);
  }
}
@media (pointer: coarse) {
  //
}

#133
# 개발할때 편하다
yarn add -D @types/cypress
// react-devtools 같은 확장도구가 필요할 경우

// /plugins/index.js
const path = require('path')

module.exports = (on, _config) => {
  on('before:browser:launch', (browser, launchOptions) => {
    if (browser.family === 'chromium') {
      const extensionFolder = path.resolve(__dirname, '..', '..', '4.7.0_1')

      launchOptions.args.push(`--load-extension=${extensionFolder}`)

      return launchOptions
    }
  })
}

// /support/commands.js
Cypress.on('window:before:load', (win) => {
  win.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.top.__REACT_DEVTOOLS_GLOBAL_HOOK__
})
// 파일업로드 기능 테스트
Cypress.Commands.add(
  'uploadFile',
  { prevSubject: true },
  (subject, fileName) => {
    cy.fixture(fileName).then((content) => {
      const el = subject[0]
      const testFile = new File([content], fileName)
      const dataTransfer = new DataTransfer()

      dataTransfer.items.add(testFile)
      el.files = dataTransfer.files
      cy.wrap(subject).trigger('change', { force: true })
    })
  }
)
// 에러때문에 테스트가 끊길 경우
Cypress.on('uncaught:exception', (err, runnable) => {
  console.log(err)
  return false
})
#146

테스트 하다가 특정 사이즈 파일이 필요한 경우. 순수하게 사이즈 관련된 내용만 테스트가 가능하다.1

mkfile 100m some100mfile.pdf

Footnotes

  1. How to create large PDF files (10MB, 50MB, 100MB, 200MB, 500MB, 1GB, etc.) for testing purposes? - Stack Overflow

#147

테스트 코드에서 mock으로 처리하는 경우1


Footnotes

  1. 그래서 내가 내린 결론은 저렇게 까지는 테스트할 필요가 없고 오히려 애매하게 결합된 컴포넌트들을 분리해서 관리하는게 맞을 것 같다는 생각을 해봤다.

#148

Footnotes

  1. 랜덤 벨류

  2. mask feTurbulence feDisplacementMap 합성

  3. viewbox 값을 제어

  4. 비디오 게임에서 좋은 아이디어…특히 애니메이션 기법을 배운다는 내용

#151

live2d 구현이 필요할 경우 pixi+plugin을 사용하면 된다

#152

Footnotes

  1. 가장 작은 파일 크기와 가장 빠른 성능을 위해 Web Animations API를 기반으로 구축된 새로운 애니메이션 라이브러리입니다.

#154

Footnotes

  1. Matter.js 는 웹용 JavaScript 2D 물리 엔진입니다.

  2. 2d 물리 엔진이 어떤 공식으로 구현되는지 설명해주는 글.

#156
**title**: 반품 및 교환은 재고로 이동합니다.

**as a** 가게 주인으로서,
**I want** 반품 또는 교환 시 상품을 재고에 다시 추가하고 싶습니다.
**so that** 인벤토리를 추적할 수 있습니다.

**Scenario 1:** 환불을 위해 반품된 항목은 인벤토리에 추가되어야 합니다.
**Given** 고객이 이전에 나에게서 검은색 스웨터를 구매했다는 점을 감안할 때
**and** 인벤토리에 검은색 스웨터 3개가 있습니다.
**when** 환불을 위해 검은색 스웨터를 반환할 때
**then** 그러면 인벤토리에 4개의 검은색 스웨터가 있어야 합니다.

Feature > […Scenario] > […Step]

  • Given: Given단계는 시스템의 초기 컨텍스트( 시나리오 장면) 를 설명하는 데 사용됩니다 . 그것은 일반적으로 과거에 일어난 일입니다.
  • When: When단계는 이벤트 또는 작업 을 설명하는 데 사용됩니다 . 이것은 시스템과 상호 작용하는 사람이거나 다른 시스템에 의해 트리거되는 이벤트일 수 있습니다.
  • Then: Then단계는 예상되는 결과 또는 결과 를 설명하는 데 사용됩니다 .
  • And, But
Scenario: 항목 검색 후 결제 페이지로 이동
Given 사용자가 Greencart 방문 페이지에 있다
When 사용자가 야채<이름>를 검색했을 때
And 장바구니에 항목이 추가된다
And 사용자는 구매를 위해 Checkout 페이지로 이동한다
Then 선택한 <이름> 항목이 체크아웃 페이지에 표시된다
#149

Footnotes

  1. polyfill.io 사용 중 dynamic import() 쪽에서 ie11 symbol 에러가 발생한다. 이슈는 등록 되어 있는데 안 고쳐줄 것 같다.

  2. 모바일 사파리 하단 영역 터치 안되고 네비바가 활성화 됨

  3. ios 크롬, 사파리 폰트 사이즈 차이

  4. 공백을 해당 유니코드로 replace해서 문제 해결

  5. react-error-overlay가 잘못 동작할때 특정 버젼으로 고정해서 해결

  6. resolutions로 우회하는 방법으로 해결

  7. 모달 내부에 존재하는 <input /> 포커스가 안됨

#157
interface MathFn {
  (a: number, b: number): number
}
const sum: MathFn = (a, b) => a + b

Footnotes

  1. function declarations, function expressions, arrow functions, methods등 TypeScript에서 함수를 선언하는 다양한 방법들.

#15
import isAfter from 'date-fns/isAfter';

isAfter(new Date(), new Date(DATE))

날짜 비교 할 일이 있어서 별 생각 없이 new Date를 때렸는데 safari에서 안되는 문제가 발견되었다. 콘솔을 확인해보니 yyyy-MM-dd HH:mm:ss 해당 형태의 포멧 에서는 안된다. 평소에 new Date 보다는 moment나 date-fns같은 라이브러리를 당연하게 써오다 보니 몰랐다. 그런데 또 다른 생각을 해보자면 저런 문제가 있기 때문에 더 적극적으로 라이브러리를 사용해야 한다는 게 함정.

import isAfter from 'date-fns/isAfter';
import format from 'date-fns/format';

isAfter(new Date(), format(DATE))
#159

Footnotes

  1. type은 인라인이 가능해서 용량이 커지는 문제가 발생할 수 있다.

  2. 두 가지 구성을 비교 및 대조하고 각 구성을 언제 사용해야 하는지에 대한 가이드

#16

Google Optimize에서 Redirect 테스트를 진행했는데 기능 구현은 문제없는데 세션수가 안 잡히는 문제가 발생했다.

이번에 문제되었던 부분은 query 처리를 인지하지 못했던 점이었다. Optimize에서 redirect 처리가 이루어질 때 query에 정보(예. utm_expid)들이 추가되는데 리다이렉트 url 변경하는 부분이 있었고 optimize에서 참조되어야 할 query를 날려버리면서 측정이 불가능했던 것. 결국 replace 실행 코드를 이전 query를 assign 시켜주도록 변경해서 해결.

여담이지만 Charles를 사용하고 있어서 배포 없이 테스트 가능했던 점도 같이 메모.

#160

connect가 실행된 컴포넌트를 (enzyme) mount로 테스트한 경우 실패 케이스가 발생해서 찾아본 내용들 인 것 같은데 정확하게 기억이 안남. shallow로 변경했더니 이번에는 ref를 못쓰는 문제도 있었다고 하고… 여담이지만 컴포넌트를 명확하게 정의하고 분리해서 테스팅 스트레스를 줄이는 게 중요합니다.

#161

brew로 ffmpeg 설치할때1

brew install ffmpeg $(brew options ffmpeg | grep -vE '\s' | grep -- --with-' | tr '\n' ' ')

Footnotes

  1. brew install ffmpeg with all options

#162

youtube-dl 업데이트가 안되면서 다운로드 속도가 굉장히 저조하다. 그래서 yt-dlp로 일단 변경해서 사용 중.


여러 개 주소를 한꺼번에 받으려면 주소를 txt 파일로 저장하고 -a(-a, --batch-file FILE) 옵션을 추가하면 된다.

youtube-dl -a list.txt

가끔 중간에 에러가 발생하는 경우도 있는데 이럴 경우 ignore로 방어 코드를 설정하고 실행시키면 잘 된다.

youtube-dl -i, --ignore-errors

자막을 다운로드해야 된다면 —sub-lang 옵션을 추가한다. 자동 자막이 필요하다면 --write-auto-sub

youtube-dl --write-sub --sub-lang ko
youtube-dl --write-auto-sub

playlist를 다운 받을때 index도 파일에 포함시키고 싶을 경우1

-o '%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s'

기본적으로 평소 사용하는 alias 설정

alias dm="youtube-dl"
alias da="youtube-dl -x --audio-format mp3"

Footnotes

  1. https://github.com/ytdl-org/youtube-dl#output-template

#163

스트리밍 비디오 다운로드 할 경우 403 에러가 발생할때 해당 옵션으로 우회한다.1

youtube-dl --referer "URL" --user-agent "UA"
dmRef() {
  youtube-dl $1 --referer $1 --user-agent "UA"
}

Footnotes

  1. https://www.reddit.com/r/youtubedl/comments/g342nr/masterm3u8_http_error_403_forbidden_youtubedl_or/

#164
::-webkit-input-placeholder /* for (Chrome/Safari/Opera) */
:-ms-input-placeholder /* for IE. */
::-ms-input-placeholder /* for Edge (also supports webkit prefix) */

::-ms-clear {}
::-ms-reveal {}

#166

Footnotes

  1. form 내부 element에 enterkeyhint 속성을 설정해서 모바일 환경일 경우 submit 버튼 레이블에 힌트를 제공해서 ux를 개선시킬 수 있는 정보를 알려주고 있다.

  2. labelinput을 연결하는 방식과 주의사항을 알려주고 있다. 핵심은 pair로 만드는 것.

  3. form 디자인 할때 고려사항과 좋은 사례들을 정리한 글

#165

자동완성 기능은 편리할수도 있지만 비활성화가 필요한 경우도 있다. 하지만 chrome에서 무시되는 경우가 있는데…이게 거의 된다 지금은 안된다…되는 것 같다…이렇게 변질이 되는 것 같다.


Footnotes

  1. 가끔 들어가보면 여전히 진행중이다…

  2. 글쓴이는 결국 안되서 스크립트로 focus될때 readonly 속성으로 제어했다고 한다.

#168

Footnotes

  1. form 예제가 같이 첨부 되서 좀 더 이해하기 쉽다. 그리고 결론부분에 어느정도 공감되는게 (서비스 개발자 입장에서는) proxy의 장점을 끌어내는게 쉽지 않을 것 같다.

  2. 그래도 최대한 리얼월드스러운 예제코드들

  3. 이 글은 좀 더 스펙에 대한 설명을 같이 첨부

  4. getters/setters에서 proxy를 사용해 코드를 변환하는 과정을 설명해주고 있다.

#169

Footnotes

  1. JSDoc(주석)으로 타입 체크하는 방법을 소개

  2. TypeScript가 작동하는 방식과 유사하게 자바스크립트 코드에 유형 어노테이션과 유형 검사를 추가하는 데 JSDoc을 사용하는 방법을 설명

  3. 기존 JavaScript 코드에 대한 선언 파일(.d.ts)을 생성하여 TypeScript가 JavaScript 코드를 유형 검사할 수 있도록 하는 방법을 설명

  4. JSDoc 구문에 대한 포괄적인 참조

  5. TypeScript를 사용하는 대신 JSDoc을 사용하여 JavaScript 코드에 타입 어노테이션을 추가할 때의 이점에 대해 설명. 경우에 따라 가볍고 유연한 대안이 될 수 있음. 2

  6. JSDoc 코멘트를 사용하여 React PropType에 타입 주석을 제공하는 방법 2

#17

Footnotes

  1. prefers-reduced-data 가능하다면

  2. 특정 문자 변경하기

  3. 텍스트 스타일

  4. 로컬 폰트 감지를 위한

  5. leading-trim은 텍스트 스타일을 보다 예측 가능하게 할 수 있도록 모든 글꼴에서 추가 공백을 제거할 수 있는 새롭게 제안된 CSS 속성입니다.

  6. Google 글꼴 지식을 사용하면 모든 기술 세트의 디자이너와 개발자가 목적에 따라 유형을 선택하고 사용할 수 있습니다.

  7. font-variant-numeric 속성을 사용하면 숫자, 분수 및 서수 표시에 대한 대체 글리프를 제어할 수 있습니다.

#171

가변 글꼴은 진보된 OpenType 사양입니다. CSS를 사용할때 가짜 굵기 또는 기울임꼴과 같은 브라우저 왜곡 에 대해 걱정하지 않고 단일 글꼴 파일에 포함된 모든 스타일에 액세스할 수 있습니다.

이전에는 여러 스타일을 사용한다는 것은 모든 너비, 두께 또는 기울임꼴에 대해 하나씩 여러 파일을 로드하는 것을 의미했습니다. 이로 인해 디자인 표현력(사용된 글꼴 수)과 웹사이트 성능(다운로드할 데이터의 양) 사이에 긴장감이 생겼습니다. 가변 글꼴을 사용하면 전체 방정식이 변경됩니다.


Footnotes

  1. 다크모드에서 weight가 시각적으로 차이가 날수 있기때문에 그럴경우 변수로 분리해서 대응하는 방법

#173

autocomplete="one-time-code" 사용자가 SMS 메시지를 수신 할 때마다, 운영 체제는 SMS에서 OTP 구문을 분석하고 키보드는 OTP를 제안합니다. iOS, iPadOS 및 macOS의 Safari 12 이상에서만 작동하지만 해당 플랫폼에서 SMS OTP 환경을 쉽게 개선할 수 있는 방법이므로 사용하는 것이 좋습니다.

<input 
  type="text"
  inputmode="numeric"
  autocomplete="one-time-code"
  pattern="\d{6}"
  required
/>
const otp = await navigator.credentials.get({
  otp: {
    transport: ['sms']
  }
})

input.value = otp.code

참고

#167

Footnotes

  1. Svelte , Redis 및 Rust(Tauri)를 이용해서 앱을 만든 과정을 설명

  2. Go로 구현 가능한 Webview, Lorca, Electron 세가지 옵션을 비교한 글. Webview, Lorca가 현실적이라는 의견.

  3. 간단한 설정과 커스터마이징 가능한 JS, CSS를 추가해서 웹뷰를 실행시켜주는 앱

  4. React, Vue 기반으로 작성이 가능한 라이브러리 비교

  5. 노드 기반의 경량화된 프레임웤

#176
#179

Footnotes

  1. 어떻게 arc를 그릴 수 있는지 d3를 사용해서 알려주고 있다.

  2. 드롭다운 이라는 용어(단어)가 갖고 있는 모호함이 있어서 혼란을 줄 수 있으므로 사용을 중지 하고, 대신 원하는 컨트롤을 정확하게 설명하는 용어를 선택하도록 정리한 글.

  3. 애니메이션 데모를 시연할 경우 가상의 마우스 움직임이 필요한 경우가 있어서 noisejs를 사용해서 시뮬레이션 하는 방법을 소개하고 있다.

  4. 2021 완벽한 햄버거

  5. css로 구현하는 아날로그, 디지털 시계. 움직임도 가능하다.

  6. 사용자로 부터 이미지(미디어)를 입력받기 위해 인풋, 클립보드, 카메라에 접근 가능한 방법을 알려주고 있다.

  7. contains를 사용해서 outside 클릭 했을때 닫힘 상태 만들기

  8. 카운트다운 타이머를 이미지 형식으로 제공하고 있다

  9. 사용자가 어떤 영역에 머무르고 있는지 체크할 수 있다

  10. GUI 프로그래밍의 대표적인 과제라 할수있는 7가지 작업을 정의

  11. Tailwind CSS와 통합되도록 설계된 액세스 가능한 UI 구성 요소입니다.

  12. 정렬, 필터링 및 페이징 기능이 있는 테이블 UI 구현을 어떻게 접근하는지에 대한 설명

#18

Footnotes

  1. <filter />를 이용하는 방법으로 좀 더 효과적인 스타일 작업이 가능하다는 걸 shadow 효과 예제로 정리한 글

  2. 애니메이션(@keyframes)이 결합된 svg를 border-image로 사용해서 만든 UI

  3. textPath를 사용해서 텍스트 흐름을 변형하는 방법

  4. path 속성값을 제어해서 모핑효과 주는 방법

  5. a11y

  6. favicon 이미지를 svg로 사용했을때 장점

  7. 디자인툴에서 output되는 svg코드 중심으로 설명

#177

Footnotes

  1. caniuse 이메일 버젼

  2. 이메일 템플릿 에디터

  3. Unlayer 이메일 에디터 React 래퍼 컴포넌트

  4. Unlayer 이메일 에디터의 간단한 설치, 사용기

  5. HTML 이메일 작업 가이드

  6. 다크모드 가능한 클라이언트와 어떻게(@media (prefers-color-scheme: dark), [data-ogsc]) 하면 되는지 정리한 글

#180

이 리포지토리에는 Adobe Photoshop CC의 Generator 확장성 레이어용 플러그인이 포함되어 있습니다. 이 플러그인을 사용하면 Photoshop 파일에서 이미지 자산을 더 쉽게 내보낼 수 있습니다. 사용자는 파일 이름과 같은 구문을 사용하여 내보내려는 문서 및 이름 레이어(또는 레이어 그룹 또는 스마트 개체)에 대한 이미지 자산 생성을 활성화하기만 하면 됩니다. 그런 다음 Generator는 이러한 레이어를 감시하고 변경될 때마다 디스크의 해당 자산을 자동으로 업데이트합니다.


하지만…그닥 부정적이다.

PhotoShop과 Node.js를 함께 사용할 수 있다는 점은 매우 훌륭하지만 Node.js와 PhotoShop을 동일한 서버에 유지하는 것은 실용적이지 않습니다. Plus Photoshop은 Windows 및 MacOS에서만 실행됩니다.

#182

GitHub - meltingice/psd.js: A Photoshop PSD file parser for NodeJS and browsers

psd 파서. 이를 통해 관리 가능한 트리 구조에서 Photoshop 문서로 작업하고 다음과 같은 중요한 데이터를 찾을 수 있습니다.

  • 문서 구조
  • 문서 크기
  • 레이어/폴더 크기 + 위치 지정
  • 레이어/폴더 이름
  • 레이어/폴더 가시성 및 불투명도
  • 글꼴 데이터( psd-enginedata 를 통해 )
    • 텍스트 영역 내용
    • 글꼴 이름, 크기 및 색상
  • 색상 모드 및 비트 심도
  • 벡터 마스크 데이터
  • 병합된 이미지 데이터
  • 레이어 구성 요소
#183

blob

new Blob(array, { type : 'text/html' })

파일 다운로드 생성이 잘 안되서 확인해보니 type 지정을 안했다.

#187

포토샵 스크립트 관련 링크(액션과는 다르다 액션과는!) 정리. 액션도 편하기는 하지만 개발적인 관점에서 보자면 너 유연한 작업이 가능한 스크립트를 작성하는게 맞다.

유튜브 강의 - 보면 시각적으로 뭐가뭔지 나오니까 이해가 바로 된다

어도비 공식 문서 - 스펙 정의에 가까워서 예제코드로 감을 익히고 백과사전용으로 사용하는게 좋을 것 같다.

그래서 어떻게 작성하면 되는거야?…에 대한 예제들. 아마도 es3로 작성해야하는 부분때문에 문법은 복잡해 보인다.

#186
# .gitignore가 규칙이 적용되지 않을때
git rm -rf --cached .
git add .

#189

RUM Browser Monitoring

premiumSampleRate The percentage of tracked sessions with Browser Premium pricing features: 100 for all, 0 for none. For more details about premiumSampleRate, see the sampling configuration.

#190

계산기 응용 프로그램의 다양한 측면을 설명합니다.


Footnotes

  1. 사용자가 평범한 문장을 사용하여 계산을 수행할 수 있는 고유한 인터페이스가 있는 계산기 응용 프로그램인 Soulver를 만든 방법을 알려주는 내용. 제작자가 직면한 어려움과 Soulver를 돋보이게 하는 기능에 대해 설명합니다.

  2. 계산기 응용 프로그램의 설계 원칙에 대해 설명합니다. 다양한 유형의 계산기와 사용자 친화적인 계산기 앱을 만드는 핵심 디자인 요소에 대한 자신의 생각을 공유합니다.

  3. 자연어 인터페이스와 단위 환산, 통화 환산, 계산 내역과 같은 고급 기능을 제공하는 Mac용 계산기 애플리케이션인 Numi의 웹사이트. Numi의 기능 및 가격에 대한 정보가 포함.

  4. Parsify Desktop은 엔지니어, 과학자 및 학생이 사용하도록 설계되었으며 복잡한 계산 및 단위 변환을 지원합니다. (자연어 인터페이스도 제공)

  5. math.js

#19
const obj = {
  a: 1,
  b: 2,
}

console.log(obj[['a']]) // 1
console.log(obj[['b']]) // 2

이게 되네 🤔

#191
# 아마도 git을 사용하고 있을테니까 `git mv`를 사용해서 변경해주자
find src -type f | grep "\.[jt]s$" | xargs -n1 grep -HE "^[^*\n]*(<\/?[a-zA-Z]*>)[\s\w]*$" | cut -d: -f1 | uniq | awk '{print "git mv "$1" "$1"x"}' | sh

Footnotes

  1. Vite가 JSX 처리를 위해 .jsx 확장자를 요구하는 이유는 대부분의 경우 일반 .js 파일이 브라우저에서 작동하기 위해 전체 AST 변환이 필요하지 않아야 하기 때문입니다. .js 파일에서 JSX를 허용한다는 것은 제공되는 모든 파일이 JSX를 포함하는 경우에 대비하여 전체 AST 처리되어야 함을 의미합니다.

  2. .jsx 또는 .js를 사용해야할 경우에 대한 답변들이 소개되어 있다.

#197

chromatic


Footnotes

  1. cli를 사용할 경우 CHROMATIC_PROJECT_TOKEN.env에서 관리하면 된다

#199

Footnotes

  1. async 함수를 실행할때 리턴과 관련된 예제로 해당 기능을 설명해주고 있다.

  2. Streaming 작업 시 비동기처리 방법을 알려주고 있는데…경험이 없어서 일단 메모.

  3. try/catch문을 사용할 경우 결과값을 다시 변수에 할당 해야 하는데 그럴 경우 문법적 간결함이 애매해 지기 때문에 .catch로 체이닝해서 다음 구문에 조건문으로 처리하는 방법을 알려주고 있다.

  4. 페이지 로딩 중 unmount 될때 이미지 요청 취소 기능을 위해 찾아봤다.

#2

지도같은 화면 구현이 보여서 확인해보니 드래그로 구현이 되어 있었다. 그래서 혹시 지도로 구현된게 없을까 해서 찾아본 자료들. leafletjs 보니까 Non-geographical maps 라는 표현을 사용했다.


Leaflet 공식 예제

활용 및 스토리맵

대형 이미지 타일링 및 플러그인


OpenLayers 관련 자료


네이버 지도 API 및 기타 지도 유형

#20
#200
<div
  dangerouslySetInnerHTML={{
    __html: `
      <img src="http://unsplash.it/100/100?random" onload="console.log('you got hacked');" />
    `,
  }}
/>

가끔 아주 가끔 이상한 일을 해야할때가 있는데 그럴때는 이렇게 하면 된다.


#193

lazyimport했을 경우 ChunkLoadError가 발생하는데 이럴 경우 어떻게 대응할 수 있는지 정리해둔 글들.

#203
# 크롬 익스텐션이 설치되는 경로는 다음과 같다
/Users/USERNAME/Library/Application Support/Google/Chrome/Default/Extensions
function isInstalled(extensionId) {
  return new Promise((resolve, reject) => {
    const img = new Image()

    img.src = `chrome-extension://${extensionId}/icon-128.png` // 해당 리소스가 `web_accessible_resources`에 선언되어 있는지 확인이 필요하다.
    img.onload = () => {
      resolve(true)
    }
    img.onerror = () => {
      reject()
    }
  })
}
#207

Clear CloudFront Cache with AWS CLI | bobbyhadz

/**
 * CloudFront 배포에 대한 캐시를 무효화하려면 경로와 함께 명령을 실행합니다
 * 
 * @param {object} params
 * @param {string} params.distributionId
 * @param {string} params.paths
 * @returns {CreateInvalidationResult}
 */
function createInvalidation({
  distributionId,
  paths = `"/*"`
}) {
  const result = $`aws cloudfront create-invalidation --distribution-id ${distributionId} --paths ${paths}`
  return result
}
#209

드래그 앤 드롭 기능이나 위지윅 편집기를 사용하여 사용자 인터페이스와 레이아웃을 구축하기 위한 도구와 라이브러리입니다. 이러한 도구는 수동으로 코드를 작성하지 않고도 복잡한 UI 디자인과 레이아웃을 만드는 과정을 간소화하는 것을 목표로 합니다.


Footnotes

  1. 랜딩 페이지, 마케팅 사이트, 전자상거래 스토어를 만들 수 있는 드래그 앤 드롭 빌더

  2. 직관적인 UI로 복잡한 반응형 레이아웃, 프로토타입 및 랜딩 페이지를 제작

  3. React 기반 비주얼 에디터

  4. 반응형 디자인을 만들고 자동으로 코드를 생성

  5. 실시간 피드백을 통해 UI 컴포넌트를 빌드할 수 있는 React 기반 비주얼 에디터

  6. React 및 ImmutableJS를 사용하여 반응형 UI 레이아웃을 만들기 위한 오픈 소스 드래그 앤 드롭 레이아웃 빌더입니다.

#21

npm 레지스트리에서 특정 패키지에 대한 메타데이터 및 기타 정보를 볼 수 있습니다. 이 명령으로 패키지의 최신 버전, 패키지의 종속성, 작성자 및 라이선스 정보, 기타 세부 정보를 확인할 수 있습니다.

# 이전 버전 리스트를 확인하고 싶을때
npm view cowsay versions

# 각 버전이 게시된 시간을 확인
npm view cowsay time


Footnotes

  1. 설치된 npm 패키지의 버전을 확인하는 방법에 대한 가이드

#210

Lambda@Edge (Node) 함수 작성 시 npm module을 사용하는 방법. esbuild를 사용하면 편하다.

{
  "scripts": {
    "build": "esbuild --bundle --minify --platform=node --target=node12 --outdir=build main.js",
    "export": "cd build && zip main.js.zip main.js"
  }
}

#208
// 현재 실행된 stage 값을 참조하기
// 하지만 스크립트를 따로 작성하는게 더 효율적으로 보인다.

{
  "scripts": {
    "dev:hello": "echo ${npm_lifecycle_event//dev:/}"
  }
}


Footnotes

  1. npm 스크립트의 이름을 얻는 방법에 대한 질문과 npm_lifecycle_event 환경 변수를 사용하는 방법으로 제안

  2. package.json 파일에서 스크립트를 사용하는 방법에 대한 개요, 기본 제공 환경 변수 목록도 포함

#213

Footnotes

  1. 자바스크립트에서 객체는 항상 참조를 통해 전달되므로 전달된 객체는 안전하지 않은 문제가 있어서 함수형 프로그래밍을 도입하고 ramda를 이용해서 읽기 쉽고 유지 관리하기 쉬운 코드를 작성한 예를 보여주고 있다.

  2. 1편에 이어서 2편에서는 ramda로 직접적인 예시를 제공하고 마지막에 메모리 이슈에 대한 우려를…

#216

BDD는 무엇을 의미합니까?

BDD는 시스템의 원하는 동작을 협력적으로 지정하는 접근 방식입니다. 행동의 일부가 합의될 때마다 우리는 그 행동을 구현할 코드의 개발을 “추진”하기 위해 해당 사양을 사용합니다.

BDD의 세 가지 관행은 무엇이며 스토리에 어떤 순서로 적용합니까?

우리는 스토리에 필요한 행동의 범위를 공동으로 발견 하는 것으로 시작합니다. 일단 우리가 행동에 동의하면 우리는 비즈니스가 읽을 수 있는 언어로 사양을 공식화 합니다. 마지막으로 공식화된 사양을 자동화 하여 시스템이 실제로 예상대로 작동하는지 확인합니다.

Cucumber와 BDD는 어떤 관련이 있습니까?

Cucumber는 문서를 이해하고 자동화된 테스트로 변환하는 도구입니다.

BDD는 세 가지 방식으로 구성된 협업적 접근 방식입니다. BDD 실무자는 Cucumber를 사용하여 문서를 자동화할 수 있습니다.

“살아있는 문서”의 특별한 점은 무엇입니까?

문서가 애플리케이션의 동작과 동기화되지 않을 때 자동으로 알려 주기 때문에 “살아있는 문서”라고 부릅니다. 그것이 특별한 점입니다.

완료에 대한 정의의 일부로 이를 검토할 수 있지만 자동으로 유효성이 검사되지 않더라도 작성한 모든 사양 문서에 대해서도 마찬가지입니다.

그것은 자동화된 테스트에 의해 생성 되지 않습니다 - 여전히 작성해야 합니다! 자동화된 테스트는 귀하가 작성한 내용이 사실인지 아닌지를 알려줍니다.

이를 위한 변경 제어 프로세스가 있을 수 있습니다. 설명하는 코드와 함께 소스 제어에 유지하는 것이 좋습니다. 그러나 다시 말해서 특별한 것은 아닙니다. Word 문서에 대해 놀랍도록 투명한 변경 제어 프로세스를 가질 수 있지만 여전히 완전히 구식이고 잘못된 것일 수 있습니다.

#218

Footnotes

  1. 시스템의 동작을 모델링하는 형식주의인 스테이트차트를 설명. 기본 개념과 이점을 다루며 실제로 스테이트차트를 사용하는 방법에 대한 예제.

  2. 공식적인 방법을 사용하여 사용자 인터페이스(UI)를 지정하고 그 정확성을 보장하는 방법을 설명. UI를 위한 공식 언어, 모델 검사 및 속성 기반 테스트와 같은 주제.

  3. 소프트웨어 개발에 널리 사용되는 모델링 언어인 UML(통합 모델링 언어)의 상태 머신 다이어그램에 대한 개요를 제공. 상태 머신 다이어그램에 사용되는 그래픽 표기법을 다루고 그 사용 예제.

  4. 그래프비즈(Graphviz)를 소개. 그래프 시각화의 기본 개념, Graphviz 설치 및 사용 방법, 소프트웨어를 사용하여 시각화를 만드는 방법에 대한 예제.

#219

Footnotes

  1. 코드 리뷰는 비동기식 커뮤니케이션의 한 형태이므로 많은 컨텍스트(바디랭귀지 및 주고받을 기회 등)가 손실됩니다. 이것은 대부분의 원격회사인 우리에게 특히 중요하므로 피드백에 약간의 뉘앙스를 추가할 방법을 원했습니다.

  2. Feedback Ladder: ⛰ 산(Mountain) / 차단 및 즉각적인 조치 필요, 🧗‍♀️ 볼더(Boulder) / 블로킹, ⚪️ 자갈(Pebble) / 비차단하지만 향후 조치가 필요함, ⏳ 모래(Sand) / 논블로킹이지만 향후 고려 필요, 🌫 먼지(Dust) / 비차단, “받거나 놔두세요”

  3. 코드 리뷰 없이 신뢰를 기반으로 엔지니어링 문화를 구축한 방법. 다른 사람들의 상황이 자신에게 적용되는지 자문해볼 필요가 있다.

#221

Debouncing and Throttling Explained Through Examples | CSS-Tricks1

  • throttle: 함수가 호출되는 속도를 제한 (일정시간 동안)
  • debounce: 추가 호출 없이 일정 시간이 경과할 때까지 함수 실행을 지연하거나 제한 (마지막 이벤트만)

Footnotes

  1. 검색 입력, 스크롤 이벤트, 크기 조정 이벤트 등 디바운싱과 스로틀링이 유용할 수 있는 사례들

#22
- 프로젝트에 새롭고 반짝이는 기술보다는 지루한 기술을 선택하는 것이 좋습니다.
- 흥미로운 기술은 프로젝트에 불필요한 위험을 초래할 수 있습니다.
- 이미 확립되고 입증된 기술이 성공적인 결과로 이어질 가능성이 더 높습니다.
- 신기술을 둘러싼 과대 광고는 오해의 소지가 있습니다.
- 신기술의 수명을 예측하기는 어렵습니다.
- 지루한 기술은 화려하지 않을 수 있지만 안정적이고 신뢰할 수 있으며 예측 가능합니다.
- 최신 기술 유행보다는 비즈니스 문제 해결에 집중하세요.
- 사람과 프로세스에 투자하면 더 나은 결과를 얻을 수 있습니다.
- 지루한 기술을 선택하는 것은 보수적이거나 위험을 피하기 위한 것이 아니라 프로젝트의 장기적인 성공을 우선시하는 정보에 입각한 결정을 내리기 위한 것입니다.

Footnotes

  1. Dan McKinley :: Choose Boring Technology

  2. 항상 최신의 최첨단 솔루션을 쫓기보다는 확립되고 신뢰할 수 있는 기술을 사용하는 것을 지지하는 글. 특히 장기 프로젝트의 경우 기술 선택에서 안정성과 예측 가능성의 중요성을 강조.

#222

Core Web Vitals & Performance Metrics

LCP 최적화

이미지 및 리소스 로딩 최적화

사이트 속도 및 성능 최적화

CSS 기반 성능 최적화

서드파티 스크립트 및 자원 관리

Partytown 및 서드파티 스크립트 성능 개선

케이스 스터디: 웹 성능 개선 사례

메모리 및 성능 이슈 해결

#224

https://github.com/mobxjs/mobx/blob/mobx4and5/docs/best/actions.md

비동기 작업 작성

action 래퍼/데코레이터는 현재 실행 중인 함수에만 영향을 미치며 현재 함수에 의해 예약된(그러나 호출되지는 않은) 함수에는 영향을 미치지 않습니다! 즉, setTimeout, promise.then 또는 async 구성이 있고 해당 콜백에서 상태가 더 변경되면 해당 콜백도 action으로 래핑되어야 합니다! 비동기 작업을 만드는 방법에는 여러 가지가 있습니다. 어떤 접근 방식도 다른 접근 방식보다 엄밀히 말하면 이 섹션에서는 비동기 코드를 작성하기 위해 취할 수 있는 다양한 접근 방식을 나열합니다. 기본 예부터 시작하겠습니다.

약속

mobx.configure({ enforceActions: "observed" }) // don't allow state modifications outside actions

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        fetchGithubProjectsSomehow().then(
            (projects) => {
                const filteredProjects = somePreprocessing(projects)
                this.githubProjects = filteredProjects
                this.state = "done"
            },
            (error) => {
                this.state = "error"
            }
        )
    }
}

위의 예에서는 fetchGithubProjectsSomehow 프라미스에 전달된 콜백이 fetchProjects 작업의 일부가 아니므로 예외가 발생합니다. 작업은 현재 스택에만 적용됩니다.

첫 번째 간단한 수정은 작업에 대한 콜백을 추출하는 것입니다. (올바른 this를 얻으려면 action.bound를 사용한 바인딩이 중요합니다!):

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        fetchGithubProjectsSomehow().then(this.fetchProjectsSuccess, this.fetchProjectsError)
    }

    @action.bound
    fetchProjectsSuccess(projects) {
        const filteredProjects = somePreprocessing(projects)
        this.githubProjects = filteredProjects
        this.state = "done"
    }

    @action.bound
    fetchProjectsError(error) {
        this.state = "error"
    }
}

이것은 명확하고 명시적이지만 복잡한 비동기 흐름에서는 약간 장황해질 수 있습니다. 또는 ‘action’ 키워드로 promise 콜백을 래핑할 수 있습니다. 다음과 같이 이름을 지정하는 것이 권장되지만 필수는 아닙니다.

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        fetchGithubProjectsSomehow().then(
            // inline created action
            action("fetchSuccess", (projects) => {
                const filteredProjects = somePreprocessing(projects)
                this.githubProjects = filteredProjects
                this.state = "done"
            }),
            // inline created action
            action("fetchError", (error) => {
                this.state = "error"
            })
        )
    }
}

runInAction 유틸리티

인라인 작업의 단점은 TypeScript가 해당 작업에 유형 추론을 적용하지 않으므로 모든 콜백을 입력해야 한다는 것입니다. 전체 콜백에 대한 작업을 만드는 대신 작업에서 콜백의 일부를 수정하는 상태만 실행할 수도 있습니다. 이 패턴의 장점은 ‘액션’으로 장소를 어지럽히지 않도록 권장한다는 것입니다. 오히려 전체 프로세스의 마지막에 가능한 한 많은 상태 수정을 넣습니다.

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        fetchGithubProjectsSomehow().then(
            (projects) => {
                const filteredProjects = somePreprocessing(projects)
                // put the 'final' modification in an anonymous action
                runInAction(() => {
                    this.githubProjects = filteredProjects
                    this.state = "done"
                })
            },
            (error) => {
                // the alternative ending of this process:...
                runInAction(() => {
                    this.state = "error"
                })
            }
        )
    }
}

runInAction은 첫 번째 인수로 이름을 지정할 수도 있습니다. runInAction(f)은 사실 action(f)()에 대한 설탕일 뿐입니다.

비동기 / 대기

비동기/대기 기반 함수는 작업을 시작할 때 처음에 혼란스러워 보일 수 있습니다. 어휘적으로는 동기 함수에 나타나기 때문에 @action이 전체 함수에 적용된다는 인상을 줍니다. 물론 그렇지 않습니다. async / await는 약속 기반 프로세스에 대한 구문상의 설탕일 뿐입니다. 결과적으로 @action은 첫 번째 await까지 코드 블록에만 적용됩니다. 그리고 각 await 후에 새로운 비동기 함수가 시작되므로 각 await 후에 상태 수정 코드를 작업으로 래핑해야 합니다. 여기에서 runInAction이 다시 유용합니다.

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending" // "pending" / "done" / "error"

    @action
    async fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        try {
            const projects = await fetchGithubProjectsSomehow()
            const filteredProjects = somePreprocessing(projects)
            // after await, modifying state again, needs an actions:
            runInAction(() => {
                this.state = "done"
                this.githubProjects = filteredProjects
            })
        } catch (error) {
            runInAction(() => {
                this.state = "error"
            })
        }
    }
}

흐름

그러나 더 좋은 접근 방식은 내장된 ‘흐름’ 개념을 사용하는 것입니다. 그들은 발전기를 사용합니다. 처음에는 무섭게 보일 수 있지만 async / await와 동일하게 작동합니다. async 대신 function *을 사용하고 await 대신 yield를 사용하세요. ‘flow’의 장점은 구문적으로 async / await(다른 키워드 사용)에 매우 가깝고 비동기 부분에 수동 작업 래핑이 필요하지 않아 매우 깨끗한 코드를 생성한다는 것입니다.

‘flow’는 데코레이터가 아닌 함수로만 사용할 수 있습니다. flow는 MobX 개발 도구와 깔끔하게 통합되어 비동기 기능의 프로세스를 쉽게 추적할 수 있습니다.

mobx.configure({ enforceActions: "observed" })

class Store {
    @observable githubProjects = []
    @observable state = "pending"

    fetchProjects = flow(function* () {
        // <- note the star, this a generator function!
        this.githubProjects = []
        this.state = "pending"
        try {
            const projects = yield fetchGithubProjectsSomehow() // yield instead of await
            const filteredProjects = somePreprocessing(projects)
            // the asynchronous blocks will automatically be wrapped in actions and can modify state
            this.state = "done"
            this.githubProjects = filteredProjects
        } catch (error) {
            this.state = "error"
        }
    })
}

흐름을 취소할 수 있습니다.

흐름은 취소 가능합니다. 즉, 반환된 약속에서 ‘cancel()‘을 호출할 수 있습니다. 이렇게 하면 생성기가 즉시 중지되지만 모든 finally 절은 계속 처리됩니다. 반환된 약속 자체는 메시지가 FLOW_CANCELLEDFlowCancellationError(이 오류는 패키지에서 내보냄) 인스턴스로 거부됩니다. 또한 제공된 인수가 ‘FlowCancellationError’인 경우에만 true를 반환하는 ‘isFlowCancellationError(error)’ 도우미도 내보냅니다.

#223
interface PaginationProps {
  /** 현재 페이지 번호 */
  currentPage: number;
  /** 전체 페이지 수 */
  totalPages: number;
  /** 페이지 변경 시 호출되는 콜백 함수 */
  onPageChange: (pageNumber: number) => void;
  /** 이전/다음 페이지 링크 표시 여부 (선택적) */
  showPreviousNext?: boolean;
  /** 페이지 번호 링크 표시 여부 (선택적) */
  showPageNumbers?: boolean;
  /** 페이지당 항목 수 (선택적) */
  pageSize?: number;
}

하지만 너무 많은 기능이 필요없기 때문에 최대한 단순한 컴포넌트를 만들었다.

#227
#229

다음은 Raspberry Pi를 사용하여 날씨 정보를 표시하는 프로젝트의 두 가지 예입니다. 이 두 프로젝트 모두 Raspberry Pi를 사용하여 날씨 표시 또는 날씨 앱을 만드는 방법을 보여줍니다. 또한 Python 스크립트 또는 웹 기반 애플리케이션을 사용하여 날씨 데이터를 가져오고 표시하는 다양한 방법을 보여줍니다. 이 프로젝트는 Raspberry Pi를 사용하여 자신만의 날씨 디스플레이 또는 앱을 구축하는 데 관심이 있는 모든 사람에게 좋은 출발점이 될 수 있습니다.1


Footnotes

  1. https://weather-mu.vercel.app/

  2. 작은 화면에 현재 시간과 시간별 일기 예보를 표시하는 프로젝트입니다. 이 프로젝트는 Raspberry Pi Zero W와 1.3인치 OLED 디스플레이를 사용하며 OpenWeatherMap API에서 날씨 데이터를 가져와 화면에 표시하는 Python 스크립트로 구동됩니다.

  3. Preact, Netlify Functions를 사용하여 OpenWeatherMap API에서 날씨 데이터를 가져옵니다.

#23
yarn add -D react-app-rewired customize-cra
// package.json
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test"
},
// .babelrc
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "legacy" }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}
// config-overrides.js
const { useBabelRc, override } = require('customize-cra')

module.exports = override(useBabelRc())


CRACO로 구성하는게 지금은 맞는 것 같다.

#228

Footnotes

  1. 거의 동일한 고민. 하지만 나는 swc를 선택

  2. 테스트중인데 별일 없다면 무난하게 진행 되는 것 같다. 목적은 역설적이게도 데코레이터를 없애기 위한.

  3. monorepo로 구성하다보니 예상 못한 부분에서 의존성이 걸리면서 에러들이 발생한다.

#230

Footnotes

  1. textarea에서 특정 키입력이 발생할 경우 액션(멘션)을 발생시키는 기능

  2. 옜날에는 어떻게 한거지? 궁금해서 찾아봤습니다

  3. 입력필드에서 문자 트리거

  4. contenteditable 컴포넌트

  5. 의식의 흐름 속 갑자기 생각나버림

#232

좋은 시스템일수록 실제 서비스에 적용하기는 더 어렵다는 문제가 발생. 결국 필요한건 headless 인데 생각보다 많지 않고 어떤 선택이 좋은 선택인가에 대한 조심스러운 부분이 있다.


Footnotes

  1. 참고

#233
# `System limit for number of file watchers reached`

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

#234
# 빌드 시 timestamp를 참조하기 위한 방법
timestamp=$(date +%s) # 1670759329
# 하지만 현실에서는 `TURBO_HASH`값을 참조하는 방식을 `.env`에서 사용
REACT_APP_VERSION=1.0.0-${TURBO_HASH}


Footnotes

  1. 스크립트에서 현재 타임스탬프를 특정 형식의 변수로 만드는 방법에 대한 질문. 특정 형식 문자열과 함께 날짜 명령을 사용하여 타임스탬프를 만들고 변수에 저장하도록 하는 답변들…

  2. dotenv-expand는 .env 파일에서 환경 변수를 확장하기 위한 모듈입니다. 중첩 변수 확장 및 셸 명령 실행과 같은 더 많은 고급 기능을 사용 가능.

#235
# 비디오 자르기
ffmpeg -i INPUT.mp4 -ss 00:00:00 -to 00:01:00 -c:v copy -c:a copy OUTPUT.mp4

#237

Footnotes

  1. 일반 텍스트 파일을 데이터 소스로 사용하여 간단한 웹 애플리케이션을 구축하고 배포하기 위한 플랫폼. 백엔드 서버나 데이터베이스 없이 배포 가능. 텍스트 파일에서 데이터를 읽고 쓰고 검색하는 데 사용할 수 있는 간단한 REST API를 제공.

  2. JavaScript 애플리케이션용 경량 데이터베이스 엔진. 브라우저에서 데이터를 저장, 쿼리 및 조작하기 위한 간단하고 직관적인 API를 제공하며 챗봇, 검색 엔진 및 게임을 포함한 다양한 유형의 애플리케이션을 구축하는 데 사용 가능. 이 데이터베이스는 오프라인에서 작동하도록 설계되어 PWA(Progressive Web Application) 및 기타 유형의 클라이언트측 애플리케이션을 구축하는 데 이상적.

#238

데이터 시각화, 교육 목적 또는 단순한 재미를 위해 자신만의 대화형 지구본을 구축하는 데 관심이 있는 모든 사람에게 도움이 될 수 있습니다. 그들은 지구본을 만들기 위한 다양한 접근 방식과 기술을 보여주고 지구본 데이터로 작업할 때의 문제와 기회에 대한 귀중한 통찰력을 제공합니다.


Footnotes

  1. WebGL과 Three.js를 사용하여 지구본을 렌더링하고 Mapbox를 사용하여 데이터를 표시하는 지구본을 디자인하고 개발하는 과정을 설명합니다.

  2. GitHub 팀이 전 세계 GitHub 사용자의 위치를 보여주는 대화형 지구본을 구축한 방법을 설명합니다. 이 게시물은 WebGL 및 d3-geo 라이브러리를 사용하여 시각화를 생성하는 지구본 구축의 기술적 문제에 대해 자세히 설명합니다.

  3. Gatsby 서버리스 기능을 사용하여 국제 우주 정거장(ISS)의 위치를 추적하는 대화형 지구본을 구축하는 방법에 대해 설명합니다. 이 기사는 ISS의 위치를 표시하기 위해 Three.js 및 satellite.js를 사용하는 지구본 구축의 기술적 세부 사항을 다룹니다.

  4. 개발자가 자신만의 대화형 지구본을 만들 수 있는 경량 WebGL 지구본 라이브러리입니다.

#24
version: 1
applications:
  - frontend:
      phases:
        preBuild:
          commands:
            # AWS Amplify 에서 모노레포 구조를 사용 할 경우 root 레벨로 올라가서 install
            - cd ../../
            - echo "$PWD"
            - yarn install
        build:
          commands:
            - echo "$PWD"
            # 현재 root로 이동한 상태이므로 $AMPLIFY_MONOREPO_APP_ROOT를 바로 참조하도록 설정
            - if [ $NODE_ENV_VARIABLES = ".env.development" ]; then cat "./$AMPLIFY_MONOREPO_APP_ROOT/$NODE_ENV_VARIABLES" > "./$AMPLIFY_MONOREPO_APP_ROOT/.env.production"; fi
            - yarn run "build:$AMPLIFY_MONOREPO_APP"
      artifacts:
        baseDirectory: build
        files:
          - '**/*'
      cache:
        paths:
          - node_modules/**/*
    appRoot: apps/app

#231
async function loader() {
  const family = 'Material Symbols Outlined'
  const source =
    'url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v75/kJEhBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oFsLjBuVY.woff2)'
  const fontFace = new FontFace(family, source)

  await fontFace.load()
}

CSS Font Loading API 는 비교적 새로운 웹 플랫폼 추가 기능입니다. API가 도입되기 전에 웹 개발자는 일반적으로 CSS에 의존하여 웹 글꼴을 로드해야 했으며 이로 인해 로드와 성능 문제가 발생할 수 있었습니다. 개발자는 로드 방법을 더 잘 제어할 수 있을 뿐만 아니라 오류를 처리하고 웹 사이트에서 사용되는 시기와 방법을 제어할 수 있습니다. 전반적으로 CSS Font Loading API는 웹 개발자에게 글꼴이 정확하고 효율적으로 로드 되도록 하는 강력한 도구를 제공하는 동시에 글꼴 로드 및 사용에 대한 더 많은 제어 기능을 제공합니다.

#243

전자 상거래 데이터 생성 및 테스트 가능한 API


Footnotes

  1. 전자 상거래 데이터를 생성하기 위한 사용하기 쉬운 REST API를 제공. 이 API는 전자 상거래 애플리케이션에서 작업하는 개발자와 테스터에게 유용할 뿐만 아니라 데이터 분석 및 시각화 도구를 위한 샘플 데이터를 생성하는 데 유용.

#245
  • Human Interface Guidelines - Human Interface Guidelines - Design - Apple Developer: 디자인 원칙은 사용자 경험을 향상시키기 위한 기본 가이드라인입니다. Apple의 Human Interface Guidelines는 애플 제품을 위한 인터페이스 디자인에 대한 원칙과 모범 사례를 제공합니다. 예를 들어, 간결하고 일관된 디자인, 직관적인 상호 작용, 정보의 집중화, 애니메이션 및 트랜지션의 적절한 사용 등을 다룹니다. 이러한 가이드라인을 따르면 사용자들이 애플 제품과 앱을 쉽게 이해하고 사용할 수 있게 됩니다.
  • All WCAG 2.1 Techniques | WAI | W3C: 접근성은 모든 사용자가 웹 콘텐츠에 접근하고 상호 작용할 수 있도록 보장하는 중요한 측면입니다. 웹 접근성은 장애를 가진 사용자, 고령자, 비전 및 청각 장애가 있는 사용자, 키보드 사용자 등을 고려하여 웹 콘텐츠가 포용적이고 이용 가능한지를 확인합니다. WCAG는 웹 접근성을 높이기 위한 일련의 표준과 기술을 제공합니다. 텍스트 대체, 키보드 접근성, 명도 대비, 컨텐츠의 가독성, 사용자 인터페이스 컨트롤의 명확성 등을 다룹니다. WCAG는 웹 개발자와 디자이너들이 웹 콘텐츠를 접근 가능하게 만들기 위한 방법과 기술을 제시합니다.

#247

npm을 사용하여 패키지의 여러 버전을 설치하려면 npm 설치 명령 뒤에 패키지 이름과 설치하려는 버전 번호를 사용하면 됩니다. 이 명령을 다른 버전 번호로 반복하여 여러 버전의 패키지를 설치할 수 있습니다.

yarn add react-tooltip-5@npm:react-tooltip@5.8.3
{
  "react-tooltip-5": "npm:react-tooltip@5.8.3"
}

#249

Immer가 생성하는 맵과 세트는 인위적으로 불변으로 만들어집니다. 즉, 프로듀서 외부에서 세트, 클리어 등과 같은 변경 메서드를 시도할 때 예외(throw an exception)가 발생합니다.

test('Map and Set', () => {
  const baseMap = new Map();

  const nextBaseMap = create(baseMap, (draft) => {
    draft.set('a', 1);
  });

  expect(nextBaseMap).toMatchInlineSnapshot(`
    Map {
      "a" => 1,
    }
  `);
});

#252


Footnotes

  1. 복잡한 워크플로의 생성 및 유지 관리를 간소화하여 기술적 구현 세부 사항보다는 비즈니스 로직에 집중할 수 있도록 하는 것을 목표로.

  2. 사용자 정의 노드와 인터페이스를 만들 수 있음

  3. 복잡한 다이어그램과 워크플로를 쉽게 만들 수 있도록 사용자 정의 및 확장 가능한 구성 요소 세트를 제공

  4. FFmpeg 사용 프로세스를 간소화하는 도구

  5. 시각적 인터페이스를 사용하여 복잡한 FFmpeg 명령줄을 생성하는 웹 기반 도구인 FFmpeg Graph의 데모 페이지

#253
const { Parser } = require('acorn')
const JSXParser = Parser.extend(require('acorn-jsx')())

const isReactComponent = Boolean(
  JSON.stringify(
    JSXParser.parse(fileContent, {
      sourceType: 'module',
      ecmaVersion: 'latest',
    })
  ).includes('JSXIdentifier')
)

#255

모듈 페더레이션과 단일 SPA는 모두 개발자가 독립적으로 개발된 여러 애플리케이션을 단일 시스템에 통합하여 마이크로프론트엔드 및 분리형 애플리케이션을 구축할 수 있도록 지원하는 자바스크립트 라이브러리입니다.

모듈 페더레이션은 개발자가 별도의 웹팩 빌드 간에 모듈을 공유할 수 있도록 하는 웹팩의 기능입니다. 이를 통해 개발자는 애플리케이션을 여러 개의 마이크로프론트엔드로 분할하여 각각 독립적으로 빌드 및 배포하면서도 각 마이크로프론트엔드 간에 기능과 상태를 공유할 수 있습니다. 이는 여러 애플리케이션에서 로드할 수 있는 공유 모듈을 생성하고 각 애플리케이션이 공유 모듈의 내보내기에 액세스할 수 있도록 설정하면 됩니다.

단일 SPA는 독립적으로 개발된 여러 애플리케이션이 하나의 애플리케이션으로 함께 작동할 수 있도록 하는 마이크로프론트엔드 구축을 위한 JavaScript 프레임워크입니다. 개발자가 애플리케이션을 마이크로프론트엔드로 정의하고 런타임에 동적으로 로드 및 통합할 수 있는 일련의 API를 제공하는 방식으로 작동합니다. 이를 통해 개발자는 보다 쉽게 확장하고 유지 관리할 수 있는 복잡하고 분리된 애플리케이션을 구축할 수 있습니다.

모듈 페더레이션과 단일 SPA는 모두 모듈식 분리형 애플리케이션을 구축하기 위한 강력한 도구입니다. 모듈 페더레이션은 독립적인 빌드 간에 모듈을 공유하는 데 더 중점을 두는 반면, 단일 SPA는 독립적으로 개발된 여러 애플리케이션을 단일 시스템으로 통합하는 데 더 중점을 둡니다. 하지만 이 두 가지를 함께 사용하면 더욱 강력하고 유연한 애플리케이션을 만들 수 있습니다.

https://module-federation.github.io/ https://single-spa.js.org/

#256
async function main({ onActivate, onError, onReading, onDenied }) {
  const result = await navigator.permissions.query({
    name: 'ambient-light-sensor',
  })

  if (result.state === 'denied') {
    onDenied()
    return
  }

  const ambientLightSensor = new AmbientLightSensor({ frequency: 20 })
  
  ambientLightSensor.addEventListener('activate', onActivate)
  ambientLightSensor.addEventListener('error', onError)
  ambientLightSensor.addEventListener('reading', () => {
    const ISO = 100
    const C = 250

    const EV = Math.round(Math.log2((ambientLightSensor.illuminance * ISO) / C))

    onReading(EV)
  })

  ambientLightSensor.start()
}


Footnotes

  1. 주변광 센서 인터페이스가 포함된 일반 센서 API를 사용하는 방법을 설명

#257
#259

Footnotes

  1. Adobe PDF Embed API를 WebAssembly로 구현한 내용을 설명하고 있다. 최근 포토샵의 사례도 그렇고 adobe가 이쪽에 꽤나 공을 들이고 있는 것 같다.

#26

https://github.blog/changelog/2022-06-06-view-commit-history-across-file-renames-and-moves/ https://stackoverflow.com/questions/72165668/losing-history-when-moving-files-with-git

파일 이름이 변경되거나 새 디렉터리로 이동되었지만 콘텐츠의 절반은 그대로인 경우, 커밋 기록은 이제 git log --follow와 유사하게 파일 이름이 변경되었음을 표시합니다.

#261
#266
  1. 프롬프트 작성을 위한 가이드라인: 모델에 명확하고 구체적인 지침을 작성하는 방법에 대한 원칙을 다룹니다.
  • 원칙 1: 명확하고 구체적인 지시사항 작성: 모델이 원하는 출력을 도출할 수 있도록 명확하고 구체적인 지시사항을 제공해야 합니다.
  • 원칙 2: 모델에게 ‘생각’할 시간을 주기: 모델이 적절한 출력을 생성하도록 하기 위해 모델에게 충분한 시간을 주는 방법을 소개합니다.
  1. 프롬프트 전술:
  • 전술 1: 입력의 구분을 명확하게 나타내기 위해 구분자 사용: 입력의 구분을 나타내기 위해 구분자를 사용하는 방법에 대해 다룹니다. (```, """, < >, <tag> </tag>, :)
  • 전술 2: 구조화된 출력 요청: 구조화된 출력을 요청하기 위해 JSON, HTML 등의 형식을 사용하는 방법을 소개합니다.
  • 전술 3: 조건 충족 여부 확인을 모델에 요청: 모델에게 조건 충족 여부를 확인하는 것을 요청하는 방법에 대해 다룹니다.
  • 전술 4: “피유-샷” 프롬프팅: 모델에게 일관된 스타일로 응답하도록 하는 방법에 대해 다룹니다.

https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/

#267

Tachyon 작동 방식

Tachyon은 사용자의 웹 브라우저에 내장된 기능을 활용하여 사용자가 <a href="..."></a> 태그에 커서를 50밀리초 이상 올려놓으면 콘텐츠를 미리 로드하는 <link rel="prerender" href="..."> 태그를 생성합니다(기본값).

기본적으로 사용자가 링크를 실제로 클릭/탭하기 전에 방문하려는 페이지의 로딩을 시작하도록 브라우저에 지시합니다. 이는 웹 브라우저가 백그라운드에서 준비를 시작하도록 지시합니다.

사용자가 실제로 링크를 클릭하고 다음 페이지로 이동할 준비가 되면 해당 페이지는 이미 준비되어 프레임으로 가져와 페이지 로드 시간이 훨씬 빨라집니다.


이유; 방법

Tachyon은 단순성을 핵심으로 설계되었으며, 이는 결코 우연이 아닙니다. 단순성에 중점을 두었기 때문에 관리자부터 최종 사용자까지 Tachyon을 사용하는 모든 사람이 성능, 확장성, 유지보수성, 보안 및 사용 편의성에서 이점을 누릴 수 있습니다.

다른 대안에 비해 Tachyon이 개선한 주요 사항 중 하나는 일반적인 <link rel="prefetch" href="..."> 대신 <link rel="prerender" href="...">를 사용하여 페이지 로드가 훨씬 빨라졌다는 점입니다. 프리페치는 페이지를 다운로드하기만 하고 프리렌더는 페이지를 다운로드하여 렌더링을 시작한다는 점에서 두 방법의 차이는 자명합니다.

또한 Tachyon은 클릭 가능성이 높은 페이지만 미리 로드하고 사용자의 커서가 링크에서 벗어나면 페이지 미리 로드를 중지하는 등 다른 방식보다 훨씬 효율적이고 방해가 덜 되는 방식으로 프리로딩 동작을 구현합니다. 이것이 바로 제가 Tachyon을 만든 이유이며, 지금까지도 왜 다른 대안이 이 기능을 제공하지 않는지 모르겠습니다. 그 결과, Tachyon은 다른 대안에 비해 사이트에 대역폭 부하를 극히 일부만 추가합니다.

다른 프로젝트보다 기능이 적은 것도 아닙니다(인스턴트 페이지와 가상 기능 동등성 및 몇 가지 추가 기능이 있습니다). 단지 다른 프로젝트보다 간결한 방식으로 구현된 기능일 뿐입니다. 별도의 설정 없이 모바일을 지원하고 화이트리스트, 블랙리스트, 사용자 지정 타이밍 및 동일 출처 제한을 구현하며, 이러한 기능을 사용하기가 훨씬 쉽습니다. 매우 복잡한 기능이 필요한 경우 Tachyon이 최선의 선택이 아닐 수 있지만, 그 외의 모든 사용자에게는 처음부터 최고의 옵션이 될 수 있도록 설계된 Tachyon이 적합합니다.


#268

모바일 웹뷰에서 특정 영역을 zoom할수 있게 해달라는 요청이 있어서 적용한 내역. 정확히 기억은 안나는데 react-prismazoom를 선택했다.

react-prismazoom은 CSS 변환을 사용하여 React에서 확대 및 이동 기능을 제공하는 팬 및 줌 컴포넌트입니다. 이 라이브러리는 prop-types, react, react-dom 모듈에만 의존하며, 데스크톱 및 모바일에서 모두 작동합니다.

주요 기능 및 특징

  • 확대 기능 : 마우스 휠이나 두 손가락으로 확대할 수 있습니다. 더블 클릭 또는 더블 탭을 사용하여 확대할 수도 있으며, 선택한 영역을 확대하여 중앙에 배치할 수 있습니다.
  • 이동 기능 : 마우스 포인터나 줌 인 상태에서 손가락을 사용하여 이동할 수 있습니다. 확대된 상태에서는 사용 가능한 공간에 따라 직관적으로 이동합니다. 요소를 이동할 수 있는 방향을 나타내기 위해 커서 스타일을 조정합니다.

그 외 비슷한

#269
ffmpeg -framerate 25 -i image_%1d.png -c:v libvpx-vp9 -pix_fmt yuva420p output.webm

단점

  • 두벌로 인코딩 작업을 해야한다. MacOS가 아닐경우 번거로운 부분이 존재한다.
  • 브라우저 지원이 애매하게 걸친 부분이 존재한다.
  • (개인적인 느낌) 사이즈가 커졌을 경우 프레임 드랍이 있다.

…그래서 어차피 안되는거 새로운 도전을 해보고 싶어서 크로마키 효과를 떠올렸다. sharp로 이미지 배경을 green 컬러로 채우고 그 이미지들을 합쳐서 동영상으로 변환. 그리고 canvas에 그리고 색상을 추출해서 green값을 alpha값으로 변환하면 완벽하지 않을까 싶었는데 겹치는 영역을 전혀 생각못했다. 이부분은 뭔가 특정 알고리즘이 있는 것 같은데 그냥 단순히 근사치2로 적용했을때 결과물이 완벽하지는 않다.


Footnotes

  1. 여기서는 좀 더 나아가서 ffmpeg으로 드로잉 하는 부분까지 알려주고 있다.

  2. javascript - How to accurately filter RGB value for chroma-key effect - Stack Overflow

  3. 기본 배경지식을 설명하고 있다. video->canvas

  4. hsv로 색상값을 체크하는 방법을 알려주고 있다. 그리고 해당 연산은 worker로 따로 분리해서 처리.

  5. canvas 에서 색상값을 체크해서 필터 기능을 구현하고 머신러닝으로 모션탐지 기능구현이 가능하다는 예제를 보여준다.

#27
const root = createRoot('#confirm-root')

root.render(<Confirm />)

confirm ui를 만들다보면 window.confirm을 호출하는 방식으로 사용하는게 가장 좋은 방법인데 (안그러면 state로 관리해야하고 결국 이건 무의미한 코드의 반복이다.) 이걸 react로 구현하려면 결국 render를 사용해야함. react-confirm, react-confirm-alert 둘다 소스를 보면 비슷한 방식으로 접근한다.

#270

deep-object-diff와 비슷한 비교 알고리즘을 구현하는 방법은 다양할 수 있지만, 대표적인 접근 방식은 재귀적으로 객체를 탐색하면서 속성을 비교하는 것입니다. 이를 위해 일반적으로 다음과 같은 과정을 따릅니다:

  1. 입력으로 받은 두 객체를 비교합니다.
  2. 첫 번째 객체의 속성을 순회하면서 두 번째 객체에 동일한 속성이 있는지 확인합니다.
    • 동일한 속성이 있다면, 해당 속성의 값을 비교합니다.
    • 값이 같다면, 두 객체의 해당 속성은 동일하므로 비교를 종료합니다.
    • 값이 다르다면, 속성이 변경된 것으로 간주하고 변경된 값을 기록합니다.
  3. 첫 번째 객체의 속성을 순회하면서 두 번째 객체에 동일한 속성이 없는 경우, 해당 속성은 첫 번째 객체에서 삭제된 것으로 간주합니다.
  4. 두 번째 객체의 속성을 순회하면서 첫 번째 객체에 동일한 속성이 없는 경우, 해당 속성은 두 번째 객체에 추가된 것으로 간주합니다.
  5. 만약 속성이 객체나 배열인 경우, 재귀적으로 해당 객체나 배열을 탐색하면서 내부의 속성을 비교합니다.

이러한 과정을 재귀적으로 반복하면서 객체의 모든 속성을 비교하고 차이를 식별합니다. 재귀적으로 탐색하므로 중첩된 객체나 배열에 대해서도 동일한 비교 알고리즘을 적용할 수 있습니다.

이러한 비교 알고리즘을 구현하기 위해 각 언어나 라이브러리는 자체적으로 다양한 방식과 최적화 기법을 사용할 수 있습니다. 그리고 deep-object-diff나 비슷한 도구들은 이러한 알고리즘을 구현하여 사용자에게 편리한 인터페이스를 제공하는 것입니다.


#271

Stacking Workflow는 큰 엔지니어링 작업을 작은 단위의 코드 변경으로 나누어 독립적으로 테스트, 검토 및 병합할 수 있는 프로세스입니다. 스택은 서로 의존하는 일련의 코드 변경으로 이루어집니다.

Why stack: Stacking은 작성자가 검토를 기다리는 동안 블록되지 않고 작업할 수 있게 하며, 더 품질 높은 검토 의견을 유도하고 작은 변경 사항을 빠르게 병합할 수 있게 합니다. 검토어는 작은 규모의 변경 사항을 검토하고, 재검토하는 시간을 줄이며, 무언가 잘못되었을 때 이를 분석하고 롤백할 수 있는 세분성을 갖추게 됩니다.

What is stacking: Stacking은 여러 개의 의존하는 Pull Request(PR)로 이루어진 것입니다. Stacking은 작성자가 빠른 검토를 받을 수 있도록 하고, 높은 품질의 검토 의견을 얻을 수 있게 하며, 작은 변경 사항을 빠르게 병합할 수 있습니다. 검토어는 작은 규모의 변경 사항을 검토하며, 재검토 시간이 줄어들고, 필요한 경우 분석 및 롤백할 수 있는 세분성을 갖게 됩니다.

Stacking vs feature branches: Stacking과 기능 브랜치 모두 트렁크 기반 개발을 가능하게 합니다. 그러나 Stacking은 기능 브랜치와 다른 점이 있는데, 기능 브랜치는 주로 main 브랜치에서 직접 분기되어야 하는 반면, Stacking은 서로 의존하는 PR들 간에 분기될 수 있다는 점입니다. 기능 브랜치는 비대해지고 검토하기 어려워질 수 있지만, 스택은 항상 가볍고 모듈식으로 유지될 수 있습니다. 기능 브랜치는 한꺼번에 검토되고 CI를 통과하고 한 번에 병합되어야 하지만, 스택은 작은 조각으로 검토되고 각각의 브랜치에서 CI를 통과한 후 언제든 병합될 수 있습니다.

Creating a change: Stacking은 이미 브랜치에서 분기하고 서로 의존하는 두 개의 PR을 열었다면 이미 사용한 것입니다. 이 워크플로우는 git에서 기본적으로 지원되며, main 대신에 부모 PR에서 개발을 시작하고 브랜치를 확인하면 됩니다.

Splitting: 일부 개발자는 처음에 큰 변경 사항을 작성하고 나중에 이를 작은 변경 사항의 스택으로 나누는 것을 선호합니다. 이는 최종 코드 구조를 미리 예측하기 어려울 때 도움이 되며, 작은, 독립적으로 검토 가능한 일련의 변경 사항을 제출하고자 할 때 유용합니다.

Updating a change: 스택된 브랜치를 업데이트하는 것은 전통적인 브랜치를 업데이트하는 것과 다르지 않습니다. 유일한 차이점은 스택된 브랜치를 업데이트한 후에는 의존하는 PR의 병합 베이스도 업데이트해야 한다는 것입니다. 변경 사항마다 리베이스하는 것은 불안하게 느껴질 수 있지만, 도구를 사용하면 자동화할 수 있습니다.

Opening and updating pull requests: 검토를 위해 준비되면 스택의 각 브랜치당 하나의 PR을 생성할 수 있습니다. 두 번째 브랜치에서 스택을 만들면 첫 번째 PR을 열 수 있습니다. 자주 변경 사항을 제출하면 PR이 작고 동료들이 쉽게 검토할 수 있습니다.

Merging PRs: 변경 사항이 승인되고 CI를 통과하면 병합할 준비가 됩니다. 스택된 변경 사항은 항상 트렁크로 병합되며, 스택의 하단부터 병합됩니다. PR이 병합되면 의존하는 PR의 베이스 브랜치를 트렁크로 업데이트해야 합니다.


#272

한 마을에 개발자들이 모여 프로젝트를 진행하고 있었습니다. 그 중 한 명의 개발자인 에릭은 팀에서 핵심적인 역할을 맡고 있었고, 그의 전문적인 지식과 능력은 프로젝트의 성공에 큰 영향을 미칠 정도였습니다. 그러나 에릭은 여행을 갈 계획을 세우고 모두에게 알리지 않았습니다.

어느 날, 프로젝트 팀은 예기치 않은 문제에 직면했습니다. 시스템의 일부가 오작동을 일으켜 복구해야 할 상황이었는데, 당연히 에릭이 이를 해결할 수 있었습니다. 그러나 에릭은 이미 여행을 떠나버렸고, 팀은 그를 찾을 수 없었습니다. 에릭이 없는 상태에서는 아무도 그의 전문적인 지식을 대체할 수 없었기 때문에 팀은 큰 혼란에 빠지게 되었습니다.

프로젝트 팀은 에릭의 결석으로 인한 위기를 극복하기 위해 긴급 회의를 열었습니다. 모두가 버스 팩터에 대해 이야기하며, 이 사태로부터 배운 교훈에 대해 논의했습니다. 팀은 이제부터 지식을 공유하고 업무를 분산시키기로 결정했습니다. 각 개발자는 다른 팀원의 역할을 이해하고, 중요한 결정과 지식을 모두가 공유하도록 노력하기로 했습니다.

이제 마을의 개발자들은 에릭 없이도 프로젝트를 진행할 수 있었습니다. 에릭은 멋진 여행을 즐겼지만, 팀은 그의 결석으로 인한 위기를 극복하고 지속 가능한 개발 환경을 조성하는 데 성공했습니다. 그들은 버스 팩터를 기억하며, 이 작은 이야기는 그들에게 프로젝트 관리의 중요성을 상기시켜주었습니다.


https://www.google.com/search?q=%EB%B2%84%EC%8A%A4%ED%8C%A9%ED%84%B0

#274
import { match } from 'ts-pattern'

type Format = 'webp' | 'jpg'

type Params = {
  id: string
  quality: keyof typeof QUALITY_MAP
  format: Format
}

const QUALITY_MAP = {
  player_background: '0',
  video_frames_start: '1',
  video_frames_middle: '2',
  video_frames_end: '3',
  lowest_quality: 'default',
  medium_quality: 'mqdefault',
  high_quality: 'hqdefault',
  standard_quality: 'sddefault',
  unscaled_resolution: 'maxresdefault',
}

const BASE_URL = 'https://i.ytimg.com'

const VI = (format: Format) =>
  match(format)
    .with('jpg', () => 'vi')
    .otherwise(() => ['vi', format].join('_'))

export function getThumbnail({ id, quality, format }: Params) {
  return [BASE_URL, VI(format), id, QUALITY_MAP[quality]]
    .join('/')
    .concat(`.${format}`)
}

#275
SERVICE=wantedspace-dashboard
MINIFIED_PATH_PREFIX=https://dashboard.wantedspace.ai/static/js
VERSION=$(git log --pretty=format:'%h' -n 1);

yarn datadog-ci sourcemaps upload ./build --service $SERVICE --minified-path-prefix $MINIFIED_PATH_PREFIX --release-version $VERSION

rm ./build/static/js/*.map
#276
{
  "scripts": {
    "zod": "openapi-zod-client -a \"./spec.yaml\" -o \"./spec.ts\"",
    "convert": "swagger2openapi ./spec.json -o ./spec.yaml"
  },
  "dependencies": {
    "openapi-zod-client": "^1.6.4",
    "orval": "^6.15.0",
    "prettier": "^2.8.8",
    "swagger2openapi": "^7.0.8"
  }
}
#277

https://github.com/ecyrbe/zodios/issues/402 https://www.jacobparis.com/content/type-safe-env


if (err instanceof z.ZodError) {}
if(err instanceof ZodiosError) {
   if (err.cause instanceof ZodError) {
      console.log(fromZodError(err.cause).toString())
   }
}
#278
type Props = {
  popover: 'auto' | 'manual'
  popovertarget: string
  popovertargetaction: 'hide' | 'show' | 'toggle'
}

type State = {
  hasBackdrop: boolean
  isPopoverOpen: boolean
}

type Methods = {
  hidePopover: () => void
  showPopover: () => void
  togglePopover: () => void
}

type Events = {
  beforetoggle: () => void
  toggle: () => void
}

#279
import z from 'zod'

const envSchema = z.object({
  REACT_APP_FEATURE_VAC_ASK: z.string(),
  REACT_APP_FEATURE_RECORDS: z.string(),
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
})

const windowSchema = z.object({
   SOMETHING_COOL: z.string()
})

export const ENV = envSchema.parse(process.env)
export const WINDOW = windowSchema.parse(window)

#280

Footnotes

  1. headless 모드가 있으면 좋을 것 같은데 찾기 어렵다. 이런게 있다…정도로만 생각하자. 실제 갖다 써보면 뭔말인지 알 수 있을거다.

  2. 네이티브로 날짜 계산(?)을 구현하려면 참고

#28
enum LogLevel {
  DEBUG = 'debug',
  INFO = 'info',
  WARN = 'warn',
  ERROR = 'error',
}

type Message = string

class Logger {
  private level: LogLevel

  constructor(level: LogLevel = LogLevel.DEBUG) {
    this.level = level
  }

  private log(level: LogLevel, message: Message) {
    if (this.level === LogLevel.DEBUG || level !== LogLevel.DEBUG) {
      const label = level.toUpperCase()

      console.log(`[${label}] ${message}`)
    }
  }

  /**
   * - 개발 혹은 테스트 단계
   * - 운영 환경에서는 남기고 싶지 않은 로그 메세지
   */
  public debug(message: Message) {
    this.log(LogLevel.DEBUG, message)
  }

  /**
   * - 정상 작동에 대한 정보
   * - 시스템을 파악하는데 유익한 정보
   */
  public info(message: Message) {
    this.log(LogLevel.INFO, message)
  }

  /**
   * - 잠재적으로 문제가 될 수 있는 상황
   * - 언제든 발생할 수 있는 일반적인 문제 상황
   * - 사용자에게 노출되는 메세지에 상세한 가이드가 필요
   */
  public warn(message: Message) {
    this.log(LogLevel.WARN, message)
  }

  /**
   * - 심각한 오류나 예외 상황
   * - 즉시 조치가 필요할때
   */
  public error(message: Message) {
    this.log(LogLevel.ERROR, message)
  }
}

1. 효율적으로 로그 모니터링하기 - 로그 레벨 구분하기

#281

지속적인 폴링에 의존하지 않고 SPA를 업데이트하고 다시 로드하기 위한 다양한 기술과 전략에 대해 설명합니다. CloudFront와 같은 콘텐츠 전송 네트워크(CDN)를 API 프록시로 활용하여 SPA를 무효화하고, 새 콘텐츠를 사용할 수 있을 때 사용자가 애플리케이션을 새로 고치라는 메시지를 구현하고, 캐시 버스팅 기술과 서비스 워커를 사용하는 등의 방법을 살펴봅니다.


#282
#286
class CustomError extends Error {
  name = 'CustomError'

  constructor(message?: string) {
    super(message)

    Object.setPrototypeOf(this, CustomError.prototype)
  }
}

try {
  throw new CustomError('This is a custom error message.')
} catch (error) {
  if (error instanceof CustomError) {
    console.log('CustomError occurred:', error.message)
    console.log('Error name:', error.name)
  } else {
    console.log('An error occurred:', error)
  }
}
#287
import {
  addDays,
  addMonths,
  eachDayOfInterval,
  eachWeekOfInterval,
  startOfMonth,
} from 'date-fns'

const startOfMonthDate = startOfMonth(new Date())
const matrix = eachWeekOfInterval({
  start: startOfMonthDate,
  end: addMonths(startOfMonthDate, 1),
}).map((weekDay) => {
  const startDate = new Date(weekDay)

  return eachDayOfInterval({
    start: startDate,
    end: addDays(startDate, 6),
  })
})
#29
configureWebpack: {
    module: {
      rules: [
        {
          test: /\.mjs$/,
          include: /node_modules/,
          type: 'javascript/auto',
        },
      ],
    },
  },
#292
const bodyReadingMethods = ['arrayBuffer', 'blob', 'formData', 'text', 'json']

bodyReadingMethods.forEach((methodName) => {
  request[methodName] = new Proxy(request[methodName], {
    apply(...args) {
      console.trace(`Premature "request.${methodName}" call!`)
      return Reflect.apply(...args)
    },
  })
})

https://redd.one/blog/debugging-like-a-pro-xy

#294
[data-scope='slider'][data-part='thumb']

[data-scope='slider'][data-part='track']

[data-scope='slider'][data-part='control']

Ark UI의 각 컴포넌트 파트는 data-scopedata-part 속성으로 지정됩니다. data-scope 속성은 컴포넌트를 식별하고, data-part 속성은 컴포넌트의 개별 부분을 지정합니다.

ark-ui에서 사용하는 방식인데 적용해볼만한 컨셉이라고 생각한다.


#297

Footnotes

  1. 포괄적인 가이드를 제공합니다.

  2. Lookbehind assertions, Named capture groups, s (dotAll) Flag, Unicode property escapes

  3. /"([^"\\]|\\.)*"/gus

#3

360 이미지를 드래그해서 회전시키는 기능을 개발할때 3d 리소스를 제공 받을 수 있다면 three.js 같은 라이브러리를 사용해서 쉽게(?) 구현이 가능하다. three.js가 초반 진입 장벽이 높은 것 같지만 단순히 모델 리소스를 보여주는 정도는 배경지식이나 기본지식이 없더라도 어느정도 예제 코드들을 본다면 구현이 가능하다고 본다. 물론 제대로 하고 싶다면 배경지식과 three.js 자체에 대한 학습이 필요.

그런데 부득이하게 여러장으로 된 이미지만 제공 받을 수 있다면 직접 구현해야한다. 그런데 생각보다 드래그 기능을 처음부터 구현한다는게 보통의 개발자들에게는 불편한게 사실이므로 이미 존재하는 플러그인이나 라이브러리를 사용하는게 현실적이다.

그래서 찾아본 gsap의 Draggable. 그런데 문서를 보면 다이얼 같이 (뭐라 표현하는게 적당한지 모르겠지만) 직접적으로 드래그 하는 관점의 설명들이 대부분이다. 그런데 360 이미지 같은 경우는 직접적으로 드래그 라기보다는 액션을 빌린 동작이어서 처음에 약간 혼란스러웠는데 좀 더 찾아보니 proxy의 개념을 빌려서 설명된 부분이 있었고 저런식으로 하면 쉽게 구현이 가능하다.

참고

반대 방향으로 진행시 계산법

#30

로컬에서만 실행 해야하는 프로젝트가 있길래 아무래도 사전설정 같은 귀찮은 문제가 있으니 pkg를 사용하면 좋을 것 같아서 들어가봤는데 개발이 중단 되었다.

노드 21버젼에서 해당 기능이 지원되는데 재미있는 시도들이 많이 나왔으면 좋겠다.


#300

Astro를 사용한 프로젝트에서 빌드를 하는데 JavaScript heap out of memory 에러가 발생했다. 쉽게 해결하자면 NODE_OPTIONS=--max_old_space_size(in megabytes) 설정해서 우회할수는 있겠지만 정리가 필요해서 메모를 남겨본다. 일단 원인은 파일사이즈가 크다는 점, 그리고 카테고리(국가)별 데이터가 많다는 점인데 개선할만한 부분은 두가지 정도인 듯.

  • 가능한 전처리해서 데이터를 다시 생성해서 참조
  • Astro.glob을 사용해서 조건부로 데이터를 가져오기

예상치 못한 에러였는데 역시 트레이드오프는 존재하기 마련이다.


#301
  • Cornerstone.js HTML5 canvas를 지원하는 웹 브라우저에서 가벼운 의료 이미지 표시에 사용되는 JavaScript 라이브러리
  • OpenSeadragon 데스크톱 및 모바일에서 사용 가능한 고해상도 줌 가능한 이미지를 위한 순수 JavaScript 뷰어
  • dicomParser DICOM 파일을 처리하여 의료 이미지 데이터를 JavaScript 객체로 변환하는 라이브러리
  • AMI Medical Imaging (AMI) 웹에서 의료 영상을 효과적으로 표시하고 주석을 추가하는 데 사용되는 JavaScript 라이브러리
  • OHIF Medical Imaging Viewer
  • itk-wasm 의료 영상 처리를 위한 JavaScript 라이브러리
  • Brainchop

예전에 관련 회사 기술 블로그 보다가 생각나서 찾아본 라이브러리 리스트. 대용량, 의학용 이미지 포멧을 다루기 위한 도구들.

as로 간단하게 처리가 가능할정도의 복잡도라면 괜찮다고 보는데 더 복잡하게 된다면 asChild가 최선의 방법.

function Button({ asChild, ...props }) {
  const Comp = asChild ? Slot : 'button'

  return <Comp {...props} />
}

#295

Grid View

grid view(또는 datagrid)는 데이터의 표 형식 보기를 제공하는 그래픽 제어 요소입니다.

일반적으로 다음 중 일부 또는 전부를 지원합니다.

  • 열 머리글을 클릭하여 그리드의 정렬 순서 변경
  • 열 머리글을 끌어서 크기 및 순서 변경
  • 보기 데이터의 제자리 편집
  • 행과 열 구분 기호 및 행 배경색 번갈아 지정하기

자연어 날짜 처리와 접근성 높은 데이트피커 - chrono와 inclusive-dates

chrono는 다양한 형식의 날짜/시간을 처리하고 주어진 텍스트에서 정보를 추출할 수 있도록 설계된 자연어 날짜 파서.

  • “Today”, “Tomorrow”, “Yesterday”, “Last Friday” 등의 상대적 날짜 처리
  • “17 August 2013 - 19 August 2013”와 같은 날짜 범위 처리
  • “This Friday from 13:00 - 16.00”와 같은 시간 포함 날짜 처리
  • “5 days ago”, “2 weeks from now”와 같은 상대적 시간 표현 처리
  • “Sat Aug 17 2013 18:40:39 GMT+0900 (JST)“와 같은 표준 날짜 형식 처리
  • “2014-11-30T08:15:30-05:30”와 같은 ISO 8601 형식 처리

parse()123

parse(text: string, referenceDate?: ParsingReference | Date, option?: ParsingOption): ParsedResult[] {
  // 1. 파싱 컨텍스트 생성
  // 2. 모든 파서를 실행하고 결과 수집
  // 3. 결과를 인덱스 기준으로 정렬
  // 4. 모든 리파이너를 적용하여 결과 개선
  // 5. 최종 결과 반환
}

inclusive-dates는 자연어 입력을 지원하는 사용자 친화적이고 완전히 접근 가능한 데이트피커. 내부적으로 chrono를 사용하여 자연어 날짜 입력을 처리. 이는 두 라이브러리의 장점을 결합한 좋은 예시.

const parsedDate = await chronoParseDate(text, {
  locale: this.locale.slice(0, 2),
  minDate: this.minDate,
  maxDate: this.minDate,
  referenceDate: removeTimezoneOffset(new Date(this.referenceDate)),
  ...chronoOptions,
})
  1. chrono의 강력한 자연어 날짜 파싱 능력을 활용.
  2. inclusive-dates는 이를 사용자 친화적이고 접근성 높은 UI 컴포넌트로 구현.

이러한 조합을 통해, 개발자들은 사용자에게 직관적이고 유연한 날짜 입력 방식을 제공하면서도 접근성과 사용성을 높일 수 있음.


Footnotes

  1. executeParser

  2. sort

  3. refine

#304

Google Sheets API를 활용하기 위해서는 Google Cloud Platform(GCP)에서 필요한 설정을 하고, google-spreadsheet 라이브러리를 통해 스프레드시트를 조작할 수 있습니다. 다음은 이를 위한 단계별 가이드입니다.

  1. 서비스 계정 설정
  2. Google Cloud 프로젝트 생성
  3. Google Sheets API 활성화
  4. API 자격 증명 생성
  5. 서비스 계정 생성
  6. 서비스 계정 키 생성
  7. 환경 변수 설정

#305

Next.js의 Route Loader와 Mini CSS Extract Plugin 살펴보기

Next.js의 router-loader.ts #prefetchViaDom()

function prefetchViaDom(
  href: string,
  as: string,
  link?: HTMLLinkElement
): Promise<any> {
  return new Promise<void>((resolve, reject) => {
    const selector = `
      link[rel="prefetch"][href^="${href}"],
      link[rel="preload"][href^="${href}"],
      script[src^="${href}"]`
    if (document.querySelector(selector)) {
      return resolve()
    }

    link = document.createElement('link')

    // The order of property assignment here is intentional:
    if (as) link!.as = as
    link!.rel = `prefetch`
    link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
    link!.onload = resolve as any
    link!.onerror = () =>
      reject(markAssetError(new Error(`Failed to prefetch: ${href}`)))

    // `href` should always be last:
    link!.href = href

    document.head.appendChild(link)
  })
}

이 함수는 미리 가져오는(prefetch) 역할을 하며 주요 특징은 다음과 같다.

  • 이미 prefetch 또는 preload된 리소스인지 확인.
  • 새로운 요소를 생성하여 리소스를 prefetch.
  • 로드 성공 또는 실패 시 적절한 처리.

Webpack의 Mini CSS Extract Plugin에서 CSS 청크 로드 실패 시 에러 처리 코드를 볼 수 있다.

 Template.indent([
  "var errorType = event && event.type;",
  "var realHref = event && event.target && event.target.href || fullhref;",
  'var err = new Error("Loading CSS chunk " + chunkId + " failed.\\n(" + errorType + ": " + realHref + ")");',
  'err.name = "ChunkLoadError";',
  // TODO remove `code` in the future major release to align with webpack
  'err.code = "CSS_CHUNK_LOAD_FAILED";',
  "err.type = errorType;",
  "err.request = realHref;",
  "if (linkTag.parentNode) linkTag.parentNode.removeChild(linkTag)",
  "reject(err);",
]),

이 코드는 CSS 청크 로드 실패 시 발생하는 에러를 처리하며 주요 특징은 다음과 같다.

  1. 에러 타입과 실제 URL을 파악.
  2. 상세한 에러 메시지를 생성.
  3. 에러 객체에 추가 정보(name, code, type, request)를 설정.
  4. 실패한 <link> 태그를 DOM에서 제거.
  5. Promisereject하여 에러를 전파.

이 플러그인은 CSS를 별도의 파일로 추출하는 데 사용되며, 위 코드는 그 과정에서 발생할 수 있는 오류를 처리하는 중요한 부분.

#306
if (!data) {
  throw fetch()
}
  1. 컴포넌트가 렌더링될 때, 비동기 작업(예: 데이터 패칭)이 시작됩니다. 이 작업은 일반적으로 promise를 반환합니다.
  2. 비동기 작업이 완료되지 않은 경우, 컴포넌트는 promise를 던집니다. 이는 JavaScript에서 예외를 던지는 것과 유사합니다. Suspense는 promise가 던져질 때 이를 캐치하고 fallback UI를 표시하는 역할을 합니다.
  3. React는 컴포넌트가 promise를 던졌을 때 이를 감지하고, Suspense 컴포넌트에서 이를 “캐치”합니다. Suspense는 이 promise가 해결될 때까지 대체 UI (fallback)를 렌더링합니다. Concurrent Mode에서는 React가 이 promise를 추적하고, 비동기 작업이 완료될 때까지 렌더링을 중단합니다.
  4. Promise가 해결되면(즉, 비동기 작업이 완료되면) React는 컴포넌트를 다시 렌더링합니다. Suspense는 현재 데이터 패칭 라이브러리(예: React Query, SWR)와 함께 사용되어 비동기 작업의 상태를 쉽게 관리할 수 있도록 도와줍니다.

#307

최근에 Enzyme을 제거하고 React Testing Library로 교체하는 작업을 진행했습니다. JSX 영역은 별 문제 없이 진행되었으나, state나 props를 다루는 구현 부분에서 약간의 애매함이 있었습니다. 개인적으로는 기존 접근 방식이 나쁘지 않았다고 생각했기 때문에, 새로운 접근 방식으로 전환하는 데 주저하게 되었습니다.

결론적으로, state와 props의 JSON 결과물을 렌더링하고 getByTestId를 사용하여 이를 참조하는 방식으로 테스트를 진행하기로 했습니다. 다음은 그 예시입니다:

render() {
  return <>
    {process.env.NODE_ENV === 'test' && (
      <pre data-testid="ThumbnailDebug">
        {JSON.stringify({
          props: this.props,
          state: this.state,
        })}
      </pre>
    )}
  </>
}

이 방법을 통해 Enzyme을 제거한다는 목표를 달성했기 때문에, 어느 정도는 해결된 것처럼 보이며, 추후 더 나은 테스트 코드를 작성하기 위한 고민을 할 수 있을 것 같습니다.


#309

React Router의 Link 컴포넌트 테스트하기

React 애플리케이션에서 라우팅은 중요한 부분. 특히, 사용자가 링크를 클릭할 때 적절한 페이지로 이동하는지 테스트하는 것은 중요. 다음은 React Router<Link /> 컴포넌트를 테스트하는 코드.

describe('Link', () => {
  it('이동', () => {
    const Home = () => <Link to="/about">About</Link>
    const About = () => <h1>About</h1>

    const { getByRole } = render(
      <MemoryRouter initialEntries={['/']}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </MemoryRouter>
    )

    fireEvent.click(getByRole('link'))

    expect(screen.getByRole('heading')).toHaveTextContent('About')
  })
})

가끔 <Link />도 테스트해야 할 때가 있다. 위 코드는 사용자가 “About” 링크를 클릭하면 “About” 페이지로 이동하는지 테스트. fireEvent.click을 사용해 링크 클릭 이벤트를 발생시키고, screen.getByRole을 통해 헤딩 요소의 텍스트가 ‘About’인지 확인. 이 테스트를 통해 라우팅이 제대로 동작하는지 확인할 수 있음.


#310

Avoiding premature abstraction with Unstyled React Components (buildui.com)

React 컴포넌트를 작성할 때, 불필요한 추상화를 피하고 컴포넌트의 유연성을 유지하는 방법. 특히 스타일이 없는 컴포넌트를 통해 어떻게 컴포넌트의 기능에 집중할 수 있는지를 설명.

import React, { ComponentProps, FC, ReactNode } from 'react'

function Spinner() {
  return (
    <span className="absolute inset-0 flex items-center justify-center">
      `<Spinner />`
    </span>
  )
}

type LabelProps = ComponentProps<'span'>

function Label({ children, ...rest }: LabelProps) {
  return <span {...rest}>{children}</span>
}

type LoadingButtonProps = ComponentProps<'button'> & {
  children: FC | ReactNode
}

/**
 * @description 버튼이 비활성화되었을 때 로딩 스피너를 표시할 수 있는 버튼 컴포넌트입니다. 이 컴포넌트는 children 속성으로 함수나 React 노드를 받을 수 있으며, 버튼이 비활성화되면 스피너가 표시되고, 텍스트는 보이지 않게 됩니다.
 */
function LoadingButton({
  disabled,
  className,
  children,
  ...rest
}: LoadingButtonProps) {
  return (
    <button {...rest} className={`${className} relative`} disabled={disabled}>
      {typeof children === 'function' ? (
        children({})
      ) : (
        <>
          {disabled && <Spinner />}
          <Label className={disabled ? 'invisible' : ''}>{children}</Label>
        </>
      )}
    </button>
  )
}

LoadingButton.Spinner = Spinner
LoadingButton.Label = Label

이 패턴은 컴포넌트를 작성할 때 불필요한 스타일링이나 구조를 미리 정의하지 않고, 각 컴포넌트가 자신의 역할에 충실할 수 있도록 도와줍니다. 이를 통해 코드의 유연성을 유지하고, 필요에 따라 컴포넌트를 확장하거나 수정할 수 있는 여지를 남겨두게 됩니다.

#311

Excel View1 라이브러리를 보던 중, Excel과 Node의 상호작용에 대한 궁금증이 생겼다. 아무래도 웹개발을 하다보니 어플리케이션과 통신할 수 있는 부분에 대해서 전혀 생각을 안했었다는 걸 깨닫고 이러한 부분을 보완하기 위해 Excel과의 통신 방식을 찾아봄.

우선, 위 라이브러리 코드를 통해 ActiveXObject('Excel.Application')2로 Excel 객체를 생성하고, 이를 통해 Excel의 다양한 기능에 접근할 수 있다는 것을 확인했다. 그리고 node-activex의 문서에서 링크를 통해 추가적인 정보들을 확인할 수 있었는데 아무래도 자주 보던 영역이 아니라 일단 확인만 하는 단계에서 멈춤.

이 접근을 통해, Excel과 상호작용하는 방법에 대해 실마리를 찾을 수 있었음. 그러나 모든 과정이 순조롭지만은 않은게. ActiveXObject를 사용하는 부분에서 개념적 이해가 부족했고, 그밖에 Excel의 객체모델에 대해서도 배경지식이 많이 부족하다는 걸 알게됨.

이번 경험을 통해, 웹 개발자가 어플리케이션 레벨의 개념들도 이해하면 좋겠다는 생각을 하게 됨. 나중에 기회가 되면 살펴보고 일단 view 기능을 스프레드시트로 구현해 봐야겠다.


Footnotes

  1. Excel에서 긴 줄을 탐색하는 데 도움이 되는 도구입니다. 활성 셀을 추적하고 전체 행 데이터를 자동으로 가져와 별도의 창에 표시합니다. 이를 통해 좌우로 스크롤하지 않고도 행의 모든 열을 쉽게 볼 수 있습니다.

  2. https://github.com/timepp/excelview/blob/master/excel.js#L70

#312

대화 상자(Dialog Box)는 사용자에게 정보를 전달하고 응답을 요청하는 그래픽 제어 요소이다. Dialog box

대화 상자를 연 소프트웨어와의 상호작용을 차단합니다.

  • System Modal - 이 대화 상자를 닫기 전까지 다른 작업을 할 수 없게 하며, 과거 단일 작업 시스템에서 주로 사용되었습니다.
  • Application Modal - 프로그램을 일시적으로 중단시키며, 대화 상자가 닫히기 전까지 다른 작업을 할 수 없습니다. 이는 워크플로우를 방해하거나 사용자 오류를 초래할 수 있어 종종 비판받습니다.
  • Document Modal - 부모 창만 차단하며, 다른 창에서 작업을 계속할 수 있습니다. macOS에서 주로 사용되며, 부모 창에 연결된 시트 형태로 나타납니다.

Modeless

소프트웨어의 다른 부분과의 상호작용을 허용합니다. 이 대화 상자는 소프트웨어와의 상호작용을 차단하지 않으며, 대화 상자가 열려 있는 동안에도 사용자가 작업을 계속할 수 있습니다. 툴바가 모델리스 대화 상자의 예입니다.

고려 사항

  • 모달 대화 상자의 문제점 - 모달 대화 상자는 사용자 흐름을 방해하고, 반복적인 사용으로 인해 사용자가 실수로 잘못된 선택을 하게 만들 수 있습니다. 사용자는 습관적으로 확인을 누르는 경향이 있으며, 이는 작업 손실로 이어질 수 있습니다.
  • 경고 대신 실행 취소 - 경고 메시지로 실수를 방지하려는 접근은 한계가 있습니다. 경고를 더 강하게 만들어도 사용자는 이를 빠르게 무시하고 실수를 반복할 수 있습니다. 대신, 실행 취소(Undo) 기능을 제공하여 사용자가 언제든지 실수를 되돌릴 수 있도록 하는 것이 중요합니다. 이는 사용자의 스트레스를 줄이고 더 나은 사용자 경험을 제공합니다.
  • 인간 중심의 디자인 - 소프트웨어 디자인은 사용자의 습관을 존중해야 합니다. 사용자가 실수를 하더라도 복구할 수 있는 실행 취소 기능을 제공하는 것이 인간 중심의 디자인입니다. 경고를 사용하는 대신, 실행 취소를 제공하라는 원칙이 바람직한 디자인 방향입니다.1

Footnotes

  1. https://alistapart.com/article/neveruseawarning/

#313

핵심 요점

  1. 비순수한 컴포넌트

    • new Date()와 같은 비순수한 함수는 호출할 때마다 다른 결과를 선택하므로, 테스트 결과의 일관성이 없음
  2. 해결책 - 의존성 주입

    • 비순수한 함수의 결과를 prop으로 전달하여 컴포넌트를 예측 가능하게 만들고, 테스트 시 특정 날짜를 제어
  3. 기본 매개변수

    • 기본 매개변수 사용으로 오늘 날짜의 기본 동작을 유지하면서, 테스트에서 유연성을 제공

개선된 예시

function Date({ date = new Date() }) {
  const [date, setDate] = React.useState(date)

  return (
    <input
      type="date"
      onChange={(e) => setDate(e.target.value)}
      defaultValue={date}
    />
  )
}

function Random({ randomizer = Math.random }) {
  const [state, setState] = React.useState(randomizer())

  return <div>{state}</div>
}

테스트

describe('Date Component', () => {
  it('should update state on date change', () => {
    render(<Date date={new Date('2023-01-01')} />)

    const input = screen.getByRole('textbox')

    expect(input.value).toBe('2023-01-01')
  })
})

describe('Random Component', () => {
  it('should render a random number', () => {
    const mockRandomizer = () => 0.5

    render(<Random randomizer={mockRandomizer} />)

    const div = screen.getByText('0.5')

    expect(div).toBeInTheDocument()
  })
})
#314

vitest와 jest에서 가짜 타이머를 사용하는 방법. 내부적으로는 @sinonjs/fake-timers를 사용. vitest와 jest 모두 시스템 시간을 조작할 수 있도록 하여 테스트가 일관되게 실행되도록 하고 다양한 시간과 날짜를 시뮬레이션하여 다양한 조건에서 코드가 예상대로 작동하는지 확인할 수 있음.

describe('isSameDate', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime('2024-07-20')
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it('현재 날짜와 동일한 날짜를 전달하면 true를 반환한다', () => {
    expect(isSameDate('2024-07-20')).toBe(true)
  })

  it('현재 날짜와 다른 날짜를 전달하면 false를 반환한다', () => {
    expect(isSameDate('2024-07-19')).toBe(false)
    expect(isSameDate('2024-07-21')).toBe(false)
  })
})

#315

button 또는 a 요소를 선택적으로 렌더링할 수 있는 ButtonOrLink 컴포넌트를 구현한 부분. props로 전달된 as 값에 따라 해당 태그를 렌더링하며, React.JSX.IntrinsicElements의 타입을 활용하여 각 태그에 맞는 props를 받을 수 있도록 한다.

import * as React from 'react'

type ComponentPropsWithAs<T extends keyof React.JSX.IntrinsicElements> = {
  as: T
} & React.ComponentProps<T>

type ButtonOrLinkProps =
  | ComponentPropsWithAs<'a'>
  | ComponentPropsWithAs<'button'>

export function ButtonOrLink(props: ButtonOrLinkProps) {
  switch (props.as) {
    case 'a':
      return <a {...props} />
    case 'button':
      return <button {...props} />
    default:
      return null
  }
}
#316

vitest에서 사용자 지정 assertion을 추가하여 Zod 스키마와 Response 객체를 비교하기

import { expect } from 'vitest'
import type { ZodTypeAny } from 'zod'

expect.extend({
  /**
   * @param received 테스트할 Response 객체
   * @param schema 검증할 Zod 스키마
   */
  async toMatchSchema(received: Response, schema: ZodTypeAny) {
    const response = await received.json()
    const result = await schema.safeParseAsync(response)

    return {
      message: () => '',
      pass: result.success,
    } satisfies ExpectationResult
  },
})

vitest.d.ts 파일에서 CustomMatchers 인터페이스를 확장하여 TypeScript와의 통합성을 유지.

import type { ZodTypeAny } from 'zod'

interface CustomMatchers<R = unknown> {
  toMatchSchema(schema: ZodTypeAny): Promise<R>
}

todoResponse의 응답 데이터가 todoSchema에 정의된 Zod 스키마와 일치하는지 확인한다.

test('todo', async () => {
  expect(todoResponse.ok).toBeTruthy()
  expect(todoResponse).toMatchSchema(todoSchema)
})

#317
💡

Blob URL 방식은 Firefox 58+에서 CSP 제한이 있는 페이지에서도 스크립트를 주입할 수 있는 방법입니다. 이 방식을 사용할 때, 비동기성 때문에 발생할 수 있는 문제를 방지하려면 초기화 코드가 준비된 후 스크립트를 전달해야 합니다.

const b = new Blob([script], { type: 'text/javascript' })
const u = URL.createObjectURL(b)
const s = document.createElement('script')
s.src = u
document.body.appendChild(s)
document.body.removeChild(s)
URL.revokeObjectURL(u)
#318
💡

이 패턴이 “상태 머신”처럼 들린다면, 그리 놀랄 일도 아닙니다. 결국, 선택의 문제는 상태 머신을 구축할지 말지가 아니라, 그것을 암시적으로 구축할지 명시적으로 구축할지에 달려 있습니다.

stateDiagram-v2
  [*] --> Mounting

  Mounting --> AwaitingEmailInput
  Mounting --> AwaitingCodeInput

  AwaitingEmailInput --> SubmittingEmail

  SubmittingEmail --> AwaitingCodeInput
  SubmittingEmail --> AwaitingEmailInput

  AwaitingCodeInput --> SubmittingCode

  SubmittingCode --> Success
  SubmittingCode --> AwaitingCodeInput

  Success --> [*]

#319

변수의 prefix는 역할과 의미를 명확히 하기 위해 사용된다.

/**
 * 초기값을 나타내며, 상태나 값이 처음 설정될 때 사용된다.
 * 적합한 상황: 프로그램이 시작되거나 객체가 처음 생성될 때의 값을 정의한다.
 */
const initialCount = 0 // 카운터의 초기값
const initialState = { loggedIn: false, user: null } // 초기 상태
const initialPosition = { x: 0, y: 0 } // 초기 좌표

/**
 * 기본값으로 널리 사용되는 표준 값이다.
 * 적합한 상황: 값이 없을 경우 사용할 기본값을 정의한다.
 */
const defaultTheme = 'light' // 기본 테마
const defaultUser = { name: 'Guest', role: 'viewer' } // 기본 사용자
const defaultPageSize = 20 // 페이지당 기본 항목 수

/**
 * 기준값 또는 다른 값의 참조점이 되는 값이다.
 * 적합한 상황: 값을 계산하거나 파생할 때 기준이 되는 값을 정의한다.
 */
const baseSalary = 3000 // 기준 급여
const baseUrl = 'https://api.example.com' // API의 기준 URL
const baseColor = '#FFFFFF' // 기준 색상

/**
 * 변경되기 전 원래 상태 또는 초기 상태를 강조한다.
 * 적합한 상황: 데이터를 변경하기 전에 원래 값을 유지해야 할 때 사용한다.
 */
const originalText = 'Hello World' // 변경 전 텍스트
const originalSettings = { theme: 'dark', notifications: true } // 원래 설정값
const originalImage = image.clone() // 원본 이미지 복사

사용 가이드

  • initial* vs default*:

    • initial*은 주로 초기 상태를 강조하며, 값이 재설정될 가능성이 적다.
    • default*는 기본값으로 자주 사용되며 필요에 따라 다른 값으로 교체될 수 있다.
  • base* vs original*:

    • base*는 기준값으로 사용되어 다른 값의 비교나 계산에 활용된다.
    • original*은 원래 상태를 보존하거나 복구를 위해 필요할 때 사용된다.
#320
const color =
  (isUnchecked && 'palette.label.alternative') ||
  (isChecked && 'palette.primary.normal') ||
  'palette.label.normal'

기존 접근과 문제점

기존 접근

  • 중첩된 삼항 연산자로 상태에 따라 값을 반환하는 코드 작성.
  • 간결하지만 가독성이 떨어지고, 상태가 늘어나면 유지보수가 어려워짐.

문법적 접근

  • if 문, switch 문, 객체 매핑, &&|| 논리 연산자 등 다양한 문법으로 해결 가능.
  • 하지만 이는 상태 관리의 본질적인 문제를 해결하지 못할 수 있음.

핵심 문제

  • status 모델을 어떻게 정의하고 참조할 것인가?
    • 상태의 의미를 명확히 표현할 수 있어야 함.
    • 상태와 그에 따른 동작이 쉽게 확장 가능해야 함.
    • 코드의 가독성과 유지보수성을 높여야 함.
class StatusManager {
  static Status = {
    UNCHECKED: 'UNCHECKED',
    CHECKED: 'CHECKED',
    DEFAULT: 'DEFAULT',
  } as const

  static getColor(status: keyof typeof this.Status) {
    switch (status) {
      case this.Status.UNCHECKED:
        return 'palette.label.alternative'
      case this.Status.CHECKED:
        return 'palette.primary.normal'
      default:
        return 'palette.label.normal'
    }
  }
}

const color = StatusManager.getColor(StatusManager.Status.CHECKED)

#321

타이머를 제어하기 위해 vi.useFakeTimersvi.advanceTimersByTime을 사용하기.

describe('Countdown 컴포넌트', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  test('타이머가 동작하며 시간을 업데이트한다.', () => {
    const endTime = new Date(Date.now() + 60000).toISOString()
    const renderChild = vi.fn(({ targetRef }) => (
      <div ref={targetRef} data-testid="countdown" />
    ))

    render(<Countdown end_at={endTime}>{renderChild}</Countdown>)

    expect(screen.getByTestId('countdown')).toBeEmptyDOMElement()

    act(() => {
      vi.advanceTimersByTime(10000)
    })

    expect(screen.getByTestId('countdown')).toHaveTextContent('00:00:50')

    act(() => {
      vi.advanceTimersByTime(50000)
    })

    expect(screen.getByTestId('countdown')).toHaveTextContent('00:00:00')
  })
})

#327

React 내부 동작 및 렌더링

React 패턴 및 설계 원칙

React 최적화 및 성능 개선

React 기반의 구현 실습

React와 TypeScript

React와 상태 관리

React 활용 사례 및 외부 기술 연계

React 스타일링 및 CSS

기타

#33

빈 PDF 파일이 필요해서 찾아본 방법들

import { createCanvas } from 'skia-canvas'
import { writeFile } from 'node:fs/promises'

/**
 * A4 크기의 빈 PDF 파일을 생성합니다.
 *
 * @param {string} filename - 생성할 PDF 파일의 이름입니다.
 * @returns {Promise<void>}
 */
async function createEmptyPDF(filename) {
  // A4 크기 (595x842 포인트)
  const width = 595
  const height = 842

  // PDF 형식의 캔버스를 생성합니다
  const canvas = createCanvas(width, height, 'pdf')
  const ctx = canvas.getContext('2d')

  // 아무 내용도 그리지 않고 현재 상태를 저장합니다
  ctx.save()

  // 캔버스를 버퍼로 변환하여 PDF로 저장합니다
  const buffer = await canvas.toBuffer()
  await writeFile(filename, buffer)
  console.log(`${filename} 파일이 생성되었습니다!`)
}

createEmptyPDF('empty_skia.pdf')

# Ghostscript를 사용한 빈 PDF 생성
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=empty.pdf -c "[/PageSize [595 842]] setpagedevice" -f /dev/null

# ImageMagick의 `convert` 명령어로 빈 PDF 생성
convert xc:white -page A4 empty.pdf

# `touch` 명령어와 PDF 헤더 직접 작성
echo -e "%PDF-1.4\n1 0 obj\n<<>>\nendobj\nxref\n0 1\n0000000000 65535 f \ntrailer\n<<>>\nstartxref\n9\n%%EOF" > empty.pdf
#330

조건부 렌더링을 한다고 했을때 예전에는 주로 B로 처리했던 것 같은데 디버깅 때문에 고생해서 그런지 생각이 바뀌었다.

function renderA() {
  return <Item isPacked={true} name="Space suit" />
}

function renderB() {
  return <>{isPacked ? <Item name="Space suit" /> : null}</>
}

조건부 렌더링을 보다 간결하게 표현하기 위해 If, Then, 그리고 Else 컴포넌트 개념을 차용하는 방법. 이 컴포넌트는 If에서 condition prop을 통해 조건을 받아들이고, 자식으로 ThenElse를 받아 각각의 내용을 렌더링하도록 한다.

type Props = {
  /** 렌더링할 조건 */
  condition: boolean
  /** 자식 컴포넌트 (Then, Else 포함) */
  children: React.ReactNode
}

/**
 * If 컴포넌트는 조건부 렌더링을 위한 컴포넌트
 */
function If({ condition, children }: Props) {
  let thenChild = null
  let elseChild = null

  React.Children.forEach(children, (child) => {
    if (!React.isValidElement(child)) return
    if (child.type === Then) thenChild = child
    if (child.type === Else) elseChild = child
  })

  return condition ? thenChild : elseChild
}

/**
 * Then, Else 컴포넌트는 조건이 참 또는 거짓일 때 렌더링되는 콘텐츠를 포함
 */
const Then = ({ children }: React.PropsWithChildren) => <>{children}</>
const Else = ({ children }: React.PropsWithChildren) => <>{children}</>
#335

구글 스프레드시트에서 중복 값을 찾고 그 값에 별도의 스타일을 적용하는 방법

=COUNTIF(A:A, A:A) > 1
  1. 셀 범위 선택: 중복 값을 확인하고 스타일을 적용할 셀 범위를 선택합니다. 예를 들어, A 열 전체를 선택하려면 A 열을 클릭합니다.

  2. 조건부 서식 열기: 상단 메뉴에서 **“서식”**을 클릭한 다음 **“조건부 서식”**을 선택합니다.

  3. 서식 규칙 설정: 조건부 서식 규칙 창이 열리면, “서식 규칙 추가” 옵션을 클릭합니다.

  4. 맞춤 수식 사용: 서식 규칙 드롭다운에서 **“맞춤 수식을 사용하여 서식 지정”**을 선택합니다.

  5. 중복 값 조건 수식 입력: 중복을 찾는 수식을 입력합니다. 이 수식은 A 열 전체에서 A1과 같은 값을 가지는 셀의 개수를 세고, 그 개수가 1보다 크면 중복으로 간주합니다.

  6. 서식 선택: 스타일을 지정하려면 서식 스타일에서 텍스트 색상, 배경 색상 등을 원하는 대로 설정합니다.

  7. 완료: 설정이 완료되면 완료를 클릭하여 적용합니다.

#340
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
  • 문제: 스프레드시트 데이터가 커서 내용 확인이 어려움.
  • 해결: 구글 드라이브에 스프레드시트 데이터를 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

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

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

fetchAvailability 시간 슬롯 조회

지정된 조건(근무 요일, 시간, 이벤트 충돌 여부)에 따라 예약 가능한 시간 슬롯을 가져옵니다. 결과는 사용자에게 제공할 수 있는 예약 가능한 시간 슬롯 목록과 해당 시간 슬롯의 지속 시간(분 단위)입니다.

flowchart LR
    A[시작] --> B[가장 가까운 슬롯 계산]
    B --> C[예약 기간 설정]
    C --> D[바쁜 일정 조회]
    D --> E[가능한 슬롯 필터링]
    E --> F[예약 가능한 슬롯 반환]
    F --> G[종료]
  1. 현재 시간을 기준으로 가장 가까운 시간 슬롯을 계산합니다. (슬롯 길이는 TIMESLOT_DURATION에 따라 설정됩니다.)
  2. 28일(DAYS_IN_ADVANCE) 동안의 일정 기간을 설정합니다.
  3. Calendar.Freebusy.query를 사용하여 지정된 캘린더(CALENDAR)의 바쁜 일정(busy events)을 조회합니다.
  4. 조회된 이벤트를 기반으로 조건에 맞지 않는 시간 슬롯을 제외합니다:
    • 지정된 근무 시간(WORKHOURS.start, WORKHOURS.end) 외의 시간.
    • 근무일(WORKDAYS)이 아닌 요일.
    • 다른 이벤트와 시간이 겹치는 경우.
  5. 예약 가능한 시간 슬롯을 ISO 8601 형식의 문자열로 저장하고 반환합니다.

bookTimeslot 시간 슬롯 예약

사용자가 선택한 시간 슬롯에 이벤트를 생성하여 예약합니다. 예약이 성공하면 Google Calendar에 이벤트를 추가하고 사용자에게 확인 메시지를 반환합니다.

flowchart TD
    A[시작] --> B[사용자 입력 수신]
    B --> C[시간 슬롯 유효성 검증]
    C --> D[이벤트 충돌 확인]
    D --> E{이벤트가 존재합니까?}
    E -- 예 --> F[에러 반환]
    E -- 아니오 --> G[이벤트 생성]
    G --> H[성공 메시지 반환]
    F --> I[종료]
    H --> I
  1. 사용자가 선택한 시간 슬롯(timeslot)과 추가 정보(이름, 이메일, 전화번호, 메모)를 인수로 받습니다.
  2. 시간 슬롯의 유효성을 검증하고, TIMESLOT_DURATION을 기준으로 종료 시간을 계산합니다.
  3. Calendar.Freebusy.query를 통해 선택한 시간 동안 다른 이벤트가 있는지 확인합니다.
    • 이벤트가 겹치면 예약이 불가능하다는 에러 메시지를 반환합니다.
  4. 겹치는 이벤트가 없다면, CalendarApp.getCalendarById를 사용하여 Google Calendar에 새로운 이벤트를 생성합니다:
    • 이벤트 제목: 사용자 이름이 포함된 약속 제목.
    • 이벤트 설명: 전화번호와 메모를 포함합니다.
    • 초대된 손님: 제공된 이메일로 초대합니다.
    • 초대 메일 발송(sendInvites: true).
  5. 예약 성공 여부를 메시지로 반환합니다.

사용 플로우 및 사례

  1. 사용자가 예약 가능한 시간 조회: 사용자는 시스템에 접속하여 자신의 예약 가능 시간을 확인합니다. 이는 캘린더 기반 예약 시스템에서 Google Calendar와 동기화하여 관리됩니다.

  2. 사용자가 특정 시간에 예약: 사용자가 특정 시간을 선택하면,

    • 선택된 시간과 정보를 bookTimeslot에 전달하여 캘린더 이벤트가 생성됩니다.
    • 충돌이 없을 경우, 예약이 성공적으로 완료되며 이벤트 초대 이메일이 사용자의 메일로 전송됩니다. 이 과정은 이메일 알림으로 사용자가 이벤트에 초대받음을 알려줍니다.
  3. 자동화: 시스템은 특정 근무 시간과 일정을 고려하여 예약 가능성을 실시간으로 확인합니다. 사용자는 이를 통해 보다 효율적으로 예약을 진행할 수 있습니다.


#329

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

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

thisisunsafe

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

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

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

tuple 함수 메모

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

동작 방식:

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

장점:

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

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

#358
function toggleReducer(state, action) {
  switch (action.type) {
    default:
      return state
  }
}

function useToggle({ reducer = toggleReducer } = {}) {
  const [state, dispatch] = useReducer(reducer, {})
  return { state, dispatch }
}

export function Component() {
  useToggle({
    reducer(currentState, action) {
      console.log(currentState, action)
    },
  })
}

useReducer를 이용한 커스텀훅 사용에 대한 간단한 예시. 생각해보니 reducer를 전달해서 재사용하는 방법은 잘 생각못했는데 응용할 수 있을 것 같다.



Footnotes

  1. 리듀서를 사용하여 예측 가능하고 테스트 가능한 방식으로 상태 업데이트 및 작업을 캡슐화하는 방법과 State Reducer 패턴을 사용하여 이를 사용하는 구성 요소에서 상태 업데이트를 추상화하여 해당 구성 요소가 특정 기능에 더 집중하도록 만드는 방법을 설명.

#36

취소 가능한 fetch 요청을 생성하는 createCancelableFetch 함수 구현

/**
 * 취소 가능한 fetch 요청
 *
 * @param url - 요청 URL
 * @param options - fetch 옵션
 * @returns {run, cancel} - run: 요청 실행 함수, cancel: 요청 취소 함수
 *
 * @example
 *
 * ```ts
 * const { run, cancel } = createCancelableFetch('/api/data')
 *
 * run()
 *   .then((data) => console.log('Fetched data:', data))
 *   .catch((err) => console.error('Error or canceled:', err))
 *
 * cancel()
 * ```
 */
function createCancelableFetch(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()
  }

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

  return {
    run,
    cancel,
  }
}
  • AbortController를 사용하여 요청을 중단할 수 있는 기능 제공
  • run 함수로 fetch 요청을 실행하고, cancel 함수로 요청을 취소할 수 있음
  • 요청 실패 시 오류를 처리하고 예외를 발생시킴

#360
  • 검색 매개변수별로 데이터 유효성 검증
  • useSearchParamsZod를 활용해 모든 검색 매개변수를 병합하고 유효성 검증
import { useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import { z } from 'zod'

const CombinedSchema = PaginationSchema.merge(FilterSchema)
  .merge(SortSchema)
  .merge(SearchSchema)
  .partial()

export function useSearchParamsWithSchema() {
  const [searchParams, setSearchParams] = useSearchParams()

  const parsedParams = useMemo(() => {
    // searchParams를 객체로 변환
    const paramsObject = Object.fromEntries(searchParams.entries())
    // 결합된 스키마로 유효성 검사 및 파싱
    const result = CombinedSchema.safeParse(paramsObject)

    if (result.success) {
      return result.data
    }

    console.error(result.error)

    return {}
  }, [searchParams])

  /**
   * 새로운 검색 매개변수로 업데이트하는 함수입니다.
   *
   * 1. 현재 검색 매개변수의 복사본을 생성합니다.
   * 2. 새로운 매개변수를 순회하며 값을 설정하거나 삭제합니다.
   * 3. 최종적으로 업데이트된 매개변수를 state에 설정합니다.
   */
  const updateSearchParams = (newParams: Record<string, string>) => {
    const updatedParams = new URLSearchParams(searchParams)

    Object.entries(newParams).forEach(([key, value]) => {
      if (value) {
        // 업데이트된 매개변수로 상태 갱신
        updatedParams.set(key, value)
      } else {
        // 값이 없으면 삭제
        updatedParams.delete(key)
      }
    })

    // 업데이트된 매개변수로 상태 갱신
    setSearchParams(updatedParams)
  }

  return {
    parsedParams,
    updateSearchParams,
  }
}
#367

URL.parse() 메서드를 활용하여 URL 객체를 생성하고 처리하는 방법

  • URL.parse(url) 메서드는 주어진 URL에 따라 새 URL 객체를 생성
  • 유효하지 않은 URL 값이 주어질 경우 null을 반환
  • 두 번째 파라미터 base는 상대 URL을 해석하기 위한 기준 URL로 사용되며, 이를 통해 URL의 경로가 올바르게 조정됨
  • URL 객체나 다른 문자열을 파라미터로 사용할 수 있으며, 내부에서 문자열로 변환됨
describe('null 입력에 대한 URL API 동작 테스트', () => {
  test('new URL(null)은 TypeError를 발생시킨다', () => {
    expect(() => new URL(null)).toThrow(TypeError)
  })

  test('URL.canParse(null)은 false를 반환한다', () => {
    expect(URL.canParse(null)).toBe(false)
  })

  test('URL.parse(null)은 null을 반환한다', () => {
    expect(() => URL.parse(null)).toBe(null)
  })
})
#368
@value b from "./b.module.css";

.root {
  color: aquamarine;
}

.root :global(.b) {
  text-decoration: line-through;
}

CSS 모듈에서 변수를 값으로 내보내고 사용하는 방법

  • PostCSS와 postcss-modules-values 플러그인을 사용하여 CSS 모듈 내에서 변수 값 내보내기 지원
  • 색상 변수를 정의하는 파일 생성
    • 변수 선언: @value 구문 사용
  • 다른 CSS 모듈 파일에서 해당 변수를 가져와서 사용
    • 변수 가져오기 및 CSS 클래스에 적용
#371

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions.html

  • URL rewrites or redirects
  • A/B testing and feature flags
  • Access authorization
#372
  • navigate(-1)의 위험성: 브라우저 히스토리에서 이전 위치로 이동, 앱 내부 네비게이션과 혼란을 초래할 수 있음
  • 대신 Link 컴포넌트의 state 속성을 활용하여 안전하게 앱 내에서의 “Back” 네비게이션 구현 가능
  • 사용자에게 현재 URL을 반환하는 커스텀 훅 useCurrentURL 구현 및 재사용의 용이성을 제공하는 useBackNavigation 훅 정의
function PreserveStateLink(props) {
  const location = useLocation()
  const currentURL = location.pathname + location.search

  return (
    <Link state={{ back: currentURL }} {...props}>
      {children}
    </Link>
  )
}

function BackLink() {
  const navigate = useNavigate()
  const location = useLocation()

  const handleClick: LinkProps['onClick'] = (e) => {
    const back = location.state?.back

    if (back) {
      e.preventDefault()

      navigate(back)
    }
  }

  return (
    <Link to="/todos" onClick={handleBack}>
      Back
    </Link>
  )
}
#373

https://www.emgoto.com/jest-partial-match/

objectContaining, arrayContaining, 및 toHaveBeenCalledWith를 사용하기

test('objectContaining으로 특정 키/값 매칭', () => {
  const receivedObject = {
    id: 1,
    name: 'Alice',
    age: 30,
    city: 'Seoul',
  }

  expect(receivedObject).toEqual(
    expect.objectContaining({
      name: 'Alice',
      city: 'Seoul',
    })
  )
})

test('arrayContaining으로 특정 값 배열 매칭', () => {
  const receivedArray = [1, 2, 3, 4, 5]

  expect(receivedArray).toEqual(expect.arrayContaining([2, 4]))
})

test('toHaveBeenCalledWith을 사용한 부분 매칭', () => {
  const mockFunction = jest.fn()

  // Mock 함수 호출
  mockFunction({
    id: 1,
    name: 'Alice',
    tags: ['developer', 'designer'],
  })

  expect(mockFunction).toHaveBeenCalledWith(
    expect.objectContaining({
      name: 'Alice',
      tags: expect.arrayContaining(['designer']),
    })
  )
})

test('arrayContaining + objectContaining으로 복합 매칭', () => {
  const receivedArray = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Charlie' },
  ]

  expect(receivedArray).toEqual(
    expect.arrayContaining([
      expect.objectContaining({ name: 'Alice' }),
      expect.objectContaining({ id: 2 }),
    ])
  )
})
#374

https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement

document.activeElement는 현재 DOM에서 포커스를 받고 있는 요소를 반환하는 메서드입니다. 이를 활용하면 다양한 사용자 경험과 기능을 구현할 수 있습니다. 아래는 document.activeElement를 활용해 만들 수 있는 기능들입니다:


1. 현재 포커스된 입력 필드 확인

사용자가 작성 중인 입력 필드나 텍스트 영역을 확인할 수 있습니다.

  • 예제: 포커스된 입력 필드의 내용을 자동 저장하거나 하이라이트 처리.
const activeElement = document.activeElement
if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') {
  console.log('Current input:', activeElement.value)
}

2. 포커스 전환 관리

특정 이벤트가 발생했을 때 특정 요소로 포커스를 강제로 이동.

  • 예제: 모달 창이 열릴 때 닫기 버튼에 자동으로 포커스 설정.
document.getElementById('openModal').addEventListener('click', () => {
  const modalCloseButton = document.querySelector('.modal .close-button')
  modalCloseButton.focus()
})

3. 키보드 내비게이션 개선

키보드만 사용하는 환경에서 포커스 상태를 추적해 순차적으로 이동 가능.

  • 예제: 탭 키를 눌러 순환 내비게이션 구현.
document.addEventListener('keydown', (event) => {
  if (event.key === 'Tab') {
    const activeElement = document.activeElement
    console.log('Currently focused element:', activeElement)
  }
})

4. 포커스 아웃 이벤트 감지

document.activeElement가 변경될 때마다 현재 포커스가 사라지는 경우를 처리.

  • 예제: 입력 필드에서 포커스를 잃으면 경고 메시지 표시.
document.addEventListener('focusout', () => {
  const activeElement = document.activeElement
  if (!activeElement || activeElement.tagName !== 'INPUT') {
    console.log('Input field lost focus!')
  }
})

5. 접근성 기능 강화

화면 리더기와 같은 접근성을 위해 현재 포커스된 요소를 기반으로 안내 텍스트 제공.

  • 예제: 현재 활성화된 요소에 추가 정보를 읽어주는 알림.
document.addEventListener('focusin', () => {
  const activeElement = document.activeElement
  const ariaLabel = activeElement.getAttribute('aria-label')
  if (ariaLabel) {
    console.log(`Focused on: ${ariaLabel}`)
  }
})

6. 자동 입력 감지 및 처리

포커스된 필드가 텍스트 입력인지 버튼인지 구분해 동작을 다르게 처리.

  • 예제: 포커스된 필드에 따라 Enter 키 이벤트 처리 방식 변경.
document.addEventListener('keydown', (event) => {
  if (event.key === 'Enter') {
    const activeElement = document.activeElement
    if (activeElement.tagName === 'BUTTON') {
      activeElement.click()
    } else if (activeElement.tagName === 'INPUT') {
      console.log('Input submitted:', activeElement.value)
    }
  }
})

7. 사용자 행동 분석

사용자가 어디에 더 많이 포커스를 두는지 분석해 UI 개선.

  • 예제: 포커스된 요소별 통계 수집.
const focusCount = {}

document.addEventListener('focusin', () => {
  const activeElement = document.activeElement
  const id = activeElement.id || activeElement.tagName
  focusCount[id] = (focusCount[id] || 0) + 1
  console.log(`Focus count for ${id}:`, focusCount[id])
})

8. 폼 검증 피드백 제공

사용자가 입력 중인 필드에 실시간 검증 메시지 표시.

  • 예제: 잘못된 입력값이 감지되면 사용자에게 즉각 알림.
document.addEventListener('input', () => {
  const activeElement = document.activeElement
  if (activeElement.tagName === 'INPUT' && activeElement.type === 'email') {
    if (!activeElement.value.includes('@')) {
      activeElement.setCustomValidity('Please enter a valid email address.')
    } else {
      activeElement.setCustomValidity('')
    }
  }
})

9. 커스텀 툴팁 제공

포커스된 요소에 맞춰 툴팁을 표시하거나 정보를 제공.

  • 예제: 입력 필드에 포커스될 때 툴팁이 나타남.
document.addEventListener('focusin', () => {
  const activeElement = document.activeElement
  if (activeElement.tagName === 'INPUT') {
    const tooltip = document.querySelector('.tooltip')
    tooltip.textContent =
      activeElement.getAttribute('data-tooltip') || 'Enter value'
    tooltip.style.top = `${
      activeElement.offsetTop + activeElement.offsetHeight
    }px`
    tooltip.style.left = `${activeElement.offsetLeft}px`
    tooltip.style.display = 'block'
  }
})

document.addEventListener('focusout', () => {
  const tooltip = document.querySelector('.tooltip')
  tooltip.style.display = 'none'
})

10. 다크모드 및 스타일 변경 제어

포커스된 요소에 따라 UI 스타일 변경.

  • 예제: 특정 요소에 포커스되었을 때 테마 전환.
document.addEventListener('focusin', () => {
  const activeElement = document.activeElement
  if (activeElement.classList.contains('dark-mode-toggle')) {
    document.body.classList.add('dark-mode')
  }
})

위 기능들은 document.activeElement를 다양한 상황에 응용한 사례입니다. 주로 사용자 행동 추적, UI 개선, 접근성 강화에 유용하게 사용될 수 있습니다.

#375
#377
type JsonPrimitive = string | number | boolean | null
type JsonObject = { [Key in string]: JsonValue } & {
  [Key in string]?: JsonValue | undefined
}
type JsonArray = JsonValue[] | readonly JsonValue[]
type JsonValue = JsonPrimitive | JsonObject | JsonArray

예전에 JSON 타입 정의가 필요해서 찾아봤던 내용

  • JsonObject: 문자열 키와 JsonValue 타입의 값을 가진 JSON 객체를 정의
  • JsonArray: JsonValue 타입의 요소를 포함하는 JSON 배열을 정의
  • JsonPrimitive: 문자열, 숫자, 불린, 또는 null과 같은 유효한 JSON 기본 값을 정의
  • JsonValue: 유효한 JSON 값을 나타내며, JsonPrimitive, JsonObject, 또는 JsonArray로 구성

#376

google-optimize


Footnotes

  1. setTimeout, clearTimeout 으로 최적화

  2. gtag 에서도 remove: true값 전달로 해제

  3. variants를 컴포넌트로 전달 받아서 리턴

#38

Every type is defined by its intro and elim forms

  • Intro forms: 타입의 인스턴스를 어떻게 “생성”하는지 정의.
  • Elim forms: 생성된 타입 인스턴스를 어떻게 “사용”하거나 “해체”할지 정의.

타입을 정의할 때, 생성과 사용의 명확한 경계를 설정해서 Intro/Elim 설계를 명시적으로 표현하기

class Rectangle {
  private constructor(public width: number, public height: number) {}

  static create(width: number, height: number) {
    if (width <= 0 || height <= 0) {
      return null
    }

    return new Rectangle(width, height)
  }

  getArea() {
    return this.width * this.height
  }
}

const rect = Rectangle.create(10, 20)

if (rect) {
  console.log(rect.getArea())
}

Types are not their elim forms

  • interfaceclass를 통해 Intro/Elim 모두를 명시적으로 정의
  • 팩토리 메서드 같은 패턴을 활용해 생성 방식을 추상화
interface Shape {
  getArea(): number
}

class Circle implements Shape {
  constructor(private radius: number) {}

  getArea() {
    return Math.PI * this.radius ** 2
  }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  getArea() {
    return this.width * this.height
  }
}

function createShape(type: 'circle' | 'rectangle', ...args: number[]) {
  if (type === 'circle' && args.length === 1) {
    return new Circle(args[0])
  }

  if (type === 'rectangle' && args.length === 2) {
    return new Rectangle(args[0], args[1])
  }

  return null
}

const shape = createShape('circle', 10)

if (shape) {
  console.log(shape.getArea())
}
#380

static create 패턴이 유용한 상황

대부분의 경우 생성자를 사용하는 것이 더 직관적이고 단순하지만, 다음과 같은 상황에서는 static create가 더 적합

  • 생성 로직이 복잡하거나 조건부 처리가 필요할 때
  • 팩토리 패턴을 간소화하고 싶을 때
  • 생성 과정에서의 불변성을 강화하고 싶을 때
  • 다른 타입을 반환하거나, 실패 가능성을 명시적으로 처리하고 싶을 때

1. 생성 로직 커스터마이징

  • 생성자가 단순히 필드 초기화만 수행하는 경우는 일반 생성자로 충분하지만, 생성 과정에서 유효성 검사, 값 변환, 혹은 복잡한 비즈니스 로직이 필요하다면 static create 패턴이 더 적합
  • 예를 들어, 유효성 검사를 통과하지 못하면 객체를 반환하지 않거나, 에러를 던질 수 있음
  • 생성 과정에서의 제약 조건을 명시적으로 처리할 수 있음
class User {
  private constructor(private readonly name: string) {}

  static create(name: string): User | null {
    if (!name || name.length < 3) {
      console.error('Invalid name!')

      return null
    }

    return new User(name)
  }
}

const validUser = User.create('John') // 성공
const invalidUser = User.create('Jo') // 실패, null 반환

2. 팩토리 패턴의 간소화

  • static create를 간단한 팩토리 메서드로 활용해서 객체 생성의 복잡성을 숨기기
  • 객체의 생성 방식이 호출자의 관점에서는 중요하지 않다면 내부 로직을 캡슐화할 수 있다
  • 이런 방식은 타입에 따라 다른 구현체를 생성해야 하는 경우
class Shape {
  static create(type: 'circle' | 'rectangle', size: number): Shape {
    if (type === 'circle') {
      return new Circle(size)
    } else {
      return new Rectangle(size)
    }
  }
}

3. 불변성 및 제약 강화

  • 생성자를 private로 설정하고, static 메서드만을 통해 객체를 생성하도록 제한하면 객체의 불변성을 더 강하게 유지할 수 있다
  • 특정 조건에 따라 객체의 생성을 제한하거나 실패하도록 설계할 수 있다
class CardNumber {
  private constructor(private readonly number: string) {}

  static create(number: string): CardNumber | null {
    if (!CardNumber.isValid(number)) {
      return null
    }

    return new CardNumber(number)
  }

  private static isValid(number: string): boolean {
    return number.length === 16
  }
}

const card = CardNumber.create('1234567812345678') // 유효한 카드
const invalidCard = CardNumber.create('123') // null

4. 다른 타입 반환

생성자는 항상 해당 클래스의 인스턴스만 반환할 수 있다. 반면, static create는 경우에 따라 다른 타입의 값을 반환할 수 있다.

class Result<T> {
  private constructor(
    public readonly value: T | null,
    public readonly error: string | null
  ) {}

  static success<T>(value: T): Result<T> {
    return new Result(value, null)
  }

  static failure<T>(error: string): Result<T> {
    return new Result(null, error)
  }
}

class User {
  private constructor(private readonly name: string) {}

  static create(name: string): Result<User> {
    if (!name || name.length < 3) {
      return Result.failure('Name must be at least 3 characters long')
    }

    return Result.success(new User(name))
  }
}

const result = User.create('Jo') // 실패 결과 반환

5. 객체 재사용

동일한 인스턴스를 재사용하고 싶을 때도 static create 패턴이 적합하다. 예를 들어, 싱글톤(Singleton) 패턴처럼 동작하거나 캐싱된 객체를 반환할 수 있다.

class Configuration {
  private static instance: Configuration

  private constructor(public readonly settings: Record<string, string>) {}

  static create(): Configuration {
    if (!Configuration.instance) {
      Configuration.instance = new Configuration({ mode: 'production' })
    }

    return Configuration.instance
  }
}

const config1 = Configuration.create()
const config2 = Configuration.create()

console.log(config1 === config2) // true
#381

권한관리 문제점

  1. 불필요한 복잡성과 디버깅의 어려움
  2. 계층적 권한의 비효율성
  3. 데이터베이스 부하
  4. 여러 개의 진실 소스(Sources of Truth)

해결 방법

  • JSON 직렬화 가능한 DSL을 정의하여 정책을 표현.
  • TypeScript 기반 평가 엔진을 구현하여 데이터를 기반으로 정책을 평가.
  • 기존 AST 기반보다 단순한 구조를 가진다.

type FieldName = string

type Value = string | boolean | number | Date | null

type ExpressionArgumentRef = {
  type: 'field'
  ref: FieldName
}

type BinaryExpressionDef = [
  FieldName,
  '=' | '<>' | '>' | '<' | '>=' | '<=',
  Value | ExpressionArgumentRef
]

type OrExpressionDef = {
  or: ExpressionDef[]
}

type AndExpressionDef = {
  and: ExpressionDef[]
}

type ExpressionDef = BinaryExpressionDef | OrExpressionDef | AndExpressionDef

const binaryExpression = ['file.id', '<>', null] satisfies ExpressionDef

const andExpression = {
  and: [
    ['file.id', '<>', null],
    ['team.permission', '=', 'open'],
    ['project.deleted_at', '<>', null],
  ],
} satisfies ExpressionDef

const orExpression = {
  or: [
    ['team.id', '<>', null],
    ['file.id', '=', { type: 'field', ref: 'team.id' }],
  ],
} satisfies ExpressionDef
#382
  • 프런트엔드(클라이언트)에서 MP4 파일의 오디오 존재 여부를 확인하기
  • 브라우저 API 사용 시 호환성 문제 발생 (Chrome, Safari, Firefox 각각 다른 API 사용)
    • MP4 파일의 구조를 분석하는 방법으로 방향 전환
      • hdlr 아톰에서 오디오 존재 여부를 나타내는 Component subtype 필드가 soun일 경우 오디오가 존재할 것임.
      • MP4 파일의 특정 바이트를 선택적으로 요청하기 위해 Range 필드 사용.
      • 다양한 파일 크기에 따라 ‘soun’의 예상 위치를 조사하고, 적절한 범위 값 설정.
  • 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의 중요성을 깨달았다.

#383
/**
 * - 인자 수 확인: `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
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
#397
  • 캔버스로 동영상 프레임을 캡쳐한다.
  • 텍스트를 입력 받는다. 해당 텍스트를 서버에 보내고 음성파일을 응답받는다.
    • Blob데이터는 URL.createObjectURL()로 변환해서 img, audio 태그에 연결한다.

#4
#402
#403
#408
getServerSideProps
  |> getServerSideSitemapLegacy
  |> withXMLResponseLegacy
  |>
    res.setHeader('Content-Type', 'text/xml')
    res.write(content)
    res.end()

https://github.com/iamvishnusankar/next-sitemap/blob/master/packages/next-sitemap/src/ssr/response.ts

SSR에서 XML 형식의 사이트맵을 생성하기.

#410
#417
#415
#420
#424
#427
#430

Footnotes

  1. media queries를 처리하기 위한 create-media-queries.js 함수 설명

  2. objects 방식으로 스타일 작성 가능

#43

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

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

주요업무

  • 최신 웹 기술을 사용하여 웹 애플리케이션의 사용자 인터페이스 설계 및 개발
  • 디자이너, PO, 서버개발자와 협업하여 사용자 인터페이스 디자인이 최고 수준으로 구현되도록 보장
  • 깔끔하고 유지 관리가 용이하며 효율적인 코드 작성
  • 프론트엔드 문제 디버깅 및 문제 해결
  • 프론트엔드 개발의 새로운 트렌드와 기술에 대한 최신 정보 파악

자격요건

  • HTML, CSS, JavaScript에 대한 풍부한 경험
  • React, Vue, Angular 또는 이와 유사한 최신 프런트엔드 프레임워크에 대한 경험
  • MobX, React-Query 등 다양한 상태 관리 패턴 사용 경험
  • Git 버전 관리에 익숙함
  • 뛰어난 문제 해결 능력과 세부 사항에 대한 주의력
  • 강력한 커뮤니케이션 및 협업 기술
  • 빠르게 변화하는 환경에서 작업하고 동시에 여러 프로젝트를 처리할 수 있는 능력
  • 능동적인 문제 해결사. 높은 신뢰성, 디테일 지향성, 뛰어난 후속 처리 능력

우대사항

  • Vitest, Jest 그리고 React Testing Library와 같은 테스트 프레임워크에 대한 경험
  • JS -> TS 마이그레이션 경험,
  • 프론트엔드 개발에 열정을 갖고 역동적인 팀과 함께 흥미로운 프로젝트를 진행하고자 하는 자세
  • 고객과 직접 작업한 경험 또는 고객과 직접 대면하는 역할을 수행한 경험
  • 고객을 돕고 고객의 요구를 충족하는 최고의 전략 및 기술 솔루션을 조언하는 데 있어 능동적인 자세로 임하는 열정
  • 새로운 웹 개발 트렌드를 포함하여 성장하는 비즈니스의 요구 사항에 대한 이해력
  • 팀 환경 내에서 개별적으로 또는 협력적으로 일할 수 있는 능력
  • 모호함을 편안하게 받아들이고 실패나 실수를 두려워하지 않는 자세. 피드백을 잘 받아들이고 빠르게 반복할 수 있음(스타트업 초기 단계의 경험 적극 선호)

위의 항목은 대화의 시작점일 뿐, 요구사항의 어려운 목록이 아닙니다. 이 역할에 흥미를 느낀다면 꼭 지원하시기 바랍니다!

#435

OpenAPI에서 타입스크립트 코드 생성. 클라이언트, SDK, 유효성 검사기 등을 생성하세요.

#437
#449

  • 유연성: 정책을 메커니즘에서 분리하고 인터페이스를 엔진에서 분리하면 소프트웨어 구성 요소를 설계하고 구현할 때 더 큰 유연성을 확보할 수 있습니다. 따라서 시스템의 나머지 부분에 영향을 주지 않고 구성 요소를 수정하거나 교체하기가 더 쉬워집니다.
  • 재사용 가능성: 컴포넌트를 별개의 모듈로 분리하면 다양한 컨텍스트나 애플리케이션에서 사용할 수 있는 재사용 가능한 빌딩 블록을 만들 수 있습니다. 이를 통해 코드 재사용을 촉진하여 개발 시간을 단축하고 코드 품질을 개선할 수 있습니다.
  • 테스트 가능성: 메커니즘을 정책에서 분리하고 인터페이스를 엔진에서 분리하면 개별 구성 요소를 개별적으로 테스트하기가 더 쉬워져 전반적인 테스트 범위가 개선되고 버그나 회귀의 위험이 줄어듭니다.
  • 유지 관리 가능성: 컴포넌트를 별개의 모듈로 분리하면 시간이 지남에 따라 코드를 더 쉽게 유지 관리하고 디버그할 수 있습니다. 또한 시스템의 나머지 부분에 영향을 주지 않고 개별 컴포넌트의 문제나 버그를 더 쉽게 식별하고 수정할 수 있습니다.
  • 확장성: 컴포넌트를 분리하면 특정 요구 사항에 따라 여러 컴포넌트를 독립적으로 확장할 수 있어 소프트웨어 애플리케이션을 더 쉽게 확장할 수 있습니다.
  • 상호 운용성: 구성 요소를 분리하면 서로 다른 시스템 간에 통신하는 데 사용할 수 있는 잘 정의되고 표준화된 인터페이스를 생성하여 서로 다른 시스템 또는 구성 요소 간의 상호 운용성을 향상시킬 수 있습니다.
  • 민첩성: 컴포넌트를 분리하면 나머지 시스템에 영향을 주지 않고 개별 컴포넌트를 더 빠르게 반복하고 변경할 수 있어 민첩성을 향상시킬 수 있습니다.

Footnotes

  1. React에서 컴포지션의 개념과 “Prop Drilling”을 피하기 위해 React에서 사용하는 방법을 설명.

  2. Headless UI 컴포넌트의 개념을 소개. 헤드리스 UI 컴포넌트가 무엇인지, 기존 UI 컴포넌트와 어떻게 다른지 설명하고, 다양한 프로그래밍 언어와 프레임워크에서 헤드리스 UI 컴포넌트를 구현하는 방법에 대한 예제.

#46
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

No, disabling a button is not app logic. - DEV Community

- "idle" 아무 것도 아직 일어나지 않았다.
- "loading" 진행중
- "success" 성공적
- "failure" 오류가 발생했음
#47
#474
export function parseNativeEmoji(unified: string): string {
  return unified
    .split('-')
    .map((hex) => String.fromCodePoint(parseInt(hex, 16)))
    .join('')
}
  • unified 문자열을 -로 분리하여 개별 유니코드 코드 포인트를 얻는다.
  • 각 16진수 코드 포인트를 정수로 변환하고, 그에 해당하는 캐릭터를 반환한다.
  • 최종적으로 변환된 캐릭터들을 연결하여 하나의 문자열로 생성한다.

#477

How To Maintain A Large Next.js Application — Smashing Magazine

  • TypeScript 사용
  • Lerna , Nx , Rush , Turborepo , yarn workspaces를 사용하여 Mono-Repo 구조 사용
  • Hygen과 같은 코드 생성기를 사용하여 상용구 코드 생성
  • Redux 툴킷을 통해 하위 상용구와 함께 Redux와 같이 잘 설정된 패턴 사용
  • 비동기 데이터를 가져오기 위해 React 쿼리 또는 SWR 사용
  • Husky와 함께 Commitizen 및 Semantic Release 사용
  • UI 구성 요소 시각화를 위해 스토리북 사용
  • 처음부터 유지 관리 가능한 테스트 작성
  • Dependabot을 사용하여 자동으로 패키지 업데이트
  • Going to Production | Next.js
#48
import * as React from 'react'

type Props<TRow> = {
  rows: TRow[]
  renderRow: (row: TRow, index: number) => React.ReactNode
}

const Table = <TRow extends Record<string, any>>({
  rows,
  renderRow,
}: Props<TRow>) => {
  return (
    <table>
      <tbody>{rows.map((row, index) => renderRow(row, index))}</tbody>
    </table>
  )
}

function App() {
  return (
    <Table
      rows={[{ name: 'lee' }]}
      renderRow={(row, index) => (
        <tr key={index}>
          <td>{row.name}</td>
        </tr>
      )}
    />
  )
}
import * as React from 'react'
import { UseComboboxProps, useCombobox } from 'downshift'

function Combobox<T extends Record<string, any>>(
  props: UseComboboxProps<T> & {
    renderItem: (item: T) => React.ReactNode
  }
) {
  const combobox = useCombobox(props)

  return (
    <div>
      <input
        placeholder="구성원을 검색해주세요."
        {...combobox.getInputProps()}
      />
      <div {...combobox.getMenuProps()}>
        {combobox.isOpen &&
          props.items.map((item, index) => (
            <div key={item.id} {...combobox.getItemProps({ item, index })}>
              {props.renderItem(item)}
            </div>
          ))}
      </div>
    </div>
  )
}

function App() {
  return (
    <Combobox
      items={[{ id: 1, name: 'eunsoo' }]}
      itemToString={(item) => `${item?.id}`}
      renderItem={(item) => {
        return <div>{item.name}</div>
      }}
      onInputValueChange={({ inputValue }) => {
        console.log(inputValue)
      }}
    />
  )
}
#480
import * as React from 'react'

type Prettify<T> = {
  [K in keyof T]: T[K]
} & {}

type T = React.ComponentProps<'div'>

type P = Prettify<T>
#481
import * as React from 'react'
import { IsEqual } from 'type-plus'

type ModalProps =
  | {
      variant: 'no-title'
    }
  | {
      variant: 'title'
      title: string
    }

export const Modal = (props: ModalProps) => {
  if (props.variant === 'no-title') {
    return <div>No title</div>
  } else {
    type Test = IsEqual<
      typeof props,
      {
        variant: 'title'
        title: string
      }
    >

    return <div>Title: {props.title}</div>
  }
}

export const Test = () => {
  return (
    <div>
      <Modal variant="title" title="Hello" />
      <Modal variant="no-title" />

      {/* @ts-expect-error */}
      <Modal />
      <Modal
        variant="no-title"
        // @ts-expect-error
        title="Oh dear"
      />
    </div>
  )
}
#482
import * as React from 'react'

export type NonEmptyString<T> = Exclude<T, ''>

type Props<T> = {
  src: NonEmptyString<T>
}

function Image<T extends string>({ src }: Props<T>) {
  return <img src={src} alt="" />
}

function App() {
  return (
    <>
      {/* @ts-expect-error */}
      <Image src="" />

      <Image src="ab" />
    </>
  )
}
#483
type Reducer<S, A> = (state: S, action: A) => S
type Listener<S> = (state: S) => void

interface Store<S, A> {
  getState: () => S
  subscribe: (listener: Listener<S>) => () => void
  dispatch: (action: A) => A
}

function createStore<S, A>(
  reducer: Reducer<S, A>,
  preloadedState: S
): Store<S, A> {
  let currentState: S = preloadedState
  let listeners: Listener<S>[] = []

  function getState(): S {
    return currentState
  }

  function subscribe(listener: Listener<S>): () => void {
    listener(currentState)

    listeners.push(listener)

    return function unsubscribe() {
      listeners = listeners.filter((l) => l !== listener)
    }
  }

  function dispatch(action: A): A {
    currentState = reducer(currentState, action)

    listeners.forEach((listener) => {
      listener(currentState)
    })

    return action
  }

  return {
    subscribe,
    getState,
    dispatch,
  }
}

type Item = {
  body: string
}

type State = {
  items: Item[]
}

type Action = {
  type: 'ADD'
  payload: Item
}

const store = createStore<State, Action>(
  (state, action) => {
    switch (action.type) {
      case 'ADD':
        return {
          ...state,
          items: state.items.concat(action.payload),
        }
      default:
        return state
    }
  },
  {
    items: [],
  }
)

store.subscribe((state) => {
  console.log(state.items)
})

store.dispatch({
  type: 'ADD',
  payload: {
    body: 'hello',
  },
})
#484
type Task = (callback: (result: string) => void) => void

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

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

  protected results: Array<string> = []

  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.push(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.push(result)

        this.index++

        executeTask()
      })
    }

    executeTask()
  }
}

Footnotes

  1. concurrently

#485

Advanced Features: Static HTML Export | Next.js

next export

앱의 HTML 버전을 빌드. .out 디렉토리에 빌드된 페이지 파일을 복사합니다.

#49
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

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

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


#491

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

Footnotes

  1. 클래스에 대한 구문, 메서드, 속성, 상속 등에 대한 포괄적인 설명

  2. 클래스, 프로토타입 및 팩토리 함수 사용과 같이 객체 지향 프로그래밍을 구현하는 다양한 접근 방식을 설명과 각 접근 방식의 예와 장단점들

#5
{
  "tailwindcss": "tailwindcss -i ./src/index.css -o ./src/tailwind.css",
  "start": "concurrently \"yarn tailwindcss --watch\"",
  "prebuild": "yarn tailwindcss --minify",
}

create-react-app 구형버젼(+eject)에서 설치할 경우 연관된 부분이 많아서 차라리 cli를 사용하는게 편한 상황. 그런데 concurrently1로 프로세스를 동시에 실행시켜야 되는 부분이 있다.


Footnotes

  1. https://tailwindcss.com/docs/tailwind-cli

#51


개인적으로는 Next.js를 이용한 방법이 가장 좋아보인다. window.*이 많다면 사전에 준비해두는게 좋을 것 같다.

#50

Footnotes

  1. XMLHttpRequest가 완전히 성공할때까지 정보를 주기적으로 호출하는 함수

  2. fetch 에서는 ReadableStream, Response 조합으로 구현 가능

  3. axios 에서는 onUploadProgress 콜백(progressEvent)으로 구현

#52
  • 조건에 따라서 브라우저에서 자동으로 발생1
  • OPTIONS 메서드로 요청
  • cloudfront에 OPTIONS 메서드 설정이 안되어 있다면 실패2

Footnotes

  1. 단순 요청(Simple requests)

  2. CloudFront 배포의 캐시 동작이 HTTP 요청에 대한 OPTIONS 메서드를 허용함

#53
const formData = new FormData();
const single = document.querySelector('input[type="file"]');
const multiple = document.querySelector('input[type="file"][multiple]');

formData.append('single', single.files[0]);

[...multiple].forEach((file, index) => {
  formData.append(`multiple_${index}`, file);
})

fetch('https://example.com/posts', {
  method: 'POST',
  body: formData,
})
#55

Promise를 거부할 때 객체 를 사용하도록 강제 합니다.1

// ❌
Promise.reject('An error occurred');

// ✅
Promise.reject(new Error('An error occurred'));

항상 Error객체Promise를 거부하는 것이 가장 좋습니다. 이렇게 하면 오류 개체가 스택 추적을 저장하기 때문에 오류가 발생한 위치를 더 쉽게 추적할 수 있습니다.


Footnotes

  1. 14 Linting Rules To Help You Write Asynchronous Code in JavaScript - Maxim Orlov

#56

imagemin을 사용하려고 하는데 module로 라이브러리가 업데이트 되어서 찾아본 내용. 혼란스러운 부분도 있지만 성숙해지는 과정이라고 본다.


Footnotes

  1. esm 배경과 tsup, unbuild 라는 두개의 해결방법을 소개해주고 있다.

#58

이미지 다운로드 구현. 이미지 응답값을 buffer로 변환해서 파일쓰기로 저장한다.

const fs = require('fs')
const util = require('util')
const fetch = require('node-fetch')

const writeFile = util.promisify(fs.writeFile)
const mkdir = util.promisify(fs.mkdir)

const FOLDER_PATH = 'FOLDER_PATH'

async function download({ url }) {
  const response = await fetch(url)
  const buffer = Buffer.from(await response.arrayBuffer())

  if (!fs.existsSync(FOLDER_PATH)) {
    await mkdir(FOLDER_PATH)
  }

  await writeFile(url, buffer)
}
#59

Footnotes

  1. markdown을 unified를 이용해서 파싱하고 html로 변환. (플러그인 기능을 추가해서 img->figcaption 기능 추가)

  2. AST가 무엇이며 일반 코드에서 어떻게 구축 하는지에 대한 설명과 기반으로 하는 사용 사례와 프로젝트 소개

  3. css 데이터 조작이 필요한 경우가 있어서 찾아봤다. 예를들어 특정 속성값만 추출해서 유닛값은 제거 한다든가.

#6
import sharp from 'sharp'
import fg from 'fast-glob'

const entries = await fg('./**/*.png')

for (const entry of entries) {
  const trimmedBuffer = await sharp(entry).trim().toBuffer()
  const trimmedImage = sharp(trimmedBuffer)
  const trimmedMetadata = await trimmedImage.metadata()

  trimmedImage
    .resize({
      width: Math.round(trimmedMetadata.width / 2),
      height: Math.round(trimmedMetadata.height / 2),
    })
    .png()
    .toFile('output.png')
}

offset(top, left)값, 이미지 사이즈 설정으로 crop 구현이 가능하다

sharp('img.png')
  .extract({
    left: 50,
    top: 50, 
    width: 200, 
    height: 400
  })

#60

음악 파일에서 메타데이터를 읽고 쓰기 위한 라이브러리


Footnotes

  1. MP3, MP4, OGG, FLAC 등 다양한 음악 형식을 지원. 제목, 아티스트, 앨범 등과 같은 음악 파일의 메타데이터에 액세스하기 위한 간단하고 사용하기 쉬운 API를 제공합니다.

  2. TagLib 바인딩

  3. 다양한 ID3 태그 버전을 지원하며 간단하고 사용하기 쉬운 API를 제공합니다.

#61

Footnotes

  1. 이 명령은 업그레이드를 수행하기 전에 오래된 패키지를 표시하여 사용자가 업그레이드할 패키지를 선택할 수 있도록 합니다.

  2. npm-check-updates는 지정된 버전을 무시하고 package.json 종속성을 최신 버전으로 업그레이드합니다 .

#62
  • 이미지 압축 기본 설정값 참조: mozjpeg(75), pngquant([75, 85])1

Footnotes

  1. https://github.com/antonreshetov/image-optimizer/blob/master/src/main/store/module/app.js#L32-L53

#63
// package.json 파일에 로컬 경로를 지정하는 방법. 파일 시스템에 있는 패키지 디렉터리를 사용할 수 있음.

{
  "dependencies": {
    "bar": "file:../foo/bar"
  }
}

#65

CSV와는 다르다! CSV와는!

const fs = require('fs')
const XLSX = require('xlsx')

const buf = fs.readFileSync('developer_labels_20181221.xlsx')
const workbook = XLSX.read(buf, { type: 'buffer' })

const roa = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], {
  header: 1,
})

const result = roa
  .slice(1)
  .map((item) => item.slice(1, 4))
  .filter((item) => item.some(Boolean))
  .reduce(
    (p, n) => {
      const [key, en, ko] = n
      const newKey = key.toLowerCase().replace(/\-/g, '_')

      p['en'][newKey] = en
      p['ko'][newKey] = ko

      return p
    },
    { en: {}, ko: {} }
  )

fs.writeFileSync('en.json', JSON.stringify(result.en), 'utf8')
fs.writeFileSync('ko.json', JSON.stringify(result.ko), 'utf8')

#64
# 특정 패키지에 모듈을 설치해야한다면
yarn workspace <workspace_name> <command>
#66

Footnotes

  1. https

  2. hosts

  3. Inspect는 iOS 기기에서 모바일 웹 앱과 웹사이트를 검사하고 디버그하기 위한 macOS, Linux 및 Windows용 새로운 개발자 도구입니다.

  4. 무료 redis

  5. 맥에서 웹뷰 디버깅이 필요하다면

#67

localtunnel은 쉽게 테스트하고 공유할 수 있도록 로컬 호스트를 공개합니다! 다른 사람들이 변경 사항을 테스트하도록 하기 위해 DNS를 엉망으로 만들거나 배포할 필요가 없습니다.

app.listen(PORT, async () => {
  const tunnel = await localtunnel({
    port: PORT,
    subdomain: name,
  })

하지만 너무 느려서 ngrok 쓰는게 현실적일수도 있겠다. -20220917



Footnotes

  1. 로컬 환경에서 웹훅 테스트를 위해, API 엔드포인트를 만들고 localtunnel이나 ngrok을 사용하기.

#68
#69

Designing a JavaScript Plugin System | CSS-Tricks1


Footnotes

  1. 플러그인은 라이브러리와 프레임워크의 공통 기능이며 개발자가 안전하고 확장 가능한 방식으로 기능을 추가할 수 있도록 한다. 그래서 추가 유지 관리 부담이 없다.

#7

서비스워커로 fetch를 감지해서 해당 기능을 구현한다는 내용. 개인적으로는 mock은 간단하게 구현 가능할 것 같은데 이미 같은 기능의 잘 만들어진 라이브러리들이 있으니까 아이디어 정도로 생각하면 될 것 같다.

addEventListener('fetch', e => { 
  // e.request
  // e.respondWith
})
#70

redux 사용하다가 trace가 필요하면 해당설정을 활성화 시켜주면 된다. 메모리릭 가능성이 있기 때문에…우회 한다면 약간 무식한 방법이긴 하지만 거기다 싶은 지점에서 console.trace()

#71

Footnotes

  1. cal이 단순한 달력을 보여주지만 ical-buddy는 맥에 연결된 달력을 보여주는 유틸리티

  2. gui의 현재와 cli에서 개선될 수 있는 부분들(접근성, 발견가능성, 상호작용)을 만들어 나가는 내용

  3. todo 예제로 oclif의 편리한 기능을 알려주고 있다

  4. deno에서 std, date_fns, ascii_table 모듈을 활용해 날씨 cli앱을 만드는 과정 소개

  5. curl보다 좀 더 직관적인 인터페이스로 진입장벽을 낮춘 클라이언트

  6. httpie

  7. 간단한 alias 설정과, arguments값을 받을 수 있도록 .bash_profile에 설정하는 방법 소개

  8. cat, du, find…등등등 기존 명령어에서 개선이 가능한 부분을 설명해주고 있다.

  9. mkdir, touch, 텍스트 > 파일로 생성하기에 대한 설명

  10. sed 명령어 간단한 사용방법

  11. doctor에서 영감을 받아 여러 작업 환경에 대한 정보를 출력해주는 스크립트를 작성한 내용을 알려주고 있다. 구두로 불필요하게 소비되는 부분이 많은데 정리해볼 만한 내용.

  12. Fig는 VSCode 스타일의 자동 완성을 기존 터미널에 추가합니다.

  13. 여러가지 터미널 도구들 정리한 글

#73

서버/프론트 구분이 없는 환경일 경우 옜날에는 이렇게 세팅해서 개발

const bs = require('browser-sync').create()

bs.init({
  port: 8081,
  https: false,
  proxy: 'http://localhost:8080',
  serveStatic: [
    {
      route: '/resources',
      dir: 'src/main/webapp/resources',
    },
  ],
  files: 'src/main/webapp/resources/**/*',
  open: false,
})
#72
extractFrames() {
  ffmpeg -i $1 -vf fps=30 output_frame_%d.png
}

# $1 = frames로 사용될 파일들
# $2 = output 파일
# Example: concatFrames ./frame_%5d.png output.webm
concatFrames() {
  ffmpeg -framerate 30 -i $1 -c:v libvpx-vp9 -pix_fmt yuva420p $2
}

# $1 = frame로 사용될 파일
# $2 = output 파일
# Example: frameToVideo frame.jpg output.mp4
frameToVideo() {
  ffmpeg -loop 1 -i $1 -c:v libx264 -t 10 -pix_fmt yuv420p $2
}

# $1 = 인코딩할 영상 video.mp4
# $2 = output 파일명
encodingVideo() {
  ffmpeg -an -i $1 -vcodec libx264 -pix_fmt yuv420p -profile:v baseline -level 3 "${$2}.mp4"
  ffmpeg -i "${$2}.mp4" -vcodec libvpx-vp9 -b:v 1M -acodec libvorbis "${$2}.webm"
}

#74
# /home/USERNAME/.zshrc

HOME="/mnt/c/Users/cbcru"
DL="$HOME/Downloads"

if [[ $PWD == $HOME ]]; then
    cd $DL
fi
#75
  • 혜택은 웹뷰로 존재함
  • GraphQL을 사용 중
useSWR(
  `{
    pages {
      user_group
      highlight_color
      page_list {
        type
        page_id
        page_name
        is_new
      }
    }
  }`,
  (query) => request('https://api.zigzag.kr/api/2/graphql', query)
)
#77

검색기능을 정말 간단하게 구현하고 싶을때: 프로그래밍 가능한 검색 엔진을 사용하면 웹사이트, 블로그 또는 웹사이트 모음에 대한 검색 엔진을 만들 수 있습니다. 웹 페이지와 이미지를 모두 검색하도록 엔진을 구성할 수 있습니다. 순위를 미세 조정하고 자신의 프로모션을 추가하고 검색 결과의 모양과 느낌을 사용자 지정할 수 있습니다. 엔진을 Google 애드센스 계정에 연결하여 검색으로 수익을 창출할 수 있습니다.


#78
#79

Quick tip: reusable Array search predicates - JASON Format

arr.filter(callback(element[, index[, array]])[, thisArg])

배열 메서드에서 2번째 인자 thisArg에 참조값을 전달해서 재사용 가능한 함수를 만드는 트릭. 단 성능 이슈가 있으므로 주의해야 한다.

#8

Footnotes

  1. GitHub GraphQL API를 통해 쿼리할 수 있는 다양한 개체에 대한 개요.

  2. GitHub에서 플랫 데이터를 사용하는 방법에 대한 튜토리얼. 플랫 데이터는 개발자가 API 또는 CSV 파일과 같은 다양한 소스의 데이터를 GitHub 리포지토리로 쉽게 가져올 수 있는 기능입니다. 이 자습서에서는 GitHub Actions와 함께 플랫 데이터를 사용하여 다양한 소스에서 데이터 가져오기를 자동화하는 방법과 가져온 데이터를 사용하여 시각화 및 대화형 웹 애플리케이션을 만드는 방법을 다룹니다. 또한 플랫 데이터를 사용하는 방법을 보여주는 예제 프로젝트에 대한 링크를 제공합니다. 여기에는 Google 스프레드시트를 사용한 플랫 데이터 데모 저장소와 독일의 코로나19 백신 접종 기록 데이터가 포함된 저장소가 포함됩니다.

  3. 사용자가 GitHub의 공개 리포지토리 내에서 코드 조각을 검색할 수 있는 GitHub 코드 검색에 대한 링크입니다. 다른 개발자의 코드를 빠르게 찾고 탐색할 수 있는 방법을 제공하며 새로운 라이브러리, 프레임워크 및 모범 사례를 찾는 데 사용할 수 있습니다. 검색 결과는 리포지토리, 언어, 파일 유형 및 기타 기준으로 필터링할 수 있으며 관련성, 별 또는 분기별로 정렬할 수 있습니다.

  4. https://grep.app/

#83

Cool Cats: The Coolest NFT’s on the Blockchain!

const imageSrc = `https://s3.amazonaws.com/api.coolcatsnft.com/thumbnails/${ID}_thumbnail.png`
const query = {
  sortBy: 'token_id_asc',
  limit: 48,
  page: 1,
  face: 'angry',
  hats: 'admiral',
  shirt: 'astro',
  tier: 'cool_1',
}

fetch(
  'https://prod-api.coolcatsnft.com/cat?sortBy=token_id_asc&limit=48&page=1&tier=${tier}'
)
  • face angry, angry cute, angry scar, beard brown, beard pirate, beard tan, derp, ditto, dizzy, double face, face face, glasses, glasses funny, glossy, grin, happy, happy cute, hearts, mononoke, mummy, ninja black, ninja blue, ninja red, owo, pixel, rich, shocked, smirk, stunned, sunglasses blue, sunglasses cool, sunglasses cowboy, sunglasses heart, sunglasses pixel, sunglasses squad, sunglasses yellow, three eyes, tired, tvface 404, tvface bobross, tvface nosignal, tvface xp, unamused, uwu, wink, zombie
  • hat admiral, admiral pink, afro black, afro brown, afro rainbow unicorn, antlers, apple, arrowhead, astro, astro cheeks, astro fishbowl, beanie black, beanie blue, beanie orange, beanie red, beret black, beret green, beret pink, beret red, bow, bucket hat blue, bucket hat green, bucket hat tan, bucket hat white, candle, costume dragon, costume frog, costume gorilla, cowboy black, cowboy brown, crown black, crown fire, crown gold, cupcake, deepsea bronze, deepsea orange, dutch, flower blue, flower pink, flower red, goggles seaweed, halo, halo fire, hat black, hat skull, hat visor blue, hat visor yellow, hat white, headband blue, headband red, helm army, helm biker, helm bronze, helm silver, horns, knight black, knight blue, knight red, mohawk green, mohawk purple, mohawk red, mullet blonde, mullet brown, ninja black, ninja blue, ninja red, nurse, piercings, pirate black, pirate red, prince, sunhat black, sunhat tan, sunhat white, sushi, top hat, tvhead grey, tvhead purple, tvhead white, unicorn horn, visor green, visor purple, wreath, wreath flowers
  • shirt astro, astro black, astro orange, bandana green, bandana purple, bandana red, baseball blue, baseball red, buttondown black flannel, buttondown blue flannel, buttondown green, buttondown red flannel, buttondown tan, chain, combat black, combat green, costume dragon, costume frog, costume gorilla, costume hotdog, cowboy black, cowboy brown, deepsea bronze, deepsea orange, epaulette black, epaulette red, epaulette white, gown black, gown purple, gown white, hoodie black, hoodie purple, hoodie red, knight, knight black, knight leather, labcoat, lederhosen, monk, mononoke, ninja black, ninja blue, ninja red, nurse, overalls blue, overalls flannel, overalls pink, overalls red, overalls yellow, pirate black, pirate red, punk, robe blue, robe king, robe red, robe white, shirt bowtie, shirt white, shirt yellow, sweater black, sweater green chain, sweater orange, sweater pink, tanktop orange, tanktop pink, tanktop sailor black, tanktop sailor blue, tanktop sailor red, tanktop tattoo, tanktop white, tiger, toga, tshirt blue, tshirt green, tshirt metal, tshirt pink, tshirt red, tshirt white, tshirt yellow, viking brown, viking navy, wetsuit, winter blue, winter red, work blue, work red
  • tier cool_1, cool_2, wild_1, wild_2, classy_1, classy_2, exotic_1, exotic_2
#84
function getReverseGeocodeData() {
  const response = Maps.newGeocoder()
    .setLanguage('ko')
    .reverseGeocode(40.758577, -73.984464)

  return response.results.map((result) => {
    return {
      formatted_address: result.formatted_address,
      lat: result.geometry.location.lat,
      lng: result.geometry.location.lng,
    }
  })
}
#85

hash 링크로 연결될 경우 스크롤위치가 최상단으로 위치하기 때문에 문제(헤더가 고정일 경우)가 있을수도 있어서 scroll-margin-top으로 제어가 가능한 부분을 설명하고 있다.


Footnotes

  1. 2ex 유닛을 사용하여 선택한 글꼴의 x 높이의 상대적인 크기로 설정.

#87

Footnotes

  1. 애플 웹사이트를 예제로 해서 스크롤 기반 canvas 이미지 교체하는 방식을 설명하고 있다. 문제는 이미지 리소스 크기가 너무 크다.

#88
#89

Footnotes

  1. 레코드 및 튜플 유형의 구문과 기능을 소개

  2. 레코드 및 튜플 유형, 해당 구문, 기능 및 사용 사례에 대한 자세한 설명과 예제

  3. React에서 레코드 및 튜플 유형을 사용하여 유형 안전성을 개선하고 상용구 코드를 줄이며 개발자 생산성을 향상시키는 방법

#9

Footnotes

  1. 소셜 공유 이미지를 직접 생성하는 방법

  2. 웹성능을 측정할수 있는 가이드를 제공

  3. 프로덕션 html을 가져와서 로컬에서 작업중인 부분과 결합한 개발 방식

  4. 초기 실행 시 args를 전달해야 하는 경우가 있는데 참고해서 실행시키면 된다.

  5. netlify에서 사용(설치포함) 가능한 방법

  6. pptr 에서 파일 다운로드 컨트롤 하는 방법

  7. M1 설치 가이드

#90

httpOnly, secure 플래그 쿠키값이 필요한 경우 CDPSession 실행 후 클라이언트와 통신한다.

const client = await page.target().createCDPSession()
const data = await client.send('Network.getAllCookies')

로컬에 해당 쿠키정보를 임시로 저장해서 재활용하고 만료 시 갱신하는 방법을 사용.

fs.writeFileSync('cookies.json', JSON.stringify(data))

const { cookies } = JSON.parse(fs.readFileSync('cookies.json', 'utf8'))
await page.setCookie(...cookies)
#91

setExtraHTTPHeaders 호출하기

await page.setExtraHTTPHeaders(headers)

인터셉트로 가로채기

await page.setRequestInterception(true)

page.on('request', (request) => {
  const headers = {
    ...request.headers()
  }

  interceptedRequest({ headers })
})
#92

Footnotes

  1. 다양한 디바이스 사이즈 확인이 필요할 경우 유용한 앱 소개. 개인적으로는 responsively.app 추천

  2. 자동 유형 생성으로 페이로드 확인을 위한 웹훅 테스트 도구

#94

이미지 저장1

  • 페이지에서 JavaScript를 통해 이미지 추출
    • 캔버스에서 이미지 추출
    • 서버에서 이미지 가져오기
  • DevTools 프로토콜을 사용하여 이미지 추출
const tree = await page._client.send('Page.getResourceTree')

for (const resource of tree.frameTree.resources) {
  const { content } = await page._client.send(
    'Page.getResourceContent',
    { frameId: String(page.mainFrame()._id), url: resource.url },
  )
  const contentBuffer = Buffer.from(content, 'base64')
}
page.on('response', async (response) => {
  const url = response.url()
  const buffer = await response.buffer()
})

Footnotes

  1. Saving Images from a Headless Browser

#93
  1. 파일질라 서버를 설치
  2. Edit->Users->Add / 사용자 추가
  3. 제어판->시스템 및 보안->Windows Defender 방화벽->허용되는 앱 / (설정변경|다른 앱 허용) 파일질라 서버 추가
#95

Using Slack Slash Commands to Send Data from Slack into Google Sheets

  1. 사람들이 추천한 책을 기록한다
  2. 슬랙 -> 구글시트
  3. 슬랙 커맨드 설정 /book + 구글앱스 스크립트 url 연결1
  4. POST로 전송 받은 데이터값을 기반으로 데이터 처리 완료

Footnotes

  1. 꿀벌개발일지 :: 구글 앱스 스크립트에서 비동기 작업 추가하기 doPost에서 비동기 처리

#97
#99