들어가며
웹페이지를 어떻게 하면 인터렉티브하게 꾸밀 수 있을까 고민하면서 여러 페이지를 참고해봅니다. 그러다 토스 채용의 팀문화페이지를 보게 되었고 스크롤 애니메이션 효과를 따라해보게 되었습니다. 토스는 react-framer라이브러리를 쓴다고 들어서 동일한 라이브러리를 활용하여 학습하고, 유사하게 꾸며 보았습니다.
https://toss.im/career/culture
토스 채용
지금 바로, 토스커뮤니티에 합류하세요.
toss.im
Framer-motion이란?
framer-motion은 은 React를 위한 모션 라이브러리로 react에서 모션을 사용할 때는 아래의 명령어를 통해 설치하여 사용할 수 있습니다
npm i framer-motion
motion라이브러리는 그들 자신을 하이브리드 엔진을 탑재한 유일한 라이브러리라고 소개합니다.
motion 컴퍼넌트를 통해서 간단하게 애니메이션 효과를 구현할 수 있습니다.
import { motion } from "framer-motion";
<motion.div animate={{ opacity: 1 }} />
라이브러리는 다양한 애니메이션 효과와 훅을 지원하고 있어서, 이는 추후에 정리해보고 우선 토스 페이지 구현에 필요한 기능들 위주로 언급하려고 합니다.
구현할 내용(토스채용 팀문화 페이지는 어떻게 구현되어 있는가?)

우선 "당신은 깊게 ~~~" 문구가 나타나면서 사진이 보여집니다. 이후 스크롤을 하면 사진이 확대되면서 사진의 명도가 바뀌 내용이 변경됩니다. 그러면서 투명한 글자가 한문구씩 위로 올라가면서 나타납니다.

이후에 스크롤을 더하면 사진과 글자들이 투명하게 사라지면서, 다른 내용들이 나타납니다.

즉 요약하면 크게 5가지 기능으로 보입니다.
1. 이미지와 관련 컴퍼넌트를 띄우기
2. 이후 스크롤을 끝까지 하면 이미지와 관련된 컴퍼넌트가 다 투명하게 사라지기
3. 스크롤을 통해서 사진 확대 제어 및 사진 명도제어
4. 스크롤을 통해서 글자 나타나고 사라지게 하기
5. 글자가 나타나고 사라질 때 애니메이션 효과주기
구현하기

1. 이미지와 관련된 컴퍼넌트를 먼저 띄울려고 합니다.
그전에 간단하게 레이아웃만 잡았습니다.

