notes

2023. 6. 14.
`column-count`

이력서를 리액트 컴포넌트로 작성하다가 2단 처리를 어떻게 해야 되나 싶어서 알아보니 column-count 라는 프로퍼티가 있었고,

column-count: 2;
column-count: 2;

하면 되는 것이었다...

column-count: <number>;
column-count: <number>;

기본적으로 위와 같이 쓰면 되고, 이런 저런 관련 프로퍼티가 있는데,

column-width

column-count: auto;
column-width: 8rem;
column-count: auto;
column-width: 8rem;

content 분량에 따라 8rem width에 맞춰 자동으로 채워진다.

column-fill

column-fill: auto;
column-fill: balance;
column-fill: auto;
column-fill: balance;
  • auto: 일반적으로 기대되는 분량에 따라 채워지는 동작
  • balance: column-count에 지정된 대로 content를 나누어 각 column에 배치

column-gap

ㅇㅇ 그것임

column-rule

규칙인가? 싶지만 그게 아니고 각 컬럼 사이에 라인을 그어준다.

column-rule: dotted;
column-rule: solid 8px;
column-rule: solid blue;
column-rule: thick inset blue;
column-rule: dotted;
column-rule: solid 8px;
column-rule: solid blue;
column-rule: thick inset blue;

이런 느낌.

column-span

특정 요소가 cols 사이를 가로지르게 만들 수 있다. 아래는 mdn의 예시.

image

2023. 6. 10.
Custom Nerd Fonts

nvim등 터미널 환경에선 nerd fonts(기존 폰트에 아이콘 등 각종 UI용 문자를 추가한 폰트)를 쓰는 게 편한데 유료 폰트이거나 폰트 취향이 너무 마이너인 경우 직접 만들써야 함. 귀찮아서 한동안 걍 Iosevka 쓰다가 어제 잠깐 의욕이 생겨서 알아보니 같은 곳에서 제공하는 Font Patcher를 사용하면 되는 것이었고, 이런저런 실행 방법을 제공하는데 그 중 docker로 하는게 젤 간단했음.

docker run --rm \
    -v /path/to/original/font:/in \
    -v /path/to/patched/font:/out \
    nerdfonts/patcher \
    --progressbars \
    --adjust-line-height \
    --fontawesome \
    --fontawesomeextension \
    --fontlogos \
    --octicons \
    --codicons \
    --powersymbols \
    --pomicons \
    --powerline \
    --powerlineextra \
    --material \
    --weather
docker run --rm \
    -v /path/to/original/font:/in \
    -v /path/to/patched/font:/out \
    nerdfonts/patcher \
    --progressbars \
    --adjust-line-height \
    --fontawesome \
    --fontawesomeextension \
    --fontlogos \
    --octicons \
    --codicons \
    --powersymbols \
    --pomicons \
    --powerline \
    --powerlineextra \
    --material \
    --weather

위의 /path/to/original/font, /path/to/patched/font는 폴더이고 original의 경우 그 안에 폰트 파일이 들어있으면 된다. 작업이 완료되면 patched 폴더 안에 생성된 폰트 파일을 설치해서 쓰면 됨.

Before: Before

After: After

Links

2023. 6. 8.
Meditation

  • 명상의 핵심은 퉁쳐서 말해 현재(구체적으로는 호흡)에 집중하고 떠오르는 생각을 인지한 채로 engage하지 않는 것.
  • 의학적으로 유의미한 명상의 실행 유무에 따른 차이가 나타나는 데는 대략 8주 정도가 걸린다고 함.
  • 그리고 세션당 20분 이상(정확히는 17분 +) 지속시 확실한 의학적 효과가 관찰된다고 함.
  • HPA Axis(시상하부-뇌하수체-부신 축)가 명상으로 인해 'shutdown' 되는데 그 정도의 시간이 걸리기 때문.
  • 의학적 효과는 HPA Axis 활성화도/코티졸 농도 정상화?라고 퉁쳐서 말할 수 있음.
  • 코티졸 농도가 높으면 백혈구 양이 증가하고 그로 인해 자가면역질환이 발생하는데, 거기에 명상이 직접적으로 도움을 줄 수 있음.
  • 명상을 하다가 조는 것은 매우 일반적인 현상.
  • 명상을 하다가 자꾸 딴 생각을 하는 것도 매우 자연스러운 현상.
  • 명상 state를 '유지'하는 것보다 다시 명상 state로 돌아오는 행동이 '반복'되는 것이 더 중요할 수 있음.

2023. 6. 5.
`URL` object

const url = new URL('https://sehyunchung.dev')

url.toString() // 'https://sehyunchung.dev/' <- trailing slash 생기는 것에 유의

url.pathname = 'post/1'

url.toString() // 'https://sehyunchung.dev/post/1' <- trailing slash 없음

url.searchParams.set('key', 'value')

url.toString() // 'https://sehyunchung.dev/post/1?key=value'
const url = new URL('https://sehyunchung.dev')

url.toString() // 'https://sehyunchung.dev/' <- trailing slash 생기는 것에 유의

url.pathname = 'post/1'

