본문 바로가기
Tutorial/port2023

13. 포트폴리오 사이트 만들기 : React-Site : 헤더 영역

by @webstoryboy 2023. 7. 31.
Tutorial/portfolio

포트폴리오 사이트 만들기 - React

by @webs 2023. 08. 01.
03
포트폴리오 사이트 만들기 : 헤더 영역
난이도 중간

소개

안녕하세요! 웹스토리보이입니다. 이제 하나씩 컴퍼넌트를 완성해 가봅시다. 7개의 컴포넌트를 하나씩 완성하면 기존처럼 멋있는 사이트가 완성될 것입니다. 처음에 만들었던 방식을 토대로 변화하는 과정과 재사용 및 효율적인 코딩방식으로 변화는 과정을 눈여겨 보면 좋습니다. 그럼 시작해보겠습니다.

인덱스

VITE SITE

  • 1. 셋팅하기
    • 1_1. vite 설치하기
    • 1_2. vite 폴더 정리하기
    • 1_3. gsap/lenis 설치하기
    • 1_4. git 연동하기
  • 2. 레이아웃
    • 2.1 레이아웃 구조 만들기
    • 2.2 메인 레이아웃 구조 만들기
    • 2.3 CSS 셋팅하기
    • 2.4 JavaScript 셋팅하기
  • 3. 헤더 영역
    • 3.1 헤더 구조 잡기
    • 3.2 헤더 디자인 설정
    • 3.3 반응형 작업하기
    • 3.4 메뉴 자바스크립트 설정
  • 4. 인트로 영역
    • 4.1 인트로 구조 잡기
    • 4.2 인트로 디자인 설정
    • 4.3 반응형 작업하기
  • 5. 스킬 영역
    • 5.1 스킬 구조 잡기
    • 5.2 스킬 디자인 설정
    • 5.3 반응형 작업하기
  • 6. 사이트 영역
    • 6.1 사이트 구조 잡기
    • 6.2 사이트 디자인 설정
    • 6.3 반응형 작업하기
  • 7. 포트폴리오 영역
    • 7.1 사이트 구조 잡기
    • 7.2 사이트 디자인 설정
    • 7.3 반응형 작업하기
    • 7.4 스크립트 작업하기
  • 8. 연락처 영역
    • 8.1 연락처 구조 잡기
    • 8.2 연락처 디자인 설정
    • 8.3 반응형 작업하기
  • 9. 푸터 영역
    • 9.1 푸터 구조 잡기
    • 9.2 푸터 디자인 설정
    • 9.3 반응형 작업하기
  • 10. 마무리
    • 10.1 스무스 효과주기
    • 10.2 링크 연결하기
    • 10.3 netlify에 배포하기

REACT SITE

  • 1. 셋팅하기
    • 1_1. React 설치하기
    • 1_2. React 폴더 정리하기
    • 1_3. 라이브러리 설치하기
    • 1_4. git 연동하기
  • 2. 라우팅 및 컴퍼넌트
    • 2_1. 라우팅 설정하기
    • 2_2. 컴퍼넌트 설정하기
    • 2_3. SCSS 설정하기
  • 3. 헤더 영역
    • 3_1. 헤더 구조잡기
    • 3_2. 헤더 디자인 설정
    • 3_3. 헤더 데이터 작업
    • 3_4. 헤더 토글 메뉴 작업하기

3. 헤더 영역

3.1 헤더 구조잡기

우선 기본 HTML구조를 리액트 구조에 맞추어서 작업을 해야 합니다. <a href="#">은 <a href="/">로 변경해야 경고가 없어집니다. 또한 리액트에서는 class를 사용하지 않습니다. className으로 변경해야 합니다.

import React from "react";

const Header = () => {
    return (
        <header id="header" role="banner">
            <div className="header__inner">
                <div className="header__logo">
                    <a href="/">portfolio<em>vite</em></a>
                </div>
                <nav className="header__nav" role="navigation" aria-label="메인 메뉴">
                    <ul>
                        <li><a href="#intro">intro</a></li>
                        <li><a href="#skill">skill</a></li>
                        <li><a href="#site">site</a></li>
                        <li><a href="#port">portfolio</a></li>
                        <li><a href="#contact">contact</a></li>
                    </ul>
                </nav>
                <div 
                    className="header__nav__mobile" 
                    id="headerToggle" 
                    aria-controls="primary-menu" 
                    aria-expanded="false" 
                    role="button" 
                    tabindex="0"
                >
                    <span></span>
                </div>
            </div>
        </header>
    );
};