위 이미지를 보면 투명해지고 나서 위에 여백이 있는 것을 확인할 수 있습니다. 따라서 특정 위치까지는 투명한 높이를 가진 컴퍼넌트가 존재함을 알 수 있습니다. 따라서 레이아웃은 아래와 같이 구성하였습니다.
import AnotherText from "./components/AnotherText";
import Block from "./components/Block";
import FloatingCard from "./components/FloatingCard";
const Tosslike = () => {
return (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Block height={"5000px"} />
<FloatingCard />
<AnotherText />
</div>
);
};
export default Tosslike;
각각의 컴퍼넌트에 대한 설명은 아래와 같습니다
style이 적용된 div : 간단한 정렬 레이아웃만 적용하였습니다.
Block : 높이를 잡아줄 컴퍼넌트. props로 높이를 받아서 컴퍼넌트의 최종 높이를 결정합니다.
FloatingCard : 이미지와 텍스트등 띄워진 컴퍼넌트입니다. 스크롤 애니메이션이 적용될 부분들입니다.
AnotherText : 이미지 관련 컴퍼넌트들이 투명해지고 나서 보여질 내용들입니다. 임의의 간단한 텍스트만 적용했습니다.
이제 저희가 중점을 둘 FloatingCard 컴퍼넌트에 집중하겠습니다.
기본적으로 특정 높이까지 화면에 띄워진 css를 적용하기 위해서 position : fixed 옵션을 적용했습니다.
import { motion, useScroll, useTransform } from "framer-motion";
const FloatingCard = () => {
const { scrollY } = useScroll();
const opacity = useTransform(scrollY, [3500, 4300], [0.7, 0]);
return (
<motion.div
style={{
width: "100%",
height: "100vh",
opacity,
position: "fixed",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
zIndex: 2,
pointerEvents: "none",
overflow: "hidden",
}}
>
<다른 컴퍼넌트들/>
</motion.div>
);
};
export default FloatingCard;
이때 pointerEvents를 none으로 준 이유는 이미지가 상하로 100%의 크기를 차지하기 때문에 하위에 있는 컴퍼넌트들과 상호작용을 할 수 없어집니다.
따라서 "스크롤 이후 컴퍼넌트를 언마운트 한다" 혹은 "애초에 클릭 이벤트가 먹지 않는다" 중에서 저는 후자를 선택하였습니다.
실제로 토스에서도 이미지 관련 컴퍼넌트들이 클릭되지 않는 것을 보면 동일한 옵션일 것이라고 생각합니다.
2. 스크롤을 끝까지 하면 이미지와 관련된 컴퍼넌트가 다 투명하게 사라지기
위의 코드에서 framer-motino 관련된 훅이 보입니다.
useScroll : 스크롤 관련된 정보를 받을 수 있습니다. 스크롤 진행사항, 스크롤 위치. scrollY 는 세로방향 스크롤 높이를 반환해줍니다.
useTransform : 하나 이상의 motion value의 값을 변환하여 새로운 motion value를 생성합니다.
const opacity = useTransform(scrollY, [3500, 4300], [0.7, 0]);
이를 활용하여서 y축 스크롤 높이를 활용하여서 scrollY값이 2000에서 34000일 때 동일한 비율로 0.7~0으로 변환하는 opacity라는 값을 만듭니다.
이를 통해서 스크롤이 2000px에서 3400px로 이동하면 이미지관련 컴퍼넌트들을 투명하게 만들어줍니다.
3. 스크롤을 통해서 사진 확대 제어 및 사진 명도제어
이제 이미지와 관련된 컴퍼넌트를 만들겠습니다.
import { motion, useScroll, useTransform } from "framer-motion";
import FloatingCardText from "./FloatingCardText";
import FloatingImg from "./FloatingImg";
const FloatingCard = () => {
const { scrollY } = useScroll();
const opacity = useTransform(scrollY, [3500, 4300], [0.7, 0]);
return (
<motion.div
style={{
width: "100%",
height: "100vh",
opacity,
position: "fixed",
top: "0px",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
zIndex: 2,
pointerEvents: "none",
overflow: "hidden",
}}
>
<FloatingImg scrollY={scrollY} />
<motion.div
style={{
zIndex: 3,
color: "red",
fontSize: "4rem",
textAlign: "center",
}}
>
<FloatingCardText />
</motion.div>
</motion.div>
);
};
export default FloatingCard;
FloatingCard에는 사라지고 나타나는 Text 컴퍼넌트까지 미리 적용시켜놓겠습니다.
import { motion, useTransform, MotionValue } from "framer-motion";
import React from "react";
interface FloatingImgProps {
scrollY: MotionValue<number>;
}
const FloatingImg: React.FC<FloatingImgProps> = ({ scrollY }) => {
const scale = useTransform(scrollY, [400, 2000], [1, 1.3]);
const blackOpacity = useTransform(scrollY, [0, 1500], [0.2, 0.6]);
return (
<>
<motion.img
src="/partenon.jpg"
alt="Floating img"
width="100%"
height="100%"
style={{ scale, position: "absolute", top: "0px" }}
/>
<motion.div
style={{
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "black",
opacity: blackOpacity,
}}
/>
</>
);
};
export default FloatingImg;
이미지가 일정 시점부터 확대되기 때문에 scale이라는 변수를 useTransform 훅으로 만들었습니다.
그리고 toss페이지에서는 이미지 자체가 진해지는 게 아니라, 앞에 가림막의 투명도가 조절되는 느낌이라 blackOpacity라는 변수를 만들어서 검은색 배경의 막의 투명도를 조절하였습니다.
4. 스크롤을 통해서 글자 나타나고 사라지게 하기
이제 처음에 나오는 메인 텍스트를 사라지게 하는 효과를 주겠습니다.
import { useScroll, useTransform, motion } from "framer-motion";
import FloatingLineText from "./FloatingLineText";
const FloatingCardText = () => {
const { scrollY } = useScroll();
const mainTextOpacity = useTransform(scrollY, [1200, 1300], [1, 0]);
const floatingTexts = [
{ start: 1400, text: "첫 번째 줄이 올라옵니다" },
{ start: 2100, text: "두 번째 줄이 올라옵니다" },
{ start: 2700, text: "세 번째 줄이 올라옵니다" },
];
return (
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
textAlign: "center",
color: "white",
fontSize: "2rem",
zIndex: 3,
}}
>
<motion.div style={{ opacity: mainTextOpacity }}>
당신도 깊게 몰입했던 <br /> 무언가가 있나요?
</motion.div>
{floatingTexts.map(({ start, text }) => (
<FloatingLineText
key={start}
scrollY={scrollY}
start={start}
text={text}
/>
))}
</div>
);
};
export default FloatingCardText;
텍스트들이 화면 중앙에 위치하도록 absolute를 통해서 위치를 잡았습니다.
그리고 마찬가지로 1200~1300 위치에서 사라지도록 opacity를 조절하였습니다.
이때 사라지고 나타나는 효과를 animate 훅을 통해서도 줄 수 있습니다. 다만 토스의 경우에는 스크롤 위치 기반으로 구현하였기에 해당 스타일을 주었고, animate효과로 적용할 경우 스크롤이 빠르면 글자들이 겹치는 문제가 발생합니다.
5. 글자가 나타나고 사라질 때 애니메이션 효과주기
import { motion, useTransform, MotionValue } from "framer-motion";
interface FloatingLineTextProps {
scrollY: MotionValue<number>;
start: number;
text: string;
}
const FloatingLineText = ({ scrollY, start, text }: FloatingLineTextProps) => {
const opacity = useTransform(scrollY, [start, start + 100], [0, 1]);
const y = useTransform(scrollY, [start, start + 100], [30, 0]);
return (
<motion.div style={{ opacity, y, marginTop: "1.5rem" }}>{text}</motion.div>
);
};
export default FloatingLineText;
마지막으로 다른 줄들은 map을 통해 스크롤 위치와 작성할 text를 props로 받도록 하였습니다.
그리고 y는 motion에서 translateY 역할을 하는 style 속성입니다. 이를 통해서 나타날 떄는 아래에서 위로 올라오도록, 사라질 때는 내려가도록 하였습니다.
참고 페이지 및 문서
https://toss.im/career/culture
토스 채용
지금 바로, 토스커뮤니티에 합류하세요.
toss.im
https://www.npmjs.com/package/framer-motion
framer-motion
A simple and powerful JavaScript animation library. Latest version: 12.6.5, last published: a day ago. Start using framer-motion in your project by running `npm i framer-motion`. There are 6000 other projects in the npm registry using framer-motion.
www.npmjs.com
'프론트엔드 > React' 카테고리의 다른 글
WebRTC 기술 구현하기 (0) | 2024.12.11 |
---|---|
간소화된 SPA 구현하기 (0) | 2024.11.17 |
URL 기반 검색 기능 만들기 (URLSearchParams, Router) (1) | 2024.10.26 |
Intersection Observer API를 활용하여 무한 스크롤 구현하기 (0) | 2024.10.07 |
input 요소 uncontrolled와 controlled 관련 Warning (0) | 2024.05.01 |