저번 주 React Essentials에서 보았듯, 클라이언트 컴포넌트가 아닐 때, 리액트 라이프사이클을 적용하면 에러가 난다.
만들어 보고 싶은것은 위의 애니메이션을 framer motion으로 적용하는 레이아웃인데,
가장 간단한 방법은 위 모든 컴포넌트들을(상단 navBar, 왼쪽 오른쪽 fixed 메뉴 모두) use client로 만들어버리면 아무 문제가 없을 것 같다.
하지만, nextJS의 기본 개념과는 맞지 않다고 생각하므로:
client components는 반응성이 있거나, 리액트 라이프사이클을 사용하는 경우
server components는 반응성이 없고 고정된 밸류만을 표시하는 static한 경우
위 상술한 튜토리얼을 바탕으로 먼저 적용해 보았다.
// @/components/common/motion/index.ts
'use client';
import { motion as Motion } from 'framer-motion';
export default Motion;
// app/navbar/index.tsx
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { logo } from '@/components/common/icons';
import { montserrat } from '@/components/common/fonts';
import Motion from '@/components/common/framer/motion';
const Navbar = () => {
return (
<div className="w-full shadow-navbar-shadow h-20 lg:h-[12vh] sticky top-0 z-50 bg-body-color px-4">
<div
className={`max-w-screen h-full mx-auto py-1 flex items-center justify-between ${montserrat.className}`}
>
<Motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<Image className="w-14 bg-white" src={logo} alt="logo" />
</Motion.div>
</div>
</div>
);
};
export default Navbar;
왜인지는 모르겠지만 에러가 난다.
잠깐 고민을 한 뒤에 문제점을 깨달았다.
해당 라이브러리 framer-motion에서는 Compound Components Pattern 형식으로 컴포넌트를 임포트하기 때문에, 위 패턴에서 필요한 context는 모듈인 *.ts에서 프로바이더를 마운트 할 수 없기 떄문이다.
예전에 만들었던 아코디언 메뉴의 기억을 떠올려 본다면
// src/AccordionCompount.tsx
// 이렇게 위 패턴을 구현하려면 wrapper 자체가 tsx가 되어야 할 것이다. provider를 마운트해야 하기 때문에..
const AccordionCompound = ({ children, ...rest }: {children: React.ReactNode}) => {
return <AccordionProvider {...rest}>{children}</AccordionProvider>;
};
const AccordionItem = ({ children, ...rest }: {children: React.ReactNode}) => {
return <li className="w-full" {...rest}>{children}</li>;
};
AccordionCompound.Item = memo(AccordionItem);
AccordionCompound.Toggle = memo(AccordionToggle);
AccordionCompound.Collapse = memo(AccordionCollapse);
export default AccordionCompound;
// 그리고 사용해 줄 때는
<ul className="space-y-2">
<AccordionCompound>
<AccordionConsumer>
{({menus}) => {
return (
<AccordionCompound.Item>
</AccordionCompound.Item>
)
}}
</AccordionConsumer>
</AccordionProvider>
따라서 이 부분은 wrapper를 모듈이 아닌 컴포넌트의 형태로 만들어 줄 필요가 있다.
// 해서 바뀐 부분은 다음과 같다.
'use client';
import { motion, MotionProps } from 'framer-motion';
import React from 'react';
type MotionedDivProps = {
children: React.ReactNode;
className?: string;
} & MotionProps;
function MotionedDiv({ children, ...rest }: MotionedDivProps) {
return <motion.div {...rest}>{children}</motion.div>;
}
export default MotionedDiv;
const Navbar = () => {
return (
<div className="w-full shadow-navbar-shadow h-20 lg:h-[12vh] sticky top-0 z-50 bg-body-color px-4">
<div
className={`max-w-screen h-full mx-auto py-1 flex items-center justify-between ${montserrat.className}`}
>
<MotionedDiv
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<Image className="w-14 bg-white" src={logo} alt="logo" />
</MotionedDiv>
</div>
</div>
)
}
와 같은 방법으로 wrapping햇을 때, server 컴포넌트에서 framer-motion을 사용하는 client component를 임포트해 사용할 수 있었다.