이번 과제는 '브랜드의 intro 페이지'를 만드는 것이였다.
특이 사항으로는 3주차(월) 수업시간에 배운 Next.js와 Typescript 활용을 해야 했다.
(주관적인 의견으로 단일 페이지라 라우팅이 필요 없었고 Next.js의 장점을 느껴볼 수 없었다..👀)
클론코딩이다 보니 마크업이 80프로 이상이었고,
구현할 기능은 scroll 이벤트에 반응하는 애니메이션 정도였다.
CNA + TS template
ESLint, Prettier (+ TypeScript)
styled-components, reset-css
절대경로 세팅 src/jsconfig.json
components 나누기
정적 파일 public/videos, images 넣기
common/constants
git repo 생성
git subscribe pre-onboarding-course-team-6/{레포이름}
스크롤을 내리기 전에는 투명+흰 글씨, 스크롤이 있으면 흰 배경+검정 글씨로 바뀌는 헤더를 구현하였다.
scroll을 감지하는 코드를 찾았고 이를 적용하기로 하였다.
import { useState, useEffect } from "react";
export const useScroll = () => {
// 초기상태 설정
const [state, setState] = useState({
x: 0,
y: 0,
});
// 현재의 좌표를 State에 저장한다
const onScroll = () => {
setState({
x: window.scrollX,
y: window.scrollY,
});
};
// 스크롤이 감지되면 onScroll을 호출한다
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
// 좌표가 저장되어 있는 state 반환
return state;
};
const Header: React.FC = () => {
const { y } = useScroll(); // 스크롤 좌표중 y축의 좌표만 불러온다
return (
<S.Header yScroll={y}>
<S.Container yScroll={y}>
<a href="#">
{/* y축이 0이상이면 검정색 로고, 0이면 흰색 로고(default) */}
<img src={y > 0 ? BLACK_LOGO_URL : WHITE_LOGO_URL} alt="logo" />
</a>
<nav>
<ul>
<li>도서구매</li>
<li>장바구니</li>
<li>|</li>
<li>이용권 관리</li>
<li>로그인/회원가입</li>
</ul>
</nav>
</S.Container>
</S.Header>
);
};
컴포넌트의 일부분이 화면에 노출 되기 시작하면 떠오르는 애니메이션을 구현했다.
라이브러리를 쓸 수 있었지만 스스로 구현을 해보고 싶었다.
하지만 한정된 시간 속에서 아쉽게도 구현은 하지 못하고 학습을 위해서 hook으로 사용할 수 있는 코드를 찾았다.
아래의 코드를 읽기 전에 Intersection Observer API를 모른다면 여기를 먼저 참조하면 좋다.
intersection observer 는 Target Element 가 화면에 노출되었는 지 여부를 간단하게 구독할 수 있는 API 이다.
js -> ts로 변환 하면서 여러 이슈가 있었다 (많이 배울 수 있었다😮)
import { useRef, useEffect, useCallback, MutableRefObject } from "react";
interface ScrollFadeIn { // 반환 타입 정의
ref?: MutableRefObject<HTMLDivElement>;
style: {
opacity: number;
transform: string;
};
}
const useScrollFadeIn = (
direction = "up", // default parameter
duration = 1,
delay = 0
): ScrollFadeIn => {
// Ref는 포커스, 미디어 재생 또는 애니메이션을 직접적으로 실행 시키기 위해
// 외부에서 DOM(또는 React Component)을 제어 할 수 있게 도와줍니다.
const dom = useRef<HTMLDivElement>(null);
// name이 up이면 아래서 위로 올라오는 모션
// stay는 움직임 없이 투명도에 변화만 주고싶을때 사용
const handleDirection = (name: string) => {
switch (name) {
case "up":
return "translate3d(0, 50%, 0)";
case "down":
return "translate3d(0, -50%, 0)";
case "left":
return "translate3d(50%, 0, 0)";
case "right":
return "translate3d(-50%, 0, 0)";
case "stay":
return "translate3d(0, 0, 0)";
default:
return;
}
};
// 관찰할 대상이 등록되거나 가시성(보이는지 안보이는지)에 변화가 생기면 관찰자는 콜백(Callback)을 실행한다.
const handleScroll = useCallback(
([entry]) => {
const {
current: { style },
} = dom;
// 만약 원하는 threshold만큼 노출되었다면,
if (entry.isIntersecting) {
style.transitionProperty = "all";
// 애니메이션 동작 시간
style.transitionDuration = `${duration}s`;
style.transitionTimingFunction = "cubic-bezier(0, 0, 0.2, 1)";
// 지연시간 => 여러 컴포넌트를 각각 다른 시간에 보여주고 싶으면 사용할 수 있다.
style.transitionDelay = `${delay}s`;
// transition과 함께 투명도와 위치를 원래대로 되돌린다.
style.opacity = "1";
style.transform = "translate3d(0, 0, 0)";
}
},
[delay, duration]
);
useEffect(() => {
let observer;
const { current } = dom;
if (current) {
// IntersectionObserver에 동작 하게 할 함수와 Observer 세팅 값들을 넘겨 줍니다.
// - 'threshold'는 TargetElement의 노출 비율을 말하는 것이며,
// - 0.5는 50% 정도 노출 되었을 때 해당 이벤트가 실행되게 됩니다.
observer = new IntersectionObserver(handleScroll, { threshold: 0.5 });
observer.observe(current);
}
return () => observer && observer.disconnect();
}, [handleScroll]);
// useScrollFadeIn를 사용할 DOM에 지정할 CSS 속성들
return {
ref: dom,
style: {
opacity: 0,
transform: handleDirection(direction),
},
};
};
export default useScrollFadeIn;
const Section01: React.FC = () => {
// 애니메이션의 delay 시간을 각각 다르게 주어서
// first -> fourth 순으로 동작하게 한다
const firstAnimated = useScrollFadeIn("up", 1, 0);
const secondAnimated = useScrollFadeIn("up", 1, 0.1);
const thirdAnimated = useScrollFadeIn("up", 1, 0.1);
const fourthAnimated = useScrollFadeIn("up", 1, 0.3);
return (
<S.Section>
<S.TextWrapper>
{/* spread operator로 원하는 부분에 적용시켜준다*/}
<S.StarLine {...fourthAnimated}>
<img className="star" src="/images/star2.png" alt="star" />
<img className="line" src="/images/line.png" alt="line" />
</S.StarLine>
<S.Span {...firstAnimated}>책 읽는 재미,</S.Span>
<S.Span {...secondAnimated}>땅콩스쿨이</S.Span>
<S.Span {...thirdAnimated}>만들어줄게요!</S.Span>
</S.TextWrapper>
<S.MouseAnimation>
</S.Section>
);
};
요구사항에서 '3분 이내로 작동하는 E2E 테스트를 구현해 주세요.' 라는 항목이 있었지만 시간 부족으로 할 수 없었다..
지난번 과제에서 CSS를 자주 하지 않으면 실력이 퇴화되어 간다는 느낌을 받았다. 이번에는 과제 두 개중에 선택할 수 있었고, 이 과제를 선택했는데 연습을 열심히 할 수 있었다.
Custom hooks를 (직접 구현은 하지 못하고 🥲) 사용해볼 수 있었다. 리액트를 처음 배울때 Nomad Coders가 만든 Custom hooks 10개 만들기 강의가 있었는데, 초보자 시절에는 20%도 흡수 못했던 것 같다. 다시 수강하면서 학습을 해야할 필요성을 느꼈다.
Next.js를 처음 사용해봤는데 아직 진가를 못느끼고 있다. 공부할 시간이 필요한데 과제를 진행하면서 학습한다는 것은 정말 힘든 것 같다. POB가 끝나면 좀 더 깊숙하게 공부하고 제대로 사용해보고 싶다.