netflix-clone

Date: #2021-11-18

1. Settings

2. Home part

3. Slider part

4. Box animation part

5. Movie modal part

1. Settings & Header part

#8.0

#8.1

#8.2

#8.3

#8.4

8.0은 소개영상.

React Route DOM으로 Route 설정. + Header 컴포넌트 만들기.

React Route Dom v6라 강의와 코드는 조금 다름.

const MainRouter = () => {
    return (
        <Router>
            <Header/>
            <Routes>
                <Route path={'/'} element={<MainPage />} />
                <Route path={'/tv'} element={<TvPage/>}/>
                <Route path={'/search'} element={<SearchPage/>}/>
            </Routes>
        </Router>
    );
};

그리고 헤더 설정..부분은 그냥 스타일링이라 딱히 참고해서 적을만한 내용은 없음. 헤더 부분은 계속 스타일링과 기존에 배웠던 framer motion 애니메이션 내용 반복이라 딱히 정리할 내용은 없음.

react router dom 6

react router dom 6로 넘어오면서 useRouteMatch가 없음. useLocation 훅을 사용해야 한다.

const { pathname } = useLocation();

<li>
    <Link to={'/'}>
        Home
        {pathname.split('/')[1] === '' && (
            <motion.span
                layoutId={'circle'}
                className={'circle'}
            />
        )}
    </Link>
</li>
<li>
    <Link to={'/tv'}>
        Tv shows
        {pathname.split('/')[1] === 'tv' && (
            <motion.span
                layoutId={'circle'}
                className={'circle'}
            />
        )}
    </Link>
</li>

코드를 위와 같이 꾸며봤는데.. layoutId 덕에 멋진 애니메이션이 나온다.

MenuPingPong

그 다음은 serach bar 애니메이션 클론.

Searchbar

// css
const Container = styled.div`
.right-box {
    display: flex;
    align-items: center;
    position: relative;
    svg {
        z-index: 99;
    }
.search-box {
        padding: 8px 10px 8px 40px;
        margin-left: -30px;
        outline: none;
        width: 0;
        border: 1px solid ${(props) => props.theme.white.lighter};
        background-color: ${(props) => props.theme.black.darker};
    }
}
`
const searchBarVars = {
    onInit: {
        width: 0,
        opacity: 0,
    },
    onClicked: {
        width: 300,
        opacity: 1,
        transition: {
            duration: 0.3,
        },
    },
    onExit: {
        width: 0,
        opacity: 0,
        transition: {
            duration: 0.1,
        },
    },
};

    
<AnimatePresence>
    {openSearch && (
        <motion.input
            className={'search-box'}
            variants={searchBarVars}
            initial={'onInit'}
            animate={'onClicked'}
            exit={'onExit'}
            placeholder={'제목, 사람, 장르'}
        />
    )}
</AnimatePresence>

나는 css와 motion 꾸미기를 위와 같이 했다. 약간 부자연스러운 면도 있었지만, 나름 만족하는 편이다.

니코의 코드는 scaleX를 애니메이션 해줬다. 선구현하고 니코 강의를 듣는 편인데.. 뭔가 더 멋있어 보이는 것 같기도 하고.. 내가 잘 모르는 css 속성이 있었는데.. transform-origin이라는 것.. 니코의 코드에서 보면 scaleX를 transform 애니메이션으로 넣으니 중간에서부터 촥 펼쳐져서 뭔가 자연스럽지 못했는데, transform-origin: right center 속성을 주고 나니 우측에서부터 촥 펴지는 모션을 볼 수 있었다. transform-origin

하지만 내가 한 것도 맘에 들기 때문에 내것을 수정하진 않을 것.

useAnimation

framer motion 공식홈페이지에 useAnimation으로 검색하니까 나오진 않는다… Animation 항목에 설명되면서 같이 나오긴한다. animation control

react native Animate의 start와 거의 유사하다고 봐야 할 것 같다.

핵심 요지는 컴포넌트에 animation 및 variant를 통해서 애니메이션을 할 수도 있지만, control을 통하여 animation을 할 수도 있다는 것 . 이런것이 있다 정도 이해하고 넘어간다. useAnimation으로 만든 변수는 motion의 animation 컴포넌트에 animate의 값으로 넘겨줘야 한다.

const navAnimation = useAnimation();
useEffect(()=> {
    scrollY.onChange( () => {
        if(scrollY.get() > 80) {
            navAnimation.start({backgroundColor: "rgba(0, 0, 0, 1)"})
        }else {
            navAnimation.start({backgroundColor: "rgba(0, 0, 0, 0)"})
        }
    })
}, [scrollY])
// .. 중략
    
<Nav animate={navAnimation}>
    
// .. 하략..

useAnimation 역시 variants를 활용해서 위 코드를 아래처럼 바꿀 수도 있다.