url.toString() // 'https://sehyunchung.dev/post/1' <- trailing slash 없음

url.searchParams.set('key', 'value')

url.toString() // 'https://sehyunchung.dev/post/1?key=value'
// console.log(url)
{
  hash: ""
  host: "sehyunchung.dev"
  hostname: "sehyunchung.dev"
  href: "https://sehyunchung.dev/post/1?key=value"
  origin: "https://sehyunchung.dev"
  password: ""
  pathname: "/post/1"
  port: ""
  protocol: "https:"
  search: "?key=value"
  searchParams: URLSearchParams {size: 1}
  username: ""
}
// console.log(url)
{
  hash: ""
  host: "sehyunchung.dev"
  hostname: "sehyunchung.dev"
  href: "https://sehyunchung.dev/post/1?key=value"
  origin: "https://sehyunchung.dev"
  password: ""
  pathname: "/post/1"
  port: ""
  protocol: "https:"
  search: "?key=value"
  searchParams: URLSearchParams {size: 1}
  username: ""
}

url.pathname = ''

url.toString() // "https://sehyunchung.dev/"

url.pathname = 1

url.toString() // "https://sehyunchung.dev/1"

url.pathname = null

url.toString() // "https://sehyunchung.dev/null" ?!

url.pathname = undefined

url.toString() // "https://sehyunchung.dev/undefined" ?!
url.pathname = ''

url.toString() // "https://sehyunchung.dev/"

url.pathname = 1

url.toString() // "https://sehyunchung.dev/1"

url.pathname = null

url.toString() // "https://sehyunchung.dev/null" ?!

url.pathname = undefined

url.toString() // "https://sehyunchung.dev/undefined" ?!

2023. 6. 3.
🫠 Hydration Mismatch 🫠 (2)

요기서 이것 저것 해봤는데 다 별로인 것 같다. 왜냐면 다 hydration mismatch를 해결하는 게 아니고 피해가는 것이기 때문인듯...

그러니까 애시당초 'mismatch'가 발생하는 건

  1. client state가 client에서만 액세스 가능한 곳에 persist 되어있어서
  2. server에서 액세스가 안되니까
  3. UI 상태가 달라질 수 밖에 없다.

인데 그렇다면

  1. client state persist를
  2. server에서 액세스 가능한 곳에 하면 되는 것잉게롱.

그래서 좀 손이 가지만 걍 이렇게 해봤는데,

  1. persist엔 cookie를 사용한다.
  2. 왜냐면 쿠키는 서버 컴포넌트에서 읽기 가능이므로. 암튼 그래서 플로우는,
  3. 서버 컴포넌트에서 cookie를 읽어서 고 안에 들어있는 persisted state를 가져온다.
  4. mismatch가 발생하는 클라이언트 컴포넌트에 위 state를 넘겨줄 prop을 하나 뚫는다.
  5. 그리고 같은 클라이언트 컴포넌트에 useState로 local state를 하나 만드는데,
  6. 만들면서 initialState로 위 서버 컴포넌트에서 받아온 prop을 넘겨준다.
  7. useEffect를 하나 추가해서
  8. 클라이언트 컴포넌트가 쓰고 있는 persisted store을 local state에 묶어준다.
  9. 깅까 대략
    export function useSyncedState<T>(clientState: T, serverState?: T) {
      const [state, setState] = React.useState<T>(serverState ?? clientState)
    
      React.useEffect(() => {
        setState(clientState)
      }, [clientState])
    
      return state
    }
    export function useSyncedState<T>(clientState: T, serverState?: T) {
      const [state, setState] = React.useState<T>(serverState ?? clientState)
    
      React.useEffect(() => {
        setState(clientState)
      }, [clientState])
    
      return state
    }
  10. 왈료

이러면

  1. 어차피 서버도 클라이언트도 같은 걸 보고 있으므로 애시당초 mismatch가 아님.
  2. hydration이 되기 전에 이미 같은 상태의 UI가 보이므로 깜빡임 같은 게 없음.

근데:

  1. 서버 호출을 해야 됨
  2. 그래서 요렇게 한 컴포넌트가 들어있는 페이지는 static export가 안됨

2023. 6. 2.
🫠 Hydration Mismatch 🫠 (1)