export default Header;

3.2 헤더 디자인 설정

기존에 CSS 방식보다는 어려울 수 있지만 사용하다 보면 더 편함을 느낄 수 있습니다. 반응형도 따로 작업하지 않고 해당 영역에 동시에 작업을 하였습니다.

#header {
    @include position-fixed;
    z-index: 10000;
}
.header__inner {
    @include flex-between;
    background-color: rgba(116, 99, 99, 0.1);
    backdrop-filter: blur(15px);
    padding: 1rem;

    .header__logo {
        font-size: 0.9rem;
        text-align: center;
        text-transform: uppercase;
        line-height: 1;

        em {
            font-size: 10px;
            display: block;
            color: var(--black200);
        }
    }
    .header__nav {

        @media (max-width: 800px){
            display: none;

            &.show {
                display: block;

                ul {
                    display: block;
                    position: absolute;
                    right: 0;
                    top: 68px;
                    background-color: rgba(116,99,99,0.1);
                    backdrop-filter: blur(15px);
                    z-index: 10000;
                    min-width: 150px;
                    padding: 20px 0;

                    li {
                        display: block;
                        text-align: right;

                        a {
                            display: inline-block;
                            padding: 10px;
                        }
                    }
                }
            }
            &.show + .header__nav__mobile span::before {
                width: 20px;
            }
            &.show + .header__nav__mobile span::after {
                width: 20px;
            }
        }
        
        li {
            display: inline;
    
            a {
                text-transform: uppercase;
                font-size: 14px;
                padding: 14px;
                position: relative;
    
                &::before {
                    content: '';
                    width: calc(100% - 28px);
                    height: 1px;
                    background-color: var(--black);
                    position: absolute;
                    left: 14px;
                    bottom: 10px;
                    transform: scaleX(0);
                    transition: all 0.3s;
                }
                &:hover::before {
                    transform: scaleX(1);
                }
            }
        }
    }
    
    .header__nav__mobile {
        display: none;
        width: 40px;
        height: 40px;
        cursor: pointer;

        @media (max-width: 800px){
            display: block;
        }

        span {
            display: block;
            width: 40px;
            height: 2px;
            background-color: var(--black);
            margin-top: 19px;
            position: relative;

            &::before {
                content: "";
                width: 40px;
                height: 2px;
                background-color: var(--black);
                position: absolute;
                right: 0;
                top: 6px;
                transition: width 0.3s;
            }
            &::after {
                content: "";
                width: 40px;
                height: 2px;
                background-color: var(--black);
                position: absolute;
                left: 0;
                bottom: 6px;
                transition: width 0.3s;
            }
        }
    }
} 

3.3 헤더 데이터 작업

리액트에서 데이터를 따로 작업하는 이유는 코드를 더 구조적이고 관리하기 쉽게 만들기 위해서입니다. 데이터를 따로 작업하여 관리함으로써 코드의 가독성과 유지보수성을 향상시킬 수 있습니다. 규모가 작은 사이트를 라면 불편할 수도 있는 작업이지만 규모가 커질 수록 관리하기가 쉬어집니다.

const headerNav = [
    {
        title: "intro",
        url: "#intro",
    },
    {
        title: "skill",
        url: "#skill",
    },
    {
        title: "site",
        url: "#site",
    },
    {
        title: "portfolio",
        url: "#port",
    },
    {
        title: "contact",
        url: "#contact",
    },
];

반복적으로 처리되는 부분을 map() 메서드를 통해 작업해보겠습니다. map()은 JavaScript의 배열 메서드 중 하나로, 배열의 각 요소를 변환하여 새로운 배열을 반환하는 함수입니다. React에서도 map()을 많이 활용하여 배열 데이터를 가지고 JSX를 생성하거나 처리하는 데 사용됩니다.

const Header = () => {
    return (
        <header id="header" role="banner">
            <div className="header__inner">
                <div className="header__logo">
                    <a href="/">portfolio<em>vite</em></a>
                </div>
                <nav className="header__nav" role="navigation" aria-label="메인 메뉴">
                    <ul>
                        {headerNav.map((nav, key) => (
                            <li key={key}>
                                <a href={nav.url}>{nav.title}</a>
                            </li>
                        ))}
                    </ul>
                </nav>
                <div 
                    className="header__nav__mobile" 
                    id="headerToggle" 
                    aria-controls="primary-menu" 
                    aria-expanded="false" 
                    role="button" 
                    tabIndex="0"
                >
                    <span></span>
                </div>
            </div>
        </header>
    );
};