const navVars: Variants = {
    onInit: {
        backgroundColor: 'rgba(0, 0, 0, 0)',
    },
    onAnimate: {
        backgroundColor: 'rgba(0, 0, 0, 1)',
    },
};
// 중략
useEffect(() => {
    scrollY.onChange(async () => {
        if (scrollY.get() > 80) {
            await navAnimation.start('onAnimate');
        } else {
            await navAnimation.start('onInit');
        }
    });
}, [scrollY, navAnimation]);

return (
    <Nav
        variants={navVars}
        animate={navAnimation}
        initial={'onInit'}
        transition={{ default: 1 }}
    >
// 하략
)

Date: #2021-11-19

2. Home part

#8.5 #8.6

useQuery 사용 및 api 셋팅

이 부분은 여러번 해왔던 부분이기 때문에 정리 안해도 될 것 같다.

Loader

홈파트 시작에서부터 api가 필요하기 때문에 api 관련 세팅. 홈파트 시작하면서 로더를 만들어 봤는데.. framer motion을 이용해서 만들어 봄.

Loader

아주 짧은 애니메이션 코드가 들어갔는데, 이렇게 결과물이 멋있게 나온다(내 기준) 여기서 강의에서 배우지 않은 부분이 있었는데, transform 기존의 키값으로 애니메이션의 값을 바꾸는 것만으로는 스핀을 만들어 낼 수가 없었다.

const circleVars: Variants = {
    onInit: {
        y: 10,
        rotate: 0,
        backgroundColor: 'rgb(0, 0, 0)',
    },
    onAnimate: {
        rotate: 360,
        backgroundColor: 'rgb(0, 0, 255)',
        transition: {
            type: 'linear',
            repeat: Infinity,
            duration: 1,
        },
    },
};

이를테면 위처럼 y값을 주고, rotate되어라 얍! 한다고 스핀이 되진 않았다. transformTemplate이라는 것을 이용했었어야 했다.

https://www.framer.com/docs/component/##transform

역시 막힐 때는 공식 문서를 먼저 참고해야 한다.

const transformTemplate: TransformTemplate = ({ rotate, y }) => {
return `rotate(${rotate}) translateY(${y})`;
};

<motion.div
    className={'circle'}
    transformTemplate={transformTemplate}
    variants={circleVars}
    initial={'onInit'}
    animate={'onAnimate'}
/>

위처럼 코드를 만들어 놓으니, 맘에 들도록 스핀을 시작했다.

linear-gradient

놀랍게 background-image 속성은 여러가지를 같이 사용할 수 있었다.