이런 저런 이유로 클라이언트 상태를 localStorage 등에 persist 하고 있을 경우 server/client mismatch가 발생할 수 밖에 없는데 그래서 서버에서 프리렌더가 안되게 하려면 아래와 같은 난리 법석이 필요.

  1. useState + useEffect
    function Comp() {
      const storeState = useStoreState()
            ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우,
    
      const [state, setState] = React.useState()
      React.useEffect(()=>{
        setState(storeState)
      },[])               // 2) 이런 난리 법석 후에
    
      return <div>{state}</div> // 3) 이러면 통과
    function Comp() {
      const storeState = useStoreState()
            ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우,
    
      const [state, setState] = React.useState()
      React.useEffect(()=>{
        setState(storeState)
      },[])               // 2) 이런 난리 법석 후에
    
      return <div>{state}</div> // 3) 이러면 통과
  2. useMounted + return null
    // 0) 일단 이런 난리 법석을 만들어두고
    const useMounted = () => {
      const [m, sM] = React.useState(false)
      //     ^^^^^ 귀찮아서 대충 씀
      React.useEffect(()=>{
        sM(true)
      }, [])
      return m
    }
    // 0) 일단 이런 난리 법석을 만들어두고
    const useMounted = () => {
      const [m, sM] = React.useState(false)
      //     ^^^^^ 귀찮아서 대충 씀
      React.useEffect(()=>{
        sM(true)
      }, [])
      return m
    }
    function Comp() {
      const storeState = useStoreState()
            ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우,
    
      const mounted = useMounted()
      if (!mounted) return null  // 2) 이러고 나서
    
      return <div>{storeState}</div> // 3) 이러면 통과
    function Comp() {
      const storeState = useStoreState()
            ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우,
    
      const mounted = useMounted()
      if (!mounted) return null  // 2) 이러고 나서
    
      return <div>{storeState}</div> // 3) 이러면 통과
  3. next/dynamic + { ssr: false } ☜ 이게 기분이 제일 덜 나쁜듯
    const Comp = dynamic(() => import('path/to/comp'), { ssr: false }); // 젤 간단?
    const Comp = dynamic(() => import('path/to/comp'), { ssr: false }); // 젤 간단?
    하지만 캐치가 하나 있는데, Comp 는 무조건 export default 여야 함.
    const Comp = dynamic(() => import('path/to/comp').then(mod => mod.Comp), { ssr: false })
                                                        // ^^^^^^^^^^^^^^^ 이러면 💣
    const Comp = dynamic(() => import('path/to/comp').then(mod => mod.Comp), { ssr: false })
                                                        // ^^^^^^^^^^^^^^^ 이러면 💣
    대신 loading으로 서스펜스 간지를 낼 수 있음(...)
    const Comp = dynamic(() => import('path/to/comp'), { ssr: false, loading: () => <Sekeleton /> }); // 이 가능
    const Comp = dynamic(() => import('path/to/comp'), { ssr: false, loading: () => <Sekeleton /> }); // 이 가능
    물론 next 한정이지만요...

이거 다 별루고... 로 시작하는 글을 한참 쓰고 있었는데 브라우저 꺼져서 날아감...

2023. 6. 2.
`blurDataUrl` 만들기

갯츠비는 알아서 해주지만 넥스트는 해줘야 되기 때문에...

import { decode, encode } from "blurhash"
import sharp from "sharp"

const loadImageData = async (src: string) => {
  const response = await fetch(src)
  if (!response.ok)
    throw new Error(
      `Failed to load image: ${response.status} ${response.statusText}`
    )

  const imageBuffer = await response.arrayBuffer()

  const { data, info } = await sharp(imageBuffer)
    .ensureAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true })

  return {
    data: new Uint8ClampedArray(data),
    width: info.width,
    height: info.height,
  }
}

export const encodeImageToBlurhash = async (imageUrl: string) => {
  const { data, width, height } = await loadImageData(imageUrl)
  return encode(data, width, height, 4, 4)
}

export const blurhashToBase64 = async (
  blurhash: string,
  width: number,
  height: number
) => {
  const pixels = decode(blurhash, width, height)
  const webp = sharp(Buffer.from(pixels), {
    raw: { width, height, channels: 4 },
  }).webp()
  const dataString = (await webp.toBuffer()).toString("base64")

  return `data:image/png;base64,${dataString}`
}

export const generateBlurDataUrl = async (
  imageUrl: string
): Promise<string | undefined> => {
  try {
    const blurhash = await encodeImageToBlurhash(imageUrl)
    return await blurhashToBase64(blurhash, 4, 4)
  } catch (error) {
    console.error(error)
    return undefined
  }
}
import { decode, encode } from "blurhash"
import sharp from "sharp"

const loadImageData = async (src: string) => {
  const response = await fetch(src)
  if (!response.ok)
    throw new Error(
      `Failed to load image: ${response.status} ${response.statusText}`
    )

  const imageBuffer = await response.arrayBuffer()

  const { data, info } = await sharp(imageBuffer)
    .ensureAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true })

  return {
    data: new Uint8ClampedArray(data),
    width: info.width,
    height: info.height,
  }
}

export const encodeImageToBlurhash = async (imageUrl: string) => {
  const { data, width, height } = await loadImageData(imageUrl)
  return encode(data, width, height, 4, 4)
}

export const blurhashToBase64 = async (
  blurhash: string,
  width: number,
  height: number
) => {
  const pixels = decode(blurhash, width, height)
  const webp = sharp(Buffer.from(pixels), {
    raw: { width, height, channels: 4 },
  }).webp()
  const dataString = (await webp.toBuffer()).toString("base64")

  return `data:image/png;base64,${dataString}`
}

export const generateBlurDataUrl = async (
  imageUrl: string
): Promise<string | undefined> => {
  try {
    const blurhash = await encodeImageToBlurhash(imageUrl)
    return await blurhashToBase64(blurhash, 4, 4)
  } catch (error) {
    console.error(error)
    return undefined
  }
}

여러가지 더 있다.