사용 프레임워크: React.js

배포 링크: https://kosy0907.github.io/Portfolio/

소스코드 링크: https://github.com/kosy0907/Portfolio (Public)

타입스크립트로 제작할까 하다가 TS는 아직 부족한 거 같아서 그냥 JS로 제작

언제나 그랬듯이 디자인 -> 구현 순서로 진행

 

디자인 못해서 여러 번 수정했다(지금도 수정중)
하지만 재밌죠?

Section은 3개로 나눴다.

  • Section1(Home) - Parallax Effect, Scroll Effect, Bounce Animation
  • Section2(About) - FadeIn Animation
  • Section3(Project) - Click-FadeIn Animation

시작은 npx create-react-app

미리 보는 component

 

1. Custom Cursor

웹사이트를 서치하다보면 가끔 둥글거나 다른 모양이 적용된 Cursor를 확인할 수 있다.

내심 어떻게 구현한 건지 궁금했는데 웹 사이트를 제작하면서 확실하게 알게 되었다.

import React, { useEffect, useRef } from 'react';
import './Cursor.css';

const Cursor = () => {
    const mainCursorRef = useRef(null);
    const subCursorRef = useRef(null);

    useEffect(() => {
        const onMouseMove = (e) => {
            const { clientX, clientY } = e

            mainCursorRef.current.style.transform = `translate3d(${clientX}px, ${clientY}px, 0)`;
            subCursorRef.current.style.transform = `translate3d(${clientX}px, ${clientY}px, 0)`;
            subCursorRef.current.style.transition = `all 0.15s`;
        }
        document.addEventListener('mousemove', onMouseMove);

        return () => {
            document.removeEventListener('mousemove', onMouseMove);
        }
    }, [])

    return (
        <div>
            <div className='mainCursor' ref={mainCursorRef} />
            <div className='subCursor' ref={subCursorRef} />
        </div>
    );
}

export default Cursor;

1. div에 useRef Hook을 사용해 mainCursorRef, subCursorRef를 걸고

2. useEffect Hook으로 마우스가 이동할 때마다 커서의 위치(clientX, clientY)를 업데이트 하는 이벤트 리스너를 추가한다. 

3. subCursor(조금 더 큰 원)이 커서를 천천히 따라올 수 있도록 transition animation을 추가한다.

이렇게 하면 Component가 마운트되면 이벤트 리스너가 추가되고 언마운트되면 제거된다.

Custom Cursor 완성!

 

2. Section1 - Home

Parallax Effect를 넣고 싶어서 직접 제작한 픽셀아트 gif의 레이어를 밤하늘, 별, 건물, 산 등등으로 나눴다.

아이패드 없었으면 어쩔 뻔했어

예전에는 Steam에서 구매한 Aseprite 썼는데 아이패드로 하는 게 훨씬 편하고 빠르다.

진작에 프로크리에이트 쓸걸

잠깐! 포토샵을 사용한 이유는?

픽셀아트는 캔버스 사이즈가 작기 때문에 (보통 200x200) 막 늘려서 웹페이지에 적용했다가는 이미지가 깨질 수 있다.

포토샵으로 레이어 하나하나 크기를 늘려준다. 귀찮은 작업일 수 있지만 픽셀아트는 이것마저 재밌다.

image 폴더에 쏙

// Section1
import React, { useEffect, useState } from 'react';
...