.image {
        width: 100%;
        height: 100%;
        background-image: linear-gradient(
                rgba(0, 0, 0, 0.5),
                rgba(0, 0, 0, 0.5)
            ),
            url('${(props) => props.image}');

배경을 여러개 놓을 수 있는 것은 처음 알았는데, 이렇게 하면, 배경 + alpha 값이 있는 gradient를 같이 이용할 수 있게 된다.

Date: #2021-11-20

3. Slider part

#8.7 #8.8

항상 강의 듣기 선 구현을 하고, 강의를 듣는 편인데 슬라이더면 구현하는데 시간이 그래도 조금은 걸릴 것인디.. 너무 쉽게 빨리 구현되어서 놀랐다.

const sliderVars: Variants = {
    onInit: (toLeft: boolean) => ({
        x: toLeft ? '100%' : '-100%',
    }),
    onAnimate: {
        x: 0,
        transition: {
            duration: 0.8,
        },
    },
    onExit: (toLeft: boolean) => ({
        x: toLeft ? `-100%` : '100%',
        transition: {
            duration: 0.8,
        },
    }),
};

<Slider>
    <div className={'button-container'}>
        <motion.div
            className={'button'}
            initial={{ opacity: 0.3 }}
            onClick={onClickSliderLeft}
            whileHover={{ opacity: 1 }}
        />
        <motion.div
            className={'button'}
            initial={{ opacity: 0.3 }}
            onClick={onClickSliderRight}
            whileHover={{ opacity: 1 }}
        />
    </div>
    <AnimatePresence custom={toLeft}>
        <motion.div
            variants={sliderVars}
            custom={toLeft}
            key={`SliderPager:${sliderPage}`}
            className={'row'}
            initial={'onInit'}
            animate={'onAnimate'}
            exit={'onExit'}
        >
            {data.results
                .slice(
                    sliderPage * PAGE_SIZE,
                    (sliderPage + 1) * PAGE_SIZE,
                )
                .map((m) => (
                    <div
                        className={'box'}
                        key={m.id}
                        style={{
                            backgroundImage: `url("${imageUrl(
                                m.poster_path,
                                'w200',
                            )}")`,
                        }}
                    />
                ))}
        </motion.div>
    </AnimatePresence>
</Slider>

위 짧은 코드로 아래 그림과 같은 슬라이더가 완성이 되었다. Slider

너무 짧은 코든데.. 물론 미완성이긴 하지만, css 조금 더 손보면 되는 것.. key값 트릭과 AnimatePresence 컴포넌트를 이용하니.. 코드 내용도 참 짧고 좋다. 저번에 배운 custom을 이용하면 슬라이더 방향도 설정할 수 있었다. 저번 삽질의 결과 덕분인지 코드는 한 번에 완성이 되었다.

onExitComplete

슬라이더가 애니메이션 중에 계속 슬라이드 넘기는 액션을 넣어주면 이상한 애니메이션이 나온다. onExitComplete 속성은 AnimatePresence 컴포넌트의 속성인데.. 속성이름에서 짐작을 할 수 있듯, exit가 끝나면 호출되는 콜백이다.

<AnimatePresence
    onExitComplete={() => setLeaving(false)}
    custom={toLeft}
>

위의 코드처럼 leaving하는 동안 다음 행동을 못하도록 하는 state를 넣어 놓고 나서, 슬라이드 애니메이션 끝나고 나서 onExitComplete가 호출되면 다시 슬라이드를 할 수 있게끔 하는 코드를 만들 수 있다.

4. Box animation part

슬라이더 메뉴에 마우스를 올려놨을 때 커지는 애니메이션을 구현. 넷플릭스와는 똑같이 구현할 메뉴가 없어서, 대략적인 메뉴만 구현한다.

transform-origin

슬라이더의 양측을 보면 가장 왼쪽이나 가장 오른쪽 박스는 다른 안쪽에 있는 박스와 똑같은 양식으로 움직이면 안된다.

before-transform-origin

그래서

.box {
        background-size: cover;
        background-position: center center;
        width: 100%;
        aspect-ratio: 1.5;
        display: flex;
        &:first-child {
            transform-origin: left;
        }
        &:last-child {
            transform-origin: right;
        }
    }

그래서 위의 코드처럼 transform origin을 주면..

after-transform-origin 이렇게 애니메이션이 바뀔 수 있다.

Animation propagation

box animation 구현 중인데, 항상 그렇듯.. 선구현 후강의라.. 상위 박스가 hover 되었을 때, 하위 박스가 hover를 같이 전달 받고 싶었는데, 구글링해보니 생각보다 쉬운 개념인게.. framer motion에서는 이미 이 문제를 알고 있기 때문에 propagation을 구현해 놨다.

Propagation Stackoverflow-Propagation

중요한 내용! 따로 children에 whileHover를 해주지 않아도 hover animation을 트리거한다. 같은 이름을 가지고 있어야 하고, variants를 이용해야 한다.

5. Movie Modal part

모달창 띠우기. layoutId를 이용하면 쉽게 구현할 수 있다.

<AnimatePresence exitBeforeEnter>
    {movieDetail && (
        <MovieDetail
            variants={movieDetailVars}
            initial={'onInit'}
            animate={'onAnimate'}
            exit={'onExit'}
            onClick={() => setMovieDetail(null)}
        >
            <motion.div
                layoutId={`movieDetail:${movieDetail.id}`}
                className={'detail-box'}
            >
                <div
                    className={'backdrop'}
                    style={{
                        backgroundImage: `url("${imageUrl(
                            movieDetail.backdrop_path,
                        )}")`,
                    }}
                />
            </motion.div>
        </MovieDetail>
    )}
</AnimatePresence>

대충 이런 코드로 구현을 했는데, 상기 layoutId에 해당하는 부분은 movie를 렌더링 하는 부분에도 넣어줬다. 강의와는 내 코드가 사뭇다른데, 니코는 url path에 movie id를 넘겨주어서, 정보를 캐치했는데, 아마도 이렇게 하는 방법도 있다는 것을 보여주기 위함인 것으로 보인다. 나는 굳이 그렇게 할 필욘 없을 것 같아서 그냥 state를 이용했다. (강의를 조금 더 듣다 보니 왜 그런줄 알았다. tmdb에서 movie detail을 fetching하려 하는 것.. 사실 그런 목적에도 굳이 route를 설정할 필요가 없기 때문에 나는 생략…)

Modal

저 짧은 코드로 이렇게 모달 + 애니메이션이 완성이 된다. 후덜덜..

fixed & absolute

딱히 길게 적진 않겠음. fixed가 필요할 때와 absolute 필요할 때를 잘 구분해야 한다. 참고 링크

나는 css의 fixed를 사용하는 것이면 충분하다 생각하는데.. 니코는 useViewportScroll 훅을 이용해서 absolute + top 속성을 이용해서ㅓ 위치를 표현했다. 아마도 이렇게도 이용할 수 있음을 보여주는 것인 것 같다.

나머지…

나머지 부분은 반복되는 부분 + 코드 챌린지 내용. 이제 내용 정리하고 코드챌린지 들어간다.


Written by@pleed0215
records.length > recalls.length

InstagramGitHubFacebook