잠깐 map() 메서드에 대해서 알아보고 가겠습니다. currentValue : 현재 배열 요소의 값, index : 현재 배열 요소의 인덱스, array : 원본 배열을 의미합니다.

const newArray = array.map((currentValue, index, array) => {
    // 변환 로직을 작성하여 currentValue를 새로운 값으로 변환 후 반환
    return newValue;
});

map()을 사용하면 원본 배열의 각 요소들을 순회하면서 새로운 배열을 만들 수 있습니다. 즉, 기존 배열을 변형하지 않고 새로운 배열을 생성합니다

const numbers = [1, 2, 3, 4, 5];

const doubledNumbers = numbers.map((number) => {
    return number * 2;
});

console.log(doubledNumbers); // [2, 4, 6, 8, 10]

배열 요소들을 JSX 엘리먼트로 변환하는 경우 (React에서 주로 사용하는 방법)

const names = ["Alice", "Bob", "Charlie"];

const nameList = names.map((name, index) => {
    return <li key={index}>{name}</li>;
});

3.4 헤더 토글 메뉴 작업하기

반응형이 되었을 때 메뉴를 클릭하면 서브 메뉴가 나오도록 작업하겠습니다. 자바스크립트에서 썼던 방법을 써도 되지만, 효율성있고 리액트에 더 친화적인 방법을 쓰겠습니다.

import React, { useState } from "react";

const headerNav = [
    {
        title: "intro",
        url: "#intro"
    },
    {
        title: "skill",
        url: "#skill"
    },
    {
        title: "site",
        url: "#site"
    },
    {
        title: "portfolio",
        url: "#port"
    },
    {
        title: "contact",
        url: "#contact"
    }
];

const Header = () => {
    const [show, setShow] = useState(false);

    const toggleMenu = () => {
        setShow((prevShow) => !prevShow);
    };

    return (
        <header id="header" role="banner">
            <div className="header__inner">
                <div className="header__logo">
                    <a href="/">portfolio<em>react.js</em></a>
                </div>
                <nav className={`header__nav ${show ? "show" : ""}`} role="navigation" aria-label="메인 메뉴">
                    <ul>
                        {headerNav.map((nav, key) => (
                            <li key={key}>
                                <a href={nav.url}>{nav.title}</a>
                            </li>
                        ))}
                    </ul>
                </nav>
                <div
                    className="header__nav__mobile"
                    id="headerToggle"
                    aria-controls="primary-menu"
                    aria-expanded={show ? "true" : "false"}
                    role="button" 
                    tabIndex="0"
                    onClick={toggleMenu}
                >
                    <span></span>
                </div>
            </div>
        </header>
    );
};

export default Header;
  • const [show, setShow] = useState(false); : show라는 상태 변수를 생성하고 useState 훅으로 초기값을 false로 설정합니다. 이 변수는 모바일 메뉴의 표시 여부를 관리합니다. show가 true이면 모바일 메뉴가 나타나고, false이면 숨겨집니다.
  • const toggleMenu = () => { ... }: toggleMenu라는 함수를 정의합니다. 이 함수는 모바일 메뉴를 표시하거나 숨기기 위해 show 상태를 변경하는 역할을 합니다. setShow 함수를 사용하여 show 상태를 업데이트합니다.

3. 마무리

이제 모든 코딩 준비가 되었습니다. 그럼 깃에 올립시다.

git add .
git commit -m "🥳 헤더영역 완료"
git push -u origin main

처음 배우는 개념들이 있으면 혼란스러울 수 있습니다. 하지만 몇 번 따라하면서 익히면 재미가 있습니다. 포기하지 말고 끝까지 잘 따라해주시면 감사하겠습니다. 그럼 이번 리액트도 끝까지 완성해봅시다.!

😘 이런 에러가 난다면...

리액트에서는 class 대신에 className을 사용합니다. class를 썼는지 확인해봐야 합니다.

react-dom.development.js:86 Warning: Invalid DOM property `class`. Did you mean `className`?

리액트에서는 tabindex 대신에 tabIndex을 사용합니다. tabindex를 썼는지 확인해봐야 합니다.

Warning: Invalid DOM property `tabindex`. Did you mean `tabIndex`?

예제 목록

댓글