Skip to content

Conversation

@seueooo
Copy link
Contributor

@seueooo seueooo commented Mar 11, 2025

🔥 Related Issues

✅ 작업 리스트

  • DetailSection 애니메이션 구현

🔧 작업 내용

사용자가 스크롤하면서 콘텐츠를 읽어 내려가면 그에 맞는 이미지가 우측에 표시되도록 하는 애니메이션을 framer-motion을 통해 구현했습니다.
typed 사이트의 애니메이션 참고함

  • 사용자의 scroll 위치를 기반으로 뷰포트 중앙에 위치한 TextBlock을 감지하여 activeSection 상태에 저장하고, 이 상태값에 따라 MediaBlock에서 해당 이미지 표시 -> 해당 로직은 useActiveSection 커스텀훅으로 분리
    모바일에서는 TextBlock 아래에 이미지를 직접 배치하고 MediaBlock은 숨김 처리하도록 하여 반응형 구현
    - TextBlock과 MediaBlock을 각각 mapping하도록 수정
    -> 이유: 텍스트와 미디어가 완전히 다른 그리드 영역에 배치되어 있어서 분리된 두 개의 컨테이너에서 각각 매핑해야 함, 모바일에서는 텍스트 아래에 이미지가 바로 나타나고, 데스크톱에서는 오른쪽에 고정된 영역에 이미지가 표시, activeSection 상태에 따라 데스크톱의 이미지만 활성/비활성이 전환되어야 하므로... 좀 지저분해 보이는데 레이아웃 구조상 어쩔 수 없었습니당,,

배열로 관리하던 텍스트데이터로 매핑해주니까 알아보기 쉽지 않은 코드가 된 것 같아 DetailContent 컴포넌트 구조를 아래와 같이 변경했습니다!
- TextBlock을 없애고 DetailContent 컴포넌트를 생성하여 텍스트와 모바일용 이미지를 같이 두었습니다.
- 텍스트 데이터로 한꺼번에 매핑하는 방식에서, 각 섹션을 key로 두고 참조하도록 변경했습니다. 코드 중복이 좀 있으나 가독성 면에선 더 낫다고 판단했어요!

