notes

2023. 6. 1.
`aria-current`

현재 페이지/단계/위치/시간/등등을 나타낼 수 있는 어트리뷰트이고 대략 이런 느낌이다.

<ul>
<!-- ... -->
  <li aria-current="page">
    <a href="">link</a>
  </li>
<!-- ... -->
</ul>
<ul>
<!-- ... -->
  <li aria-current="page">
    <a href="">link</a>
  </li>
<!-- ... -->
</ul>

다양한 걸 다룰 수 있음:

  • page: Represents the current page within a set of pages such as the link to the current document in a breadcrumb.
  • step: Represents the current step within a process such as the current step in an enumerated multi step checkout flow .
  • location: Represents the current location within an environment or context such as the image that is visually highlighted as the current component of a flow chart.
  • date: Represents the current date within a collection of dates such as the current date within a calendar.
  • time: Represents the current time within a set of times such as the current time within a timetable.
  • true: Represents the current item within a set.
  • false (default): Does not represent the current item within a set.

mdn

2023. 5. 29.
MDX에 무심코 <3을 쓰면 터진다

<3은 emoji가 없던 시절 하트를 표시하던 방법이다. 좀 전에 graphite cheatsheet til을 쓰면서 무심코 하트를 <3으로 했었는데 로컬 빌드가 터져서 음? 하고 보니 당연한 얘기였다.

MDX에게 <jsx의 시작이기 때문에...


추가:

  • <>도 터짐
  • <-도 터짐

2023. 5. 29.
Graphite cheatsheet

https://graphite.dev/docs/graphite-cheatsheet

어제부터 본격적으루다가 써보는 중. cli 베이스인 것도 맘에 들고 플로우도 맘에 들고 이래저래 편하고 좋당🖤

커밋하기

gt bc -am '<message>'
gt bc -am '<message>'

브랜치를 만들지 않고 변경+커밋하면서 브랜치까지 만드는 플로우. 브랜치 이름도 알아서 만들어줌.

생성되는 브랜치명의 기본 템플릿은 아래와 같음.

mm-dd-commit_message
mm-dd-commit_message

변경 추가

gt add -A && gt ca
gt add -A && gt ca

기본적으로 git commit --amend와 같음. 그냥 커밋 추가도 가능하긴 한데(gt cc) 브랜치당 하나의 커밋이 스타일이므로 (변경의 영역/성격 등이 다른데 관련이 있다면 일단 PR을 보내고 거기다 다시 PR을 보내라는 스타일) 이렇게 하라고 되어있음.

PR 보내기

gt ss
gt ss

Merge

graphite 웹앱에서 할 수도 있고 걍 깃헙에서 해도 되고.

마무리

gt rs -r
gt rs -r

로 싱크를 맞춤. mainpull 해주고 머지된 작업 브랜치를 자동으로 지워줌. 꼬리 물기 PR 싱크도 요 단계에서 처리.

2023. 5. 26.
ms clarity

ms의 analytics 툴 clarity의 설치 스크립트는 이렇게 생겼다.

