728x90

 

들어가며

웹페이지를 어떻게 하면 인터렉티브하게 꾸밀 수 있을까 고민하면서 여러 페이지를 참고해봅니다. 그러다 토스 채용의 팀문화페이지를 보게 되었고 스크롤 애니메이션 효과를 따라해보게 되었습니다. 토스는 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

 

728x90

오랜만에, deep dive 교재를 다시 읽다가 단순히 규칙처럼 사용했던 불변성에 대해서 왜 그랬을까? 라는 고민을 하게 되었고 작성하게 되었습니다.

 

자바스크립트 객체의 특징

우선 객체에 대해서 먼저 알 필요가 있습니다.

JS에서 객체는 JAVA나 C++와 같은 클래스 기반 객체지향 언어와 다르게, 동적으로 프로퍼티를 삭제 및 추가할 수 있다. 

그렇기 때문에 객체의 경우에 정확히 얼마만큼의 메모리 공간을 확보해야 할지 정할 수 없습니다. 

또한 원시값들을 처리하는 것 처럼 객체가 수정될 때마다 새로운 메모리공간에 수정된 객체를 추가하는 작업은 메모리의 효율이 떨어지고 성능을 저하시킨다.

 

따라서 자바스크립트에서는 해당 객체의 직접적인  주소를 변수에 할당하는 것이 아니라, 해당 객체가 위치하는 메모리 주소를 할당한다. 이를 원시값에서 주소를 직접적으로 할당하는 것과 구별짓기 위해서 교재에서는 ' 공유 에 의한 전달'(참조에 의한 전달)이 라고 한다.

(공유에 의한 전달이 좀 더 정확한 표현이라 느끼나 참조에 의한 전달이 보편적으로 사용되는 느낌입니다.)

deep dive 교재 151pg 참고

즉, 동적인 객체를 효율적으로 관리하기 위해서 위와 같은 공유에 의한 전달 방법을 사용하지만, 그러다 보니 여러 객체가 동일한 하나의 객체를 공유하는 문제도 발생하게 된다. 하지만 객체를 하나하나 프토퍼티 값까지 확인하는 것이 아니라 주소 값을 확인하기 때문에 효율적으로 관리할 수 있게 된다.

 

리액트에서 불변성이 중요한 이유

리액트도 결국 자바스크립트 기반의 라이브러리입니다. 리액트에서는 업데이트 전 후의 두 가지의 가상 돔을 diffing 알고리즘으로 비교하여 변경된 부분만 리렌더링합니다. 따라서 변화를 감지하기 위해서 불변 객체를  사용하여 참조(공유)하고 있는 값이 다르다면 객체가 변했다고 인지하도록 합니다. 

이때 원시값들처럼 객체를 변경할 때 객체 자체를 변경하는 것이 아니라 참조(공유)되고 있는 주소 값이 다르게 하는 것을 불변성을 지킨다고 합니다.


React will ignore your update if the next state is equal to the previous state, as determined by an Object.is comparison. This usually happens when you change an object or an array in state directly:
// 출처 : https://react.dev/reference/react/useState

https://ko.legacy.reactjs.org/tutorial/tutorial.html#why-immutability-is-important

 

 

그리고 불변성을 지키기 위해선 spread문법을 활용하여 새로운 객체를 만드는 것을 공식문서에서는 추천합니다.

setObj({
  ...obj,
  x: 10
});

 

 

참고문헌 

자바스크립트 deepdive 교재

https://react.dev/reference/react/useState

 

useState – React

The library for web and native user interfaces

react.dev

https://legacy.reactjs.org/docs/reconciliation.html?

 

Reconciliation – React

A JavaScript library for building user interfaces

legacy.reactjs.org

 

+ Recent posts