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

[Next js/Approuter] google material icon 사용하기 본문

적어보자 에러 일지

[Next js/Approuter] google material icon 사용하기

yehey 2023. 12. 24. 15:32

Background

icon을 추가하면서 기존에는 필요한 svg 파일을 다운받아 사용했는데, 이번 프로젝트에서는 정해진 아이콘 없이 이것저것 사용해보고 잘 맞는 아이콘을 적용하고 싶었다. 그래서 google 에서 지원하는 google material icon 을 사용하기로 했다. 현재 프로젝트는 Next.js Approuter 를 사용하고 있다.

Contents

https://fonts.google.com/icons

 

Material Symbols and Icons - Google Fonts

Material Symbols are our newest icons consolidating over 2,500 glyphs in a single font file with a wide range of design variants.

fonts.google.com

구글에서 제공하는 개발자 가이드를 확인하면 간단하게 추가할 수 있다. app router 에서는 기본 파일이
app/layout.tsx, page.tsx 가 있는데 이 중에서 layout.tsx 에 적용해주면 된다.

Issue: next/head not working

layout.tsx 에 다음과 같이 next/head 를 추가하고 적용하면 아이콘이 적용되지 않는다.

import Head from 'next/head';

export default function RootLayout({ children }: IRootLayoutProps) {
  return (
    <html lang="kr">
      <Head>
        <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
      </Head>
      <body>
        <div id="portal" />
        ...
      </body>
    </html>
  );
}

이유는 Next js 문서에 나와있다. https://nextjs.org/docs/messages/no-stylesheets-in-head-component
Next에서 제공하는 Head 컴포넌트에는 stylesheet 속성이 들어갈 수 없다. Suspense / Streaming 과 같이 쓸 경우 깨질 가능성이 존재하기 때문에, 초기 SSR 응답에 포함되는 것이 보장되므로 클라이언트 측 렌더링 시까지 로드가 지연되기 때문에 성능 저하의 문제도 발생할 수 있다.
아무튼 그래서 <Head> 태그 안에 <link rel='stylesheet'> 는 들어갈 수 없다.

My Solution1 : next/head 말고 일반태그 사용하기

export default function RootLayout({ children }: IRootLayoutProps) {
  return (
    <html lang="kr">
      <head>
        <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
      </head>
      <body>
        <div id="portal" />
        ...
      </body>
    </html>
  );
}

link 가 잘 적용되어있다.

My Solution2: next 의 Metadata 사용하기

https://nextjs.org/docs/app/building-your-application/optimizing/metadata

 

Optimizing: Metadata | Next.js

Use the Metadata API to define metadata in any layout or page.

nextjs.org

Metadata를 사용하면 html태그 안에 들어갈 <link>,<meta>태그 등을 정의하는데 사용할 수 있다.

import type { Metadata } from 'next';


export const metadata: Metadata = {
  icons: {
    other: {
      rel: 'stylesheet',
      url: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined',
      precedence: 'default',
    },
  },
};

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

stylesheet 의 경우 precedence : 'default' 를 주지 않으면 적용이 되지 않는다.
(참조: https://stackoverflow.com/questions/74934561/how-to-add-a-css-file-to-a-pages-head-in-the-app-folder-of-next-js)


javascript 에서는 문제가 되지 않지만, typescript에서는 문제가 된다. Metadata 내부 타입에 precedence는 정의되어있지 않기 때문이다. next 의 Metadata 타입을 살짝 보면 다음과 같다.

//next/dist/lib/metadata/types/metadata-interface.d.ts
interface Metadata extends DeprecatedMetadataFields {
		...
		icons?: null | IconURL | Array<Icon> | Icons;
		...
}

//next/dist/lib/metadata/types/metadata-types.d.ts

export type Icons = {
    /** rel="icon" */
    icon?: Icon | Icon[];
    /** rel="shortcut icon" */
    shortcut?: Icon | Icon[];
    /**
     * @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
     * rel="apple-touch-icon"
     */
    apple?: Icon | Icon[];
    /** rel inferred from descriptor, defaults to "icon" */
    other?: IconDescriptor | IconDescriptor[];
};

export type IconDescriptor = {
    url: string | URL;
    type?: string;
    sizes?: string;
    color?: string;
    /** defaults to rel="icon" unless superseded by Icons map */
    rel?: string;
    media?: string;
    /**
     * @see https://developer.mozilla.org/docs/Web/API/HTMLImageElement/fetchPriority
     */
    fetchPriority?: 'high' | 'low' | 'auto';
};

 

수정이 필요한 부분은 IconDescriptor 다. 여기에 precedence 속성이 없기 때문에 IconDescriptor를 상속받는 새로운 interface를 생성했다. 

// types/custom.ts

import { IconDescriptor } from 'next/dist/lib/metadata/types/metadata-types';

export interface CustomIconDescriptorType extends IconDescriptor {
  precedence?: string;
}


// app/layout.tsx

const icon: CustomIconDescriptorType = {
  rel: 'stylesheet',
  url: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined',
  precedence: 'default',
};

export const metadata: Metadata = {
  icons: {
    other: icon,
  },
};

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

적용이 잘 된 걸 확인할 수 있다.

 

google material Icon 컴포넌트화 하기

import styled from 'styled-components';

interface IIconProps {
  iconName: string;
  size?: string;
}

function Icon({ iconName, size }: IIconProps) {
  return (
    <Wrapper>
      <Span className="material-symbols-outlined" size={size} color={color}>
        {iconName}
      </Span>
    </Wrapper>
  );
}

export default Icon;

const Wrapper = styled.div`
  display: flex;
`;

type SpanType = Pick<IIconProps, 'size' | 'color'>;

const Span = styled.span<SpanType>`
  font-size: ${(props) => props.size ?? '2rem'};
`;

 

iconName을 span에 넣어주면 google material icon 이 된다. 

컬러 수정이 필요한 경우는 폰트 색 수정처럼 color 속성을 Span에 주면 된다.

<Icon iconName='add' size='3rem'/>

 

사용할 때는 위와 같이 간단히 사용할 수 있다.


Error Review

precedence: 'default' 해결방법을 찾는 건 좀 오래 걸렸는데, 그래도 next 를 사용하는 만큼 next 가 제공하는 Metadata를 이용해서 Icon을 적용하고 싶었다. 그리고 해내서 기분은 좋지만 이게 과연 맞는 방법인지는 잘 모르겠다. 오히려 precedence 속성 없이 html <head> 태그를 사용하는게 더 낫다는 생각도 든다.

참조:

https://nextjs.org/docs/app/building-your-application/optimizing/metadata

https://developers.google.com/fonts/docs/material_symbols?hl=ko

https://stackoverflow.com/questions/74934561/how-to-add-a-css-file-to-a-pages-head-in-the-app-folder-of-next-js

Comments