const DetailSection = ({ detailContents }: { detailContents: DetailContents }) => {
  const activeSection = useActiveSection();
  return (
    <div className="flex w-full items-center justify-center">
      <div className="lg:grid lg:grid-cols-12">
        {/* 왼쪽 텍스트 & 모바일 이미지 */}
        <div className="flex flex-col gap-[4.8rem] lg:col-span-6">
          <DetailContent sectionDetails={detailContents.section1} />
          <DetailContent sectionDetails={detailContents.section2} />
          <DetailContent sectionDetails={detailContents.section3} />
          <DetailContent sectionDetails={detailContents.section4} />
        </div>

        {/* 오른쪽 데스크탑용 이미지 */}
        <div className="right-0 top-[7.7rem] col-span-6 col-start-7 w-full lg:sticky lg:h-[calc(100dvh-7.7rem)]">
          <div className="relative h-full w-full">
            <MediaBlock
              sectionDetails={detailContents.section1}
              isActive={detailContents.section1.dataOrder === activeSection}
            />
            <MediaBlock
              sectionDetails={detailContents.section2}
              isActive={detailContents.section2.dataOrder === activeSection}
            />
            <MediaBlock
              sectionDetails={detailContents.section3}
              isActive={detailContents.section3.dataOrder === activeSection}
            />
            <MediaBlock
              sectionDetails={detailContents.section4}
              isActive={detailContents.section4.dataOrder === activeSection}
            />
          </div>
        </div>
      </div>
    </div>
  );
};
export const DETAIL_CONTENTS = {
  section1: {
    dataOrder: 1,
    title: '몰입에 필요한\n서비스를 한 곳에서',
    description:
      '작업에 필요한 서비스만 등록하세요.\n당신의 업무에 맞는 분야별 서비스 추천으로\n더 효율적인 몰입 환경을 만들 수 있어요.',
    imgSrc: '/section1.gif',
    imgDescription: '허용 서비스',
  },
  section2: {
    dataOrder: 2,
    title: '오늘의 할 일을\n체계적으로',
    description:
      '할 일을 등록하고 몰입 시간을 확인하세요.\n한눈에 보는 나의 할 일 현황으로\n오늘 해야 할 일을 놓치지 않고 관리할 수 있어요.',
    imgSrc: '/section2.gif',
    imgDescription: '허용 서비스',
  },
...

제안사항 있으시면 리뷰 부탁드려요.

🧐 새로 알게된 점

  • IntersectionObserver api라는 걸 알게됐는데, 그걸 사용하면 현재 컴포넌트 구조에서는 여러 요소를 매핑하고있어 ref 값을 배열로 관리해야 해서 코드가 복잡해질 수 있다고 판단하여 좀 더 간단하다고 생각한 스크롤 기반 로직으로 구현했어요.
  • 스크롤 하는데 오른쪽 이미지가 자꾸 고정이 안돼서 애를 먹었는데, 그리드 없이 우측 섹션을 배치하면 텍스트 콘텐츠의 너비에 따라 위치가 변동될 수 있어서 그리드 사용해서 우측 섹션이 항상 정확한 위치(7번째 컬럼부터 시작해 6개 컬럼을 차지)에 고정되도록 했습니다.

🤔 궁금한 점

  • 375px, 1920px 뷰 두가지 뿐이라 그 중간지점이 많이 어색한거 같아서 논의가 필요한 것 같아요..!

📸 스크린샷 / GIF / Link

  • 데스크탑
2025-03-11.8.02.07.mov
  • 모바일
2025-03-11.8.03.21.mov

@seueooo seueooo self-assigned this Mar 11, 2025
@seueooo seueooo linked an issue Mar 11, 2025 that may be closed by this pull request
1 task
Copy link
Member

@10tacion 10tacion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 어려운 작업이군요.. 고생많으셨습니다!
data-order를 설정해서 텍스트가 뷰포트안에 들어올 때만 해당 이미지의 opacity를 1로 설정해서 보이게끔 하셨군요 굿굿!

detail section에서 grid-cols-12 를 설정한 후 자식 요소에게 col-span을 6만큼 설정해주셨는데요
제 생각에는 컬럼을 2개로 나누고 각각 차지하게끔 하면되지 않을까 싶은데, 12개 컬럼으로 나눈 다른 이유가 있을지 궁금해요!

let isActiveSection = false;

sections.forEach((section) => {
const rect = section.getBoundingClientRect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBoundingClientReact라는 메서드를 처음봤네요... 뷰포트의 상대적인 위치를 계산한다고해요
rect는 TextBlock들이 반환하는 위치의 배열인가요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBoundingClientRect()는 특정 요소의 현재 뷰포트를 기준으로 한 위치와 크기 정보를 가져오는 메서드에요
여기서 rect는 TextBlock 요소들이 뷰포트에서 차지하는 위치와 크기를 담고 있는 객체라고 할수있습니다

rect.top → 요소의 상단이 뷰포트의 최상단으로부터 얼마나 떨어져 있는지
rect.height → 요소의 높이

등등

이 커스텀 훅은 사용자가 스크롤을 내릴 때, 현재 화면에서 중앙에 위치한 섹션(TextBlock)을 감지하여 해당 섹션의 data-order 값을 activeSection으로 업데이트하는 역할을 하고있어요. 즉, getBoundingClientRect()를 이용해 각 섹션(TextBlock)이 화면에서 어디에 위치하는지 계산하고, 화면 중앙에 걸친 섹션을 찾을 때 사용하고 있답니다

const sectionBottom = sectionTop + rect.height;

if (viewportMiddle >= sectionTop && viewportMiddle <= sectionBottom) {
const index = parseInt(section.getAttribute('data-order') || '0', 10);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseInt 2번째 인자에 10을 작성하지 않으면 값이 10진수가 아니게 될수도 있나요?! 단순 궁금증

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

명시해주는게 좋다곤 합니다!!

</div>
))}
</div>
<div className="right-0 top-[7.7rem] col-span-6 col-start-7 w-full md:sticky md:h-[calc(100dvh-7.7rem)]">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

md일 때 sticky와 height를 따로 설정해주셨는데 어떤 역할을 하는지 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

md는 제가 임의로 자연스러워보이는 변경 포인트로 설정한 기준점이고, 화면 크기가 md 이상일 때 우측 이미지를 sticky로 고정하고 height을 calc(100dvh-7.7rem)로 설정해서 뷰포트에서 헤더 높이(7.7rem)를 제외한 나머지 부분을 꽉 채우도록 설정한 것입니다.
sticky가 적용된 상태에서 스크롤을 내려도, 화면 높이만큼 영역을 유지하며 이미지가 교체될 수 있도록 한 것이에여

화면 크기가 md 이하일 때는 텍스트와 gif가 세로로 교차되어 나열되도록 해서 sticky나 height을 지정하지 않도록 한것입니다.

package.json Outdated
Comment on lines 13 to 14
"framer-motion": "^12.4.10",
"motion": "^12.4.7",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 framer-motion의 최신 버전에서 이름이 motion으로 변경된줄 알았는데, 다른 패키지가 존재했군요
image
두 패키지의 차이점이 있는지 궁금해요!

Copy link
Contributor Author

@seueooo seueooo Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗.. import { motion } from 'framer-motion'하려니까 에러떠서 안깔려있는줄 알고 그냥 framer-motion으로 설치했는데 다른 패키지였군요!!
저거 찾아보니까 프레이머에서 framer-motion의 새롭게 출시한 후속 패키지처럼 보여요. 삭제 후 motion으로 통일해놓겠습니다..!!

Copy link
Contributor

@Ivoryeee Ivoryeee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프레이머 사용에 익숙치 않으셨을 텐데도 뚝딱뚝딱 잘 구현해 주셨네요 작업하느라 수고 많으셨습니다!

<MediaBlock imgSrc={contents.imgSrc} imgDescription={contents.imgDescription} />
</section>
<div className="flex w-full items-center justify-center">
<div className="w-full md:grid md:grid-cols-12">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저번에 반응형 고려하실 때 대부분의 단위를 sm으로 잡으셨던 걸로 기억하는데, md로 변경하신 이유가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sm으로 하면 변경 지점의 간격이 너무 커서, 좀 더 자연스러운 변경 포인트로 제가 임의로 설정했습니다!

alt={content.imgDescription}
width={1073}
height={789}
className="rounded-[2rem]"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 모바일용 이미지도 width가 1073으로 들어가나요? 피그마에서 확인해 보니 모바일용 미디어는 327x241 인 것 같던데, 데스크탑과 동일한 크기로 넣으신 이유가 있는지 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

데스크탑용 크기를 넣어도 창 크기가 줄어들면 비율에 맞게 자연스럽게 축소되는데, 모바일용 크기를 지정해버리니까 창을 조금만 줄여도 바로 모바일 사이즈로 확 바뀌어서요,,,!

Copy link
Member

@KIMGEONHWI KIMGEONHWI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쉽지 않은 작업이었을 것 같은데 고생많으셨습니다!!

const sectionTop = window.scrollY + rect.top;
const sectionBottom = sectionTop + rect.height;

if (viewportMiddle >= sectionTop && viewportMiddle <= sectionBottom) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 sectionTop = window.scrollY + rect.top;const sectionBottom = sectionTop + rect.height;으로 sectionTop과 sectionBottom을 정의해서 viewportMiddle과 비교연산을 거치도록 로직이 되어있습니다. 그러나, const viewportMiddle = window.scrollY + window.innerHeight / 2;const viewportMiddle = window.innerHeight / 2;와 같이 getBoundingClientRect()는 뷰포트 기준 좌표를 반환하기 때문에 window.innerHeight / 2를 그대로 사용하고, if (rect.top <= viewportMiddle && rect.bottom >= viewportMiddle)으로 if문을 수정해서 사용하면 코드가 더 간소화 될 것 같은데 window.scrollY를 더해준 이유가 있을까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 코드에서 window.scrollY를 더한 이유는 스크롤 위치를 반영해서, 요소가 페이지 전체에서 어느 정도 위치해 있는지를 계산하려고 한 것입니다. 하지만 viewportMiddle도 뷰포트 기준으로 계산된 값이라, 같은 기준에서 비교하려면 window.scrollY를 더할 필요가 없을 수도 있겠네요 한번 시도해보겠습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

참고로, 제가 테스트할 때는 문제 없었습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

덕분에 코드가 훨씬 간소화됐네요! 반영했습니다~!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KIMGEONHWI 건휘님 최고네요. 코드 이해가 훨신 더 쉬워졌어요 🚀🚀🚀

@seueooo
Copy link
Contributor Author

seueooo commented Mar 12, 2025

detail section에서 grid-cols-12 를 설정한 후 자식 요소에게 col-span을 6만큼 설정해주셨는데요
제 생각에는 컬럼을 2개로 나누고 각각 차지하게끔 하면되지 않을까 싶은데, 12개 컬럼으로 나눈 다른 이유가 있을지 궁금해요!

음 그래두될것 같긴한데 지금 방식을 유지하는게 더 정밀하게 레이아웃을 컨트롤할 수 있을 것 같습니다. 사실 flex로 정렬했을때 도저히 옆에 고정이 안되길래 typed 사이트 구현 방식 따라해보느라 12로 설정해본거긴 해요,,ㅋㅋ

@KIMGEONHWI KIMGEONHWI self-requested a review March 20, 2025 19:23
Copy link
Member

@KIMGEONHWI KIMGEONHWI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM~

Copy link
Contributor

@Ivoryeee Ivoryeee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 수고하셨어요!

Copy link
Member

@10tacion 10tacion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 수고스러운 작업 감사드립니다 🙇‍♂️🙇‍♂️

@seueooo seueooo merged commit 7b9972d into main Mar 23, 2025
1 check passed
@seueooo seueooo deleted the feat/add-full-animation/#2 branch March 23, 2025 11:15
Ivoryeee added a commit to Ivoryeee/Morib-Landing that referenced this pull request Mar 23, 2025
* style: detailSection 정렬 수정

* feat: framer-motion을 통한 DetailSection animation 구현

* feat: home의 'use client' 구문 제거

* style: 모바일 반응형 구현

* refactor: useActiveSection 훅 분리

* style: 짜잘한 스타일 수정

* style: 스타일 수정 및 필요없는 코드 삭제

* chore: framer-motion 패키지 삭제 후 motion/react로 통일

* refactor: 랜딩 1차 큐에이 반영

* style: 미세한 레이아웃 조정, 여백값 변경 등 피그마에 맞춰서 수정

* style: 반응형 중단점 lg로 수정

* feat: DetailSection 컴포넌트 구조 변경

- TextBlock 컴포넌트 삭제, DetailContents 컴포넌트 생성하여 텍스트와 모바일용 이미지를 같이 관리

* style: 모바일일때 스타일 수정

* refactor: 코드리뷰 반영
@wuzoo
Copy link

wuzoo commented Mar 23, 2025

한서님 대박이시네요 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ Feat ] 애니메이션

6 participants