NextJS13으로 새 포트폴리오를 만들자 3 - Next-themes 적용과 sequential animations

Sal Jeong·2023년 6월 28일
2

NextJS로 첫 사이드프로젝트를 하면서 Next-Themes가 정말로 편했었던 기억이 있는데, 역시 이번에서도 넣어보기로 했다.

Next-Themes 적용

문제는 Next-Themes는 프로바이더 기능을 사용하기 때문에, 이전에 확인했던 대로 layout.tsx(서버) -> provider.tsx(next-themes 래퍼client) -> children으로 추가적인 조치를 해 주어야 했다.

'use client';

import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import type { ThemeProviderProps } from 'next-themes/dist/types';

export default function NextThemeProvider({
  children,
  ...props
}: ThemeProviderProps) {
  const [mounted, setMounted] = React.useState(false);

  React.useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return <></>;
  } // for persistent theme page.

  return (
    <NextThemesProvider storageKey={'theme'} attribute="class" {...props}>
      {children}
    </NextThemesProvider>
  );
}

Tailwindcss로 다크모드 적용하기

위의 코드를 tailwindcss를 사용한 컴포넌트단에서는 이렇게 사용할 수 있다.

// '@/components/leftside/index.tsx
...
    <div className="text-text-light dark:text-text-grey w-full h-full flex flex-col items-center justify-end gap-4 ">
      <div className="flex flex-col gap-4">
        <Link href="https://github.com/jihyeonjeong11" target="_blank">
          <div className="bg-body-color-light dark:bg-hover-color hover:text-white dark:hover:text-text-green w-10 h-10 text-xl rounded-full inline-flex items-center justify-center cursor-pointer hover:-translate-y-2 transition-all duration-300">
            <span>
              <TbBrandGithub />
            </span>
          </div>
        </Link>
        <Link href="https://github.com/jihyeonjeong11" target="_blank">
          <span className="bg-body-color-light dark:bg-hover-color hover:text-white dark:hover:text-text-green w-10 h-10 text-xl rounded-full inline-flex items-center justify-center cursor-pointer hover:-translate-y-2 transition-all duration-300">
            <SlSocialLinkedin />
          </span>
        </Link>
        <Link href="https://github.com/jihyeonjeong11" target="_blank">
          <span className=" bg-body-color-light dark:bg-hover-color hover:text-white dark:hover:text-text-green w-10 h-10 text-xl rounded-full inline-flex items-center justify-center cursor-pointer hover:-translate-y-2 transition-all duration-300">
            <SlSocialFacebook />
          </span>
        </Link>
        <Link href="https://github.com/jihyeonjeong11" target="_blank">
          <span className="bg-body-color-light dark:bg-hover-color hover:text-white dark:hover:text-text-green w-10 h-10 text-xl rounded-full inline-flex items-center justify-center cursor-pointer hover:-translate-y-2 transition-all duration-300">
            <SlSocialInstagram />
          </span>
        </Link>
      </div>
      <div className="bg-text-light dark:bg-text-dark w-[2px] h-32 " />
    </div>
...

dark:를 입력한 클래스는 다크모드일때 (color-scheme: dark), 아니라면 그냥 클래스가 적용된다.

이러한 류의 라이브러리가 그렇듯, 이런식으로 작성하다보면 class 자체가 길어져서 가독성이 떨어지는 문제가 생길 수 있는데,

//@/globals.css

...

@layer components {
  .nav-button {
    @apply bg-body-color-light dark-bg-body-color-dark
  }
}
...

위와 같은 방식으로, tailwindCSS의 directives들을 class식으로 묶어서 사용하는 것도 가능하다.(여기서는 하지 않았지만,)

이러한 점에서 tailwind는 styled-components와 같은 부분보다 편의성이 훨씬 좋다는 생각이 든다. 다만 개발속도때문에 너무 tailwindcss를 많이 쓰다보니 원래 css를 잊어먹고있는 생각도 드는데, 다음 프로젝트에는 뭔가 더 css적인 무언가를 사용해야 할 듯 하다.

애니메이션을 넣기

두 가지의 애니메이션을 넣으려고 한다.

  1. 처음 나타날때 메뉴와 똑같이 opacity 0 에서 1로 진행하는애니메이션
  2. 테마를 스위칭할 때(버튼을 눌렀을 때) 아이콘이 변경되면서 회전하는 애니메이션

두 가지 상황에서 대응되는 애니메이션을 만들어야 하기 때문에
기존 만들어 둔 MotionDiv(motion.div)만으로는 불가능하다.

https://www.framer.com/motion/use-animate/

framer-motion 신버전에서는 useAnimate라는 훅을 react lifecycle 안으로 포함할 수 있도록 사용할 안내하고 있다.(원래는 useAnimation이라는 이름이었는데 바뀌었음)

사실 구현된 것은 sequence가 아니지만, 실제로 적용하지 않았을 뿐 이를 통해서 시퀀스를 구현할 수 있기 때문에 제목은 바꾸지 않도록 하자...

'use client';
import { useState, useEffect, useMemo } from 'react';
import { useTheme } from 'next-themes';
import { BsSun, BsMoon } from 'react-icons/bs'; // 사용할 아이콘
import { useAnimate } from 'framer-motion'; // 이 훅을 사용해서 구현한다.

import MotionedDiv from '@/components/common/framer/MotionedDiv';

const ThemeSwitcher = () => {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  const [scope, animate] = useAnimate(); // animate 함수로 자바스크립트로 애니메이션 구현이 가능하고 scope는 움직일 ref를 넣는다.

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

  const handleThemeChange = async (newTheme: 'dark' | 'light') => {
    setTheme(newTheme);
    await animate([
      newTheme === 'light'
        ? [scope.current, { rotate: -120 }]
        : [scope.current, { rotate: 0 }],
    ]); // 해 아이콘은 -120도, 달 아이콘은 0을 줘서 회전하는 느낌을 준다.( 해는 돌려도 똑같은 형태이기 때문에, dark모드일 경우 되돌아가게 만들어주면 회전하게 된다.)
  };

  return (
    <div className="w-10 h-10 flex items-center justify-center">
      {theme !== 'dark' ? (
        <MotionedDiv
          initial={{ opacity: 0, rotate: -120 }}
          animate={{ opacity: 1 }}
          transition={{ delay: 0.05 }}
          className="text-white hover:text-text-light dark:hover:text-text-green duration-300"
        >
          <button ref={scope} onClick={() => handleThemeChange('dark')}>
            <BsSun size="25" />
          </button>
        </MotionedDiv>
      ) : (
        <MotionedDiv
          initial={{ opacity: 0, rotate: 0 }}
          animate={{ opacity: 1 }}
          transition={{ delay: 0.05 }}
          className="text-white hover:text-text-light dark:hover:text-text-green duration-300"
        >
          <button ref={scope} onClick={() => handleThemeChange('light')}>
            <BsMoon size="25" />
          </button>
        </MotionedDiv>
      )}
    </div>
  );
};

export default ThemeSwitcher;

이상의 코드로 완성된 것은 다음과 같다.

아무래도 예전에 사용해본 animejs보다는 react 환경에서 코드 쓰기가 쉬웠다.

그런데 넥스트13 개발 환경이 너무 느린 것 같은데(현재 만든 코드가 1초정도 로딩까지 딜레이가 있음) 이것은 나중에 한번 찾아보아야 할 것 같다.

profile
Can an old dog learn new tricks?

0개의 댓글