function Section1(props) {
    // parallax
    const [position, setPosition] = useState(0);
    
    const onScroll = () => {
        setPosition(window.scrollY);
    }
    const toAbout = () => {
        props.aboutRef.current?.scrollIntoView({ behavior: 'smooth' });
    }

    useEffect(() => {
        window.addEventListener('scroll', onScroll);
        return () => {
            window.removeEventListener('scroll', onScroll);
        }
    }, []);

    return (
        <div className='home'>
            <div className='introBg'>
                <div className='intro bg1' style={{ backgroundPositionY: position }} />
                <div className='intro bg2' style={{ backgroundPositionY: position }} />
                <div className='intro bg3' style={{ backgroundPositionY: position / 2 }} />
                <div className='intro bg4' style={{ backgroundPositionY: position / 2 }} />
                <div className='intro bg5' style={{ backgroundPositionY: position / 3 }} />
                <div className='intro bg6' style={{ backgroundPositionY: position / 10 }} />

                <div className='intro cover'>
                    <div className='text'>
                    // Vertical Scrolling Text
                        <div className='scrollContainer'>
                            <div className='scrollBox'>
                                ...
                            </div> 
                        </div >

                        <div className='fixed-container'>
                            ...
                        </div>
                        // Arrow Bounce Animation
                        <div style={{ marginTop: "3rem" }}>
                            <FontAwesomeIcon className='bounceArrow' icon={faAngleDown} size="2x" />
                        </div>
                       	<div className='toAboutBtn' onClick={toAbout}>About Me</div>
                    </div >
                </div >

            </div >
        </div >
    );
}

export default Section1;

intro bg1~bg6은 각각 위에서 만든 픽셀아트 레이어들을 담는 div이다.

intro cover로 전체 화면을 어둡게 덮고 그 위에 Scrolling text 애니메이션과 Bounce Animation을 적용했다.

특정 환경에서는 PositionY의 값이 변경될 수 있다.

toAboutBtn은 클릭 시 바로 About Section으로 스크롤 되도록 구현했다.

레이어 나눈 보람이 있다

// Navbar
import React, { forwardRef } from 'react';
import './Navbar.css';

function Navbar(props) {

    const HomeClick = () => {
        props.setNavState(1);
        window.scrollTo({ top: 0, behavior: 'smooth' });
    }

    const AboutClick = () => {
        props.setNavState(2);
        props.aboutRef.current?.scrollIntoView({ behavior: 'smooth' });
    }

    const ProjectClick = () => {
        props.setNavState(3);
        props.projectRef.current?.scrollIntoView({ behavior: 'smooth' });
    }

    return (
        <nav>
            <ul>
                <li className='navBtn' onClick={HomeClick}>HOME<span>.</span></li>
                <li className='navBtn' onClick={AboutClick}>ABOUT<span>.</span></li>
                <li className='navBtn' onClick={ProjectClick}>PROJECT<span>.</span></li>
            </ul>
        </nav>
    );
}
export default forwardRef(Navbar);

App.js(부모 컴포넌트)에서 props를 통해 setNavState, 다른 Section 컴포넌트(About, Project)를 가져온다.

Navbar에 있는 버튼을 클릭할 때마다 각각 HomeClick, AboutClick, ProjectClick 함수가 호출되어 부드럽게 스크롤 되도록 구현했다. 

 

3. Section2 - About

간단한 소개와 Education, Certificate, 사용 가능한 Skill을 담은 컴포넌트이다.

Education과 Certificate를 넣는 게 맞는 선택일까 고민했지만 그냥 넣었다.

import React, { forwardRef, useEffect } from 'react';
import './Section2.css';

function Section2(props, aboutRef) {

    useEffect(() => {
        const observer = new IntersectionObserver(
            entries => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        entry.target.classList.add('fadeIn');
                    } else {
                        entry.target.classList.remove('fadeIn');
                    }
                });
            },
            { threshold: 0.2 }
        );

        const targets = document.querySelectorAll('.fadeTarget');
        targets.forEach(target => observer.observe(target));

        return () => observer.disconnect();
    }, []);

    return (
        <div className='about' ref={aboutRef}>
            <div className='container'>
                <div className='title fadeTarget'>
                    <p>About Me</p>
                </div>
                <div className='aboutContent fadeTarget'>
                    ...
                </div>
            </div>
        </div>
    );
}

export default forwardRef(Section2);

Home에서 About으로 스크롤 시 fade in 애니메이션으로 나타나게끔 구현했다.

여기서는 Intersection Observer를 사용했다. 

1. 컴포넌트가 마운트될 때 IntersectionObserver를 생성하고

