notes

#react

Callback Ref2023. 6. 26.

callback refs?

ref는 보통 useRef hook과 함께 쓰지만 ref 에 함수를 넘길 수도 있는데, 요런 패턴을 callback refs 라고 하고, 대부분 DOM node를 액세스 하는 경우에 쓰인다.

How?

놀랍게도 ref엔 함수를 넘길 수 있는데, 요 경우 아래와 같이 node를 arg로 받게 된다.

const ref = (node) => {
  // access the dom node
}
const ref = (node) => {
  // access the dom node
}

사실 useRef가 넘기는 ref 도 걍 아래와 같다.

<div
  ref={(node) => {
    ref.current = node;
  }}
/>
<div
  ref={(node) => {
    ref.current = node;
  }}
/>

But why?

기존의 ref 는 보통 effect 내부에서 마운트가 된 이후에 액세스하게 된다. 근데 해당 dom node에 조건부 렌더가 걸려있다면 동작하지 않는 케이스가 생길 수 있고, node가 다이나믹하게 변경될 수 있는 케이스에 대응이 어렵다거나 등등.

When?

아래와 같은 걸 할 수 있다.

const ref = (node) => {
  node?.focus()
}

// ...

return <input ref={ref} type="text" />
const ref = (node) => {
  node?.focus()
}

// ...

return <input ref={ref} type="text" />

useCallback

근데 위와 같이 하면 매 렌더시마다 포커스가 될 것이므로, 거의 대부분의 경우 useCallback이랑 쓰게 될 것이다.

const ref = useCallback(() => {
  node?.focus()
}, [])
const ref = useCallback(() => {
  node?.focus()
}, [])

Further Readings

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

요기서 이것 저것 해봤는데 다 별로인 것 같다. 왜냐면 다 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가 안됨

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

이런 저런 이유로 클라이언트 상태를 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 한정이지만요...

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

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

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

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

  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=절대적룰을지켜

isReactElement2021. 3. 19.

const isReactElement = (element: ReactNode): element is ReactElement =>
  element !== null && typeof element === 'object' && 'props' in element;
const isReactElement = (element: ReactNode): element is ReactElement =>
  element !== null && typeof element === 'object' && 'props' in element;

`Component.displayName`2020. 11. 13.

forwardRef로 wrap한 컴포넌트의 경우 devtool에서 컴포넌트명이 뜨지 않는다. 그럴땐:

Component.displayName = 'Component'
Component.displayName = 'Component'

로 일단 해결할 수 있다.

React.note vs useMemo2020. 7. 10.

React.memo

  • Higher order component.
  • prop change만을 체크함. 감싼 컴포넌트 안에 useState, useContext등이 있다면, 역시 해당 state 변경에 따라 rerender됨.

useMemo

  • Hook.
  • deps array에 포함된 deps가 변경되지 않으면 memoized value를 반환하는 함수.

Tags