<script type="text/javascript">
    (function(c,l,a,r,i,t,y){
        //    ^^^^^^^^^^^^^  귀엽...
        c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
        // ...
</script>

2023. 5. 25.
Extracting Image Dimensions from Remote Sources

Before

  • til을 위해 repo issues를 cms로 쓰고 있었고,
  • issue body render를 위해 github api reponse 중 bodyHTMLdangerously... 에 넣어주고 있었음.
  • 근데 이러면 next가 해주는 이것저것이 아까우므로,
  • api response 중 body(markdown string)을 mdx renderer에 넘겨서 써야겠다고 생각.

Issues

How?

  • probe-image-size라는 라이브러리가 있고,
  • mdx renderer option의 component 설정시 <img /> -> <Image />로 replace 하면서 요걸 사용해서 width/height 정보를 넘겨주면 됨.
  • 코드는 대충 이런식:
    import probe from "probe-image-size"
    // ...
    const components: MDXRemoteProps["components"] = {
      // ...
      // @ts-expect-error <- ts는 아직 async 컴포넌트를 모르지만 우리는 rsc 세계로 넘어왔으므로 ok.
      img: async ({ src, alt }) => {
        if (!src) return null
    
        const { width, height } = await probe(src ?? "")
        //                        ^^^^^ rsc ftw...
    
        if (!width || !height) return null
    
        return <Image src={src} alt={alt ?? ""} width={width} height={height} />
      },
      // ...
    }
    // ...
    import probe from "probe-image-size"
    // ...
    const components: MDXRemoteProps["components"] = {
      // ...
      // @ts-expect-error <- ts는 아직 async 컴포넌트를 모르지만 우리는 rsc 세계로 넘어왔으므로 ok.
      img: async ({ src, alt }) => {
        if (!src) return null
    
        const { width, height } = await probe(src ?? "")
        //                        ^^^^^ rsc ftw...
    
        if (!width || !height) return null
    
        return <Image src={src} alt={alt ?? ""} width={width} height={height} />
      },
      // ...
    }
    // ...

Result

최종 렌더된 이미지의 url을 보면

Screenshot 2023-05-25 at 17 18 40

next가 잘 처리하고 있음을 알 수 있음.

Conclusion

RSC는 대박이다...

2023. 5. 24.
`generateStaticParams`를 사용하는 페이지에서 서버 액션을 호출하면 `405` 에러 (2023-05-24 현재)

좋아요 버튼을 달려고 @vercel/kv 랑 요렇게 저렇게 해보고 있었는데 아래 에러가 무한히 발생했다.

Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
    at AppContainer (/Users/sehyunchung/personal/sehyunchung.dev/node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/server/render.js:337:29)
    at AppContainerWithIsomorphicFiberStructure (/Users/sehyunchung/personal/sehyunchung.dev/node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/server/render.js:373:57)
    at div
    at Body (/Users/sehyunchung/personal/sehyunchung.dev/node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/server/render.js:673:21)
Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
..

검색을 해봐도 뭐가 안나와서 'use server'를 파일에 넣었다 함수에 넣었다 이케저케 해봐도 안됐는데 어쩌다 브라우저 콘솔을 열어보니 Screenshot 2023-05-24 at 17 47 28

네트워크 탭을 확인해보니 Screenshot 2023-05-24 at 17 47 54

요걸로 검색해보니 아래 이슈가 나왔다.

[NEXT-1167] Server Actions 405 "Method Not Allowed" when using generateStaticParams #49408

생각해보면 말이 되는 것 같기도... 근데 안되면 안되는데?

2023. 5. 24.
`@vercel/og` Cheatsheet (?)

  1. @vercel/og
  2. Edge Runtime 환경 기반으로 동작함.
  3. 이미지 렌더에 satori를 사용하는데,
  4. app router 사용시 app/og/route.tsx 혹은 app/og.tsx 등등으로 파일을 만들면 됨.
    • .ts도 사용할 수 있으나 고러면 jsx를 사용할 수 없겟쥬
  5. 커스텀 폰트를 사용할 수 있으나 next/font 외에 별도로 로컬에서 폰트 파일을 가져와야 함. (리모트는 아직 안해봄)
    • .ttf, .woff 사용 가능 (.woff2는 안됨)
  6. 대략의 api는 아래와 같음.
    new ImageResponse(
      element: ReactElement,
      options: {
        width?: number = 1200
        height?: number = 630
        emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' = 'twemoji', // emoji render에 어떤 lib을 사용할 것인지
        fonts?: {
          name: string,
          data: ArrayBuffer, // 폰트 파일 데이터. fetch(URL).then(res => res.imageBuffer())로 가져오면 된다.
          weight: number,
          style: 'normal' | 'italic'
        }[]
        debug?: boolean = false // true 일 경우 각 element의 border, line-height 등이 표시됨. 
    
        status?: number = 200
        statusText?: string
        headers?: Record<string, string>
      },
    )
    new ImageResponse(
      element: ReactElement,
      options: {
        width?: number = 1200
        height?: number = 630
        emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' = 'twemoji', // emoji render에 어떤 lib을 사용할 것인지
        fonts?: {
          name: string,
          data: ArrayBuffer, // 폰트 파일 데이터. fetch(URL).then(res => res.imageBuffer())로 가져오면 된다.
          weight: number,
          style: 'normal' | 'italic'
        }[]
        debug?: boolean = false // true 일 경우 각 element의 border, line-height 등이 표시됨. 
    
        status?: number = 200
        statusText?: string
        headers?: Record<string, string>
      },
    )
  7. 대략의 사용례는 아래와 같음.
    // app/og/route.tsx
    import { ImageResponse } from 'next/server'; // app router 사용시 @verce/og가 포함되어 있음
    
    export const runtime = 'edge';
    
    const font = fetch(new URL('../path/to/font/Font.woff', import.meta.url)).then(
      (res) => res.arrayBuffer(),
    );
    
    export async function GET(request: Request) {
      const fontData = await font;
    
      // query param으로 이런 저런 텍스트를 동적으로 넣을 수 있음.
      const url = new URL(request.url)
      const searchParams = url.searchParams
      const title = searchParams.has("title") ? searchParams.get("title") : null
    
      return new ImageResponse(
        (
          <div
            style={{
            backgroundColor: 'white',
            height: '100%',
            width: '100%',
            fontSize: 100,
            fontFamily: '"Font"',
            paddingTop: '100px',
            paddingLeft: '50px',
          }}
         >
           {title ? title : 'Hello World!'}
         </div>
       ),
       {
         width: 1200,
         height: 630,
         fonts: [
           {
             name: 'Font',
             data: fontData,
             style: 'normal',
           },
         ],
       },
     );
    }
    // app/og/route.tsx
    import { ImageResponse } from 'next/server'; // app router 사용시 @verce/og가 포함되어 있음
    
    export const runtime = 'edge';
    
    const font = fetch(new URL('../path/to/font/Font.woff', import.meta.url)).then(
      (res) => res.arrayBuffer(),
    );
    
    export async function GET(request: Request) {
      const fontData = await font;
    
      // query param으로 이런 저런 텍스트를 동적으로 넣을 수 있음.
      const url = new URL(request.url)
      const searchParams = url.searchParams
      const title = searchParams.has("title") ? searchParams.get("title") : null
    
      return new ImageResponse(
        (
          <div
            style={{
            backgroundColor: 'white',
            height: '100%',
            width: '100%',
            fontSize: 100,
            fontFamily: '"Font"',
            paddingTop: '100px',
            paddingLeft: '50px',
          }}
         >
           {title ? title : 'Hello World!'}
         </div>
       ),
       {
         width: 1200,
         height: 630,
         fonts: [
           {
             name: 'Font',
             data: fontData,
             style: 'normal',
           },
         ],
       },
     );
    }
  8. tailwind 사용이 가능한데 아직 experimental이 붙어있고 className 말고 tw를 사용하도록 되어있음.
  9. Hobby plan일 경우 단일 function당 1MB 제한이 있어 한글 커스텀 폰트를 추가하긴 쉽지 않았음.
  10. sehyunchung.dev에 적용해본 결과 -> https://sehyunchung.dev/og?title=암온더넧렙을&description=절대적룰을지켜

2023. 5. 22.
Vercel Edge Function size limit

@vercel/og로 og image generation을 하는 김에 폰트도 맞춰보려고 일케절케 하다보니

Error: The Edge Function "api/og" size is X MB and your plan size limit is 1 MB. Learn More: https://vercel.link/edge-function-size

라면서 빌드가 터짐.

왜냐?

  1. og image generation에 커스텀 폰트를 사용하려면 .ttf 혹은 .woff (.woff2는 안됨)를 fetch로 불러와서 arrayBuffer로 만들어야 함
  2. 영어 폰트의 자수는 72자
  3. 근데 한글 폰트는 서브셋 처리를 해도 2,350자
  4. 그래서 한글 폰트를 일단 불러오면 1MB 안엔 안들어감 Screenshot 2023-05-22 at 23 57 33
  5. 돈을 더 내거나 더 줄인 서브셋을 만들어야 함
  6. 돈을 더 낼 순 없다 왜냐면 암것도 모르는 영어권 놈들(...)이 책정한 가격이니까
  7. 자수를 더 줄인 서브셋 만들기는 아직 방법도 모르므로 이후로 미룸
  8. 일단 한글 커스텀 폰트를 뺐더니
  9. 빌드에 성공함
  10. 영어!!!! ㅠㅠㅠ