2. Observer가 관찰할 수 있도록 애니메이션을 줄 div에 'fadeTarget' 클래스를 추가한다.

3. 여기서 style에 animation delay를 줘서 애니메이션을 늦출 수 있다.

4. css에 fadeIn 애니메이션을 설정하면 끝!

 

4. Section3 - Project

Navbar의 Project 버튼을 클릭하거나 About Section에서 밑으로 스크롤하면 나타난다.

projectTitle을 Click했을 때, 프로젝트 정보를 담은 card가 fade in animation으로 cardContainer에 나타나게 구현하고 싶었다.

// section3(project)
	...
	const handleItemClick = (item) => {
        if (selectedItem === null) {
            setSelectedItem({ ...item, show: true });
            setIsCardVisible(true);
        } else if (selectedItem.id !== item.id) {
            setSelectedItem({ ...item, show: true });
            setIsCardVisible(false);
            setTimeout(() => setIsCardVisible(true), 500);
        }
    };
    ...
    return (
    	...
        <div className='projectTitle'>
        ...
        <div className='cardContainer>
        ...
    )
// Card.js
import React, { useEffect, useState } from 'react';
import './Card.css';

function Card(props) {
    const { item } = props;

    const [showCard, setShowCard] = useState(false);

    useEffect(() => {
        if (item.show) {
            setTimeout(() => {
                setShowCard(true);
            }, 20);
        }
    }, [item]);

    return (
        <div className={`card ${showCard ? 'show' : ''}`}>
            <div className="cardTitle">{item.title}</div>
            <div className="cardDescription">{item.description}</div>
        </div>
    );
}

export default Card;

하지만, 이렇게 하면 다른 projectTitle을 클릭했을 때, 가끔씩 애니메이션이 적용되지 않는 문제가 있었다.

원인은 handleItemClick의 setTimeout이 문제였다.

setTimeout은 시간을 정확하게 보장하지 않기 때문에, 대신 requestAnimationFrame을 사용하는 것이 좋다.

requestAnimationFrame은 브라우저의 리플로우, 리페인트 과정을 최적화하면서 실행되기 때문에, 애니메이션 효과를 좀 더 부드럽게 적용할 수 있다.

// section3 수정
    const handleItemClick = (item) => {
        if (selectedItem === null) {
            setSelectedItem({ ...item, show: true });
            setIsCardVisible(true);
        } else if (selectedItem.id !== item.id) {
            setSelectedItem({ ...item, show: true });
            setIsCardVisible(false);
            window.requestAnimationFrame(() => setIsCardVisible(true));
        }
    };

 

5. App.js

...

function App() {
  const [scrollIndex, setScrollIndex] = useState(1);
  const [navState, setNavState] = useState(1);
  const aboutRef = useRef(null);
  const projectRef = useRef(null);

  useEffect(() => {
    if (navState === 1) {
      setScrollIndex(1);
    } else if (navState === 2) {
      setScrollIndex(2);
    } else if (navState === 3) {
      setScrollIndex(3);
    }
  }, [navState])

  useEffect(() => {
    const wheelHandler = (e) => {
      ...
    };

    document.addEventListener("wheel", wheelHandler);
    return () => {
      document.removeEventListener("wheel", wheelHandler);
    };

  }, []);

  return (
    <div className="App">
      <Cursor />
      <Navbar aboutRef={aboutRef} projectRef={projectRef}
        setNavState={setNavState} />
      <div className='section'>
        <Dots scrollIndex={scrollIndex} />
        <Section1 />
        <div className="divider" />
        <Section2 ref={aboutRef} />
        <div className="divider" />
        <Section3 ref={projectRef} />
      </div>
      <Footer />
    </div>
  );
}

export default App;

App.js에는 Navbar에 Section을 넘겨주고 컴포넌트를 리턴하도록 구현했다.

마우스 스크롤 시 부드럽게 움직이도록 wheelHandler 함수를 추가했다.

'React.js > Project' 카테고리의 다른 글

EV-map 제작기(백업)  (0) 2023.05.03

+ Recent posts