yehey's 공부 노트 \n ο(=•ω<=)ρ⌒☆

[Next js] AppRouter 프로젝트에 styled-component 적용하기 본문

적어보자 에러 일지

[Next js] AppRouter 프로젝트에 styled-component 적용하기

yehey 2023. 12. 18. 09:50

Background

next js 를 app router 프로젝트로 시작했다. 그리고 styled-component로 스타일링 하는 것을 선호하기 때문에 설치 후 적용하면서 겪었던 모든 에러를 정리하고자... 쓴다.... (굉장히 다양하게 만남...^^)

Issue 1 : use client 에러

// app/layout.tsx
import styled, { ThemeProvider } from 'styled-components';
import { theme, GlobalStyle } from '@/styles';
import Header from '@/components/Header';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="kr">
      <body>
        <div id="portal" />
        <ThemeProvider theme={theme}>
          <GlobalStyle />
          <Header title="Home" />
          <Applayout>{children}</Applayout>
        </ThemeProvider>
      </body>
    </html>
  );
}
const Applayout = styled.div`
  display: flex;
  flex-direction: column;
  min-height: 100%;
  padding: 0 1.7rem;
  width: 100%;
  margin: 0 auto;
`;

레이아웃에 글로벌 스타일 적용하고, 커스텀 레이아웃을 사용하려고 하면

⨯ createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component

 

라는 에러가 계속 발생했다. 그냥 한마디로 'use client' 를 파일 최상단에 추가하라는 뜻인데,,, 이게 이해가 안갔던 이유는 아니 layout이 이해하기로는 최상단 껍데긴데.. 이 껍데기에 'use client'를 추가해서 클라이언트 컴포넌트 입니다. 라고 선언하면 그 아래도 클라이언트 컴포넌트가 되는 것이 아닌가.. 하면서 이건 해결방법이 아니라는 생각을 했다. 열심히 서치하고 박치기 한 결과, 서버 컴포넌트에서는 styled-component를 통해서 컴포넌트를 생성할 수 없다. 사용하려면 use client 를 명시해야한다. 그러면 layout.tsx는 어떻게 구성하냐!!!!

Solution

//utils/styledComponentsRegistry
'use client';

/* eslint-disable react/jsx-no-useless-fragment */

import React, { useEffect, useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import styled, { ServerStyleSheet, StyleSheetManager } from 'styled-components';
import { GlobalStyle } from '@/styles';

export default function StyledComponentsRegistry({ children }: { children: React.ReactNode }) {
  // Only create stylesheet once with lazy initial state
  // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
  const [isClient, setIsClient] = useState(false);

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  useEffect(() => {
    setIsClient(true);
  }, []);

  // if (typeof window !== 'undefined') return <>{children}</>;
  if (isClient) return <>{children}</>;

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      <GlobalStyle />
      <Applayout>{children}</Applayout>
    </StyleSheetManager>
  );
}

const Applayout = styled.div`
  display: flex;
  flex-direction: column;
  min-height: 100%;
  width: 100%;
  margin: 0 auto;
  height: 100%;
`;

//app/layout.tsx
import type { Viewport, Metadata } from 'next';
import StyledComponentsRegistry from '@/lib/utils/StyledSheetManager';
import ReactQueryProvider from '@/lib/utils/ReactQueryProvider';
import { CustomIconDescriptorType } from '@/types/custom';

interface IRootLayoutProps {
  children: React.ReactNode;
}

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1.0,
  maximumScale: 1,
  userScalable: false,
};

export default function RootLayout({ children }: IRootLayoutProps) {
  return (
    <html lang="kr">
      <body>
        <div id="portal" />
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  );
}

StyledComponentRegistry를 생성하고 layout.tsx에서는 그걸로 감싸주기만 하면 된다. 사실 이런다고 하위 컴포넌트는 use client가 안되는가? 에 대해 생각을 해봤는데, page.tsx 는 서버 컴포넌트더라 (styled-component 로 감쌌다가 같은 에러를 마주함..ㅎㅎ)

Issue 2 : 또..또..또!!! use client 에러

사실 이렇게 만들어둬도 styled-component만 쓰면 아주 에러를 뱉으면서 안해 안해를 외친다. 열받는다.
styled-comoponent를 써서만 error가 나는게 아니라, component에서 useState, useEffect 등등 리액트가 제공하는 무언가를 사용한 경우, use client 를 해야한다. 그럼 난 component 어떻게 불러오냐!! 라고 화를 낼 뻔 했지만...

우리의 친절한 next use client 를 어디에 쓰라고 힌트까지 주었다. 감사합니다.

Solution

간단하게 해결하자면 component 디렉토리의 index.ts 최상단에 use client를 명시하면 된다.
사실 그것보다 진짜 사용하는 곳에서만 use client를 명시하는게 좋을 것 같긴 하다. 그치만 해당 에러를 원천봉쇄하고 싶다면 index.ts 에 적는 것을 추천한다.

// components/index.ts
'use client';

export { default as LedgerItem } from './LedgerItem';
export { default as PostLedger } from './PostLedger';

 


Error Review

처음엔 영문도 모른채 use client를 붙여가며 에러를 해결했지만 오히려 서버 컴포넌트, 클라이언트 컴포넌트에 대해 열심히 찾아보고 왜 이렇게 써야하는가에 대해 알아갈 수 있었다. 지금 index.ts에 적용한 use client를 리팩토링 과정에서 꼭 필요한 곳에만 적용할 수 있도록 수정해야지
그리고 page나 layout에 styled-component로 컴포넌트를 그때 그때 생성하는 습관은 지양해야겠다.
Page에서는 이미 완성된 컴포넌트들을 잘 조합할 수 있도록 하고 새로운 컴포넌트 생성은 하지 않는 것으로....!

 

참조:

https://github.com/vercel/next.js/discussions/50473

https://nextjs.org/docs/messages/react-hydration-error

https://velog.io/@hyeonq/Next.js-Hydration-failed-error

Comments