NextJS로 첫 사이드프로젝트를 하면서 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를 사용한 컴포넌트단에서는 이렇게 사용할 수 있다.
// '@/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적인 무언가를 사용해야 할 듯 하다.
두 가지의 애니메이션을 넣으려고 한다.
두 가지 상황에서 대응되는 애니메이션을 만들어야 하기 때문에
기존 만들어 둔 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초정도 로딩까지 딜레이가 있음) 이것은 나중에 한번 찾아보아야 할 것 같다.