본문 바로가기
Tutorial/youtube2023

04. 나만의 유튜브 사이트 만들기 : 섹션 컴퍼넌트 만들기

by @webstoryboy 2023. 8. 30.
Tutorial/Portfolio

나만의 유튜브 사이트 만들기

by @webs 2023. 09. 01.
04
나만의 유튜브 사이트 만들기 : 섹션 컴퍼넌트 만들기
난이도 중간

소개

안녕하세요! 웹스토리보이입니다. 이번에는 CSS를 셋팅하겠습니다. 리액트에서 CSS를 사용하는 방법은 여러가지가 있습니다. 그 중 가장 기본적인 SCSS를 이용해서 작업해보겠습니다.

인덱스

  • 1. 셋팅하기
    • 1_1. Node.js 설치
    • 1_2. Vscode 설치
    • 1_3. React.js 설치
    • 1_4. 폴더 정리하기
    • 1_5. 필요한 파일 설치하기
  • 2. CSS 셋팅하기
    • 2_1. SCSS 설정하기
    • 2_2. style.scss 설정하기
    • 2_3. fonts.scss 설정하기
    • 2_4. vars.scss 설정하기
    • 2_5. reset.scss 설정하기
    • 2_6. mixin.scss 설정하기
    • 2_7. common.scss 설정하기
  • 3. 페이지 및 컴퍼넌트 만들기
    • 3_1. 페이지 만들기
    • 3_2. 컴퍼넌트 만들기
  • 4. 섹션 컴퍼넌트 만들기
    • 4_1. Main 컨퍼넌트 만들기
    • 4_2. Header 컨퍼넌트 만들기
    • 4_3. Header 컨퍼넌트 데이터화 하기
    • 4_4. Footer 컨퍼넌트 만들기

4. 섹션 컴퍼넌트 만들기

페이지를 만들었으니 각 페이지의 섹션을 만들겠습니다. 리액트에서는 컴퍼넌트라고 얘기하죠! 우리가 만들 사이트는 크게 header, main, footer로 나누어 집니다. 그리고 모든 페이지에 다 들어가기 때문에 App.js 페이지에서 셋팅을 해주겠습니다.

import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom';

import Home from './pages/Home'
import Today from './pages/Today'
import Developer from './pages/Developer'
import Webd from './pages/Webd'
import Website from './pages/Website'
import Gsap from './pages/Gsap'
import Port from './pages/Port'
import Youtube from './pages/Youtube'
import Channel from './pages/Channel'
import Video from './pages/Video'
import Search from './pages/Search'
import Not from './pages/Not'

import Header from './components/section/Header';
import Main from './components/section/Main';
import Footer from './components/section/Footer';

const App = () => {
    return (
        <BrowserRouter>
            <Header />
            <Main>
                <Routes>
                    <Route path='/' element={<Home />} />
                    <Route path="/today" element={<Today />} />
                    <Route path="/developer" element={<Developer />} />
                    <Route path="/webd" element={<Webd />} />
                    <Route path="/website" element={<Website />} />
                    <Route path="/gsap" element={<Gsap />} />
                    <Route path="/port" element={<Port />} />
                    <Route path="/youtube" element={<Youtube />} />
                    <Route path='/channel/:channelId' element={<Channel />} />
                    <Route path='/video/:videoId' element={<Video />} />
                    <Route path='/search/:searchId' element={<Search />} />
                    <Route path="*" element={<Not />} />
                </Routes>
            </Main>
            <Footer />
        </BrowserRouter>
    );
}

export default App;

기존 파일에 컴퍼넌트를 추가했습니다. 폴더는 다음과 같이 설정하겠습니다.

  • src
    • assets
    • components
      • section
        • Footer.jsx
        • Header.jsx
        • Main.jsx
    • pages
      • Channel.jsx
      • Developer.jsx
      • Gsap.jsx
      • Home.jsx
      • Not.jsx
      • Port.jsx
      • Search.jsx
      • Today.jsx
      • Video.jsx
      • Webd.jsx
      • Website.jsx
      • Youtube.jsx
    • App.js
    • index.js

CSS도 다음과 같이 설정하겠습니다.

  • assets
    • fonts
    • img
    • scss
      • section
        • _footer.scss
        • _header.scss
      • setting
        • _common.scss
        • _fonts.scss
        • _mixin.scss
        • _reset.scss
        • _vars.scss
      • style.scss

style.scss도 다음과 같이 수정합니다.

@charset "UTF-8";

// setting
@import "setting/fonts";
@import "setting/vars";
@import "setting/reset";
@import "setting/mixin";
@import "setting/common";

// section
@import "section/header";
@import "section/footer";

index.js 페이지에서 scss를 연동합니다.

import './assets/scss/style.scss';

4_1. Main 컨퍼넌트 만들기

터미널에 npm start 라고 작성하면 페이지 에러가 날 것입니다. Main 부터 완성을 하면 페이지 에러가 없어질 것입니다.

이 컴퍼넌트는 주어진 자식 컴포넌트(children)를 받아서 페이지의 주요 콘텐츠를 감싸는 역할을 합니다. 이 컴포넌트를 사용하면 다른 컴포넌트를 Main 컴포넌트 내부에 자식으로 넣어 페이지의 주요 콘텐츠 영역을 감쌀 수 있습니다. 이를 통해 페이지 구조화 및 스타일링을 효율적으로 관리할 수 있습니다.

import React from 'react'

const MainSection = ({ children }) => {
    return (
        <main id="main" role="main">
            { children }
        </main>
    )
}

export default MainSection
  • import React from 'react': 리액트 라이브러리를 불러옵니다.
  • const Main = ({ children }) => { ... }: Main이라는 이름의 함수형 컴포넌트를 정의합니다. 이 컴포넌트는 { children }을 인자로 받습니다. 이것은 컴포넌트에 전달된 자식 컴포넌트를 나타냅니다.
  • return ( ... ): 컴포넌트의 반환 값으로 JSX를 사용하여 UI를 작성합니다.
  • <main id="main" role="main">: main 태그를 생성합니다. 이 태그는 HTML5의 시맨틱 태그 중 하나로, 문서의 주요 콘텐츠를 나타냅니다. id 속성을 통해 CSS 스타일링이나 JavaScript 조작에 사용할 수 있습니다. role 속성은 웹 접근성을 고려하여 태그의 역할을 지정하는 데 사용됩니다.
  • { children }: 이 부분은 JSX 내부에서 중괄호 {}로 감싸진 변수나 표현식을 나타내며, 여기서는 부모 컴포넌트로부터 받은 children을 렌더링합니다. 이 컴포넌트는 자식 컴포넌트가 들어갈 자리입니다.
  • </main>: main 태그를 닫아주는 부분입니다.
  • export default Main: 이 컴포넌트를 외부에서 사용할 수 있도록 내보냅니다.

4_2. HeaderSection 컨퍼넌트 만들기

header 영역은 다음과 같이 코딩을 하겠습니다. 리액트에서는 class 대신에 className을 사용합니다. 이런 부분은 경고 표시 또는 에러 표시로 나오기 때문에 크게 걱정하지 않아도 됩니다. 또한 a 링크 대신에 Link를 사용하였습니다. 페이지가 싱글 페이지이기 때문에 이런 부분을 이렇게 처리해야 합니다.

https://react-icons.github.io/react-icons 사이트에 아이콘을 이용하여 설정하였습니다. 사용할 아이콘의 이름을 import 하고 써주면 됩니다.

import React from 'react'

import { CiBaseball } from "react-icons/ci";
import { CiCoins1 } from "react-icons/ci";
import { CiBoxes } from "react-icons/ci";
import { CiBullhorn } from "react-icons/ci";
import { CiCoffeeCup } from "react-icons/ci";
import { CiDumbbell } from "react-icons/ci";
import { CiFries } from "react-icons/ci";
import { CiMoneyBill } from "react-icons/ci";

import { AiFillGithub } from "react-icons/ai";
import { AiOutlineCodepen } from "react-icons/ai";
import { AiFillYoutube } from "react-icons/ai";
import { AiOutlineInstagram } from "react-icons/ai";

const Header = () => {
    return (
        <header id='header' role='banner'>
            <h1 className='header__logo'>
                <a href="/">
                    <em></em>  
                    <span>webs<br />youtube</span>
                </a>
            </h1>
            <div className='header__cate'>
                <ul>
                    <li><a href="/"><CiBaseball /> 웹스토리보이</a></li>
                    <li><a href="/today"><CiMoneyBill /> 추천 영상</a></li>
                    <li><a href="/developer"><CiCoins1 /> 추천 개발자</a></li>
                    <li><a href="/webd"><CiBoxes /> 웹디자인기능사</a></li>
                    <li><a href="/website"><CiBullhorn /> 웹표준 사이트</a></li>
                    <li><a href="/gsap"><CiCoffeeCup /> GSAP Parallax</a></li>
                    <li><a href="/port"><CiDumbbell /> 포트폴리오 사이트</a></li>
                    <li><a href="/youtube"><CiFries /> 유튜브 클론 사이트</a></li>
                </ul>
                <ul className='keyword'>
                    <li><a href="/search/webstoryboy">webstoryboy</a></li>
                    <li><a href="/search/html">HTML</a></li>
                    <li><a href="/search/css">CSS</a></li>
                    <li><a href="/search/javascript">JavaScript</a></li>
                    <li><a href="/search/react.js">React.js</a></li>
                    <li><a href="/search/vue.js">Vue.js</a></li>
                    <li><a href="/search/next.js">Next.js</a></li>
                    <li><a href="/search/node.js">Node.js</a></li>
                    <li><a href="/search/sql">SQL</a></li>
                    <li><a href="/search/React Portfolio">portfolio</a></li>
                    <li><a href="/search/NewJeans">music</a></li>
                </ul>
            </div>
            <div className='header__sns'>
                <ul>
                    <li>
                        <a href="https://github.com/webstoryboy" rel="noopener noreferrer">
                            <AiFillGithub />
                        </a>
                    </li>
                    <li>
                        <a href="https://www.youtube.com/webstoryboy" rel="noopener noreferrer">
                            <AiFillYoutube />
                        </a>
                    </li>
                    <li>
                        <a href="https://codepen.io/webstoryboy" rel="noopener noreferrer">
                            <AiOutlineCodepen />
                        </a>
                    </li>
                    <li>
                        <a href="https://www.instagram.com/webstoryboy" rel="noopener noreferrer">
                            <AiOutlineInstagram />
                        </a>
                    </li>
                </ul>
            </div>
        </header>
    )
}

export default Header

리액트에서의 Link는 React Router 라이브러리에서 제공하는 컴포넌트로, 페이지 간의 내부 네비게이션을 위해 사용됩니다. 기존의 a 태그와는 달리 페이지를 새로고침하지 않고도 라우팅을 처리할 수 있습니다. 이는 SPA(단일 페이지 애플리케이션)의 일부분이며, 사용자 경험을 향상시키는 역할을 합니다.

rel="noopener noreferrer"는 <a> 태그의 rel 속성에 사용되는 값들로, 웹 페이지의 보안 및 사용성을 향상시키기 위해 사용됩니다. rel="noopener noreferrer"를 사용하면 새 탭/윈도우에서 링크를 열 때 보안 및 개인 정보 보호 측면에서 안전한 방식으로 열리게 됩니다.

  • noopener: 이 값을 사용하면 링크를 새 탭/윈도우에서 열 때, 새로 열리는 페이지가 부모 페이지의 window.opener 속성을 통해 부모 페이지를 제어하는 것을 방지합니다. 이렇게 함으로써 다른 사이트에서 자신의 페이지를 조작하는 보안 취약점을 방지할 수 있습니다.
  • noreferrer: 이 값을 사용하면 링크를 클릭할 때 HTTP 리퍼러(Referer) 헤더가 전송되지 않습니다. 이는 사용자의 개인 정보를 보호하고 어떤 사이트로부터 연결되었는지 등을 감추는 데 도움이 됩니다.

scss는 다음과 같이 작성합니다.

.header__logo {
    text-align: center;
    border-bottom: 1px solid #161616;
    text-transform: uppercase;

    a {
        display: flex;
        padding: 10px;
        margin: 10px;

        em {
            width: 40px;
            height: 40px;
            display: block;
            background-color: red;
            margin-right: 10px;
        }

        span {
            text-align: left;
            font-size: 20px;
            line-height: 1;
            font-weight: 900;
        }
    }
}

.header__cate {
    padding: 10px 0;

    ul {
        margin-bottom: 10px;
        
        li {
            a {
                color: #ebebeb;
                font-size: 1rem;
                display: block;
                position: relative;
                padding: 0.9rem 2rem 0.9rem 50px;
                line-height: 1;
                margin: 0 0.625rem;
                border-radius: 40px;
                transition: background-color 0.3s;
                
                svg {
                    width: 1.25rem;
                    height: 1.25rem;
                    position: absolute;
                    left: 1.25rem;
                    top: 0.75rem;
                }
                
                &:hover {
                    background-color: #202020;
                    color: #fff;
                }
            }
            
            &.active a {
                background-color: #202020;
                color: #fff;
            }
        }
    }
    .keyword {
        border-top: 1px solid #161616;
        padding: 1.25rem;
        
        li {
            display: inline-block;
            
            a {
                display: inline-block;
                padding: 7px 15px;
                margin: 3px 1px;
                border: 1px solid #202020;
                border-radius: 40px;
                font-size: 0.8rem;
            }
        }
    }
}
.header__sns {
    border-top: 1px solid #161616;
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    text-align: center;
    padding: 15px 0 10px;
    background-color: #ffffff0b;
    backdrop-filter: blur(10px);

    li {
        display: inline-block;
        
        a {
            padding: 5px 7px;
            display: inline-block;
        }
    }
}

4_3. Header 컨퍼넌트 데이터화 하기

헤더의 데이터를 따로 변수로 만들어서 처리하겠습니다. 핵심 데이터만 따로 빼오고 반복되는 부분은 map 메서드를 이용하여 작업했습니다. 이런식으로 하면 소스보기가 좋고, 관리하기가 좋겠죠! 이런 데이터도 많아질거니 외부 파일로 따로 처리하도록 하겠습니다.

데이터 파일은 폴더를 따로 만들고 작업하겠습니다.

  • src
    • assets
    • components
    • data
      • category.js
    • pages
    • App.js
    • index.js

category.js 파일은 다음과 같이 정리하겠습니다.

import { CiBaseball } from "react-icons/ci";
import { CiCoins1 } from "react-icons/ci";
import { CiBoxes } from "react-icons/ci";
import { CiBullhorn } from "react-icons/ci";
import { CiCoffeeCup } from "react-icons/ci";
import { CiDumbbell } from "react-icons/ci";
import { CiFries } from "react-icons/ci";
import { CiMoneyBill } from "react-icons/ci";

import { AiFillGithub } from "react-icons/ai";
import { AiOutlineCodepen } from "react-icons/ai";
import { AiFillYoutube } from "react-icons/ai";
import { AiOutlineInstagram } from "react-icons/ai";

export const categories = [
    {
        title: "웹스토리보이",
        icon: <CiBaseball />,
        src: "/"
    },
    {
        title: "추천 영상",
        icon: <CiMoneyBill />,
        src: "/today"
    },
    {
        title: "추천 개발자",
        icon: <CiCoins1 />,
        src: "/developer"
    },
    {
        title: "웹디자인기능사",
        icon: <CiBoxes />,
        src: "/webd"
    },
    {
        title: "웹표준 사이트",
        icon: <CiBullhorn />,
        src: "/website"
    },
    {
        title: "GSAP Parallax",
        icon: <CiCoffeeCup />,
        src: "/gsap"
    },
    {
        title: "포트폴리오 사이트",
        icon: <CiDumbbell />,
        src: "/port"
    },
    {
        title: "유튜브 클론 사이트",
        icon: <CiFries />,
        src: "/youtube"
    },
];

export const searchCate = [
    {
        title: "webstoryboy",
        src: "/search/webstoryboy"
    },
    {
        title: "HTML",
        src: "/search/html"
    },
    {
        title: "CSS",
        src: "/search/css"
    },
    {
        title: "JavaScript",
        src: "/search/javascript"
    },
    {
        title: "React.js",
        src: "/search/react.js"
    },
    {
        title: "Vue.js",
        src: "/search/vue.js"
    },
    {
        title: "Next.js",
        src: "/search/next.js"
    },
    {
        title: "Node.js",
        src: "/search/node.js"
    },
    {
        title: "SQL",
        src: "/search/sql"
    },
    {
        title: "portfolio",
        src: "/search/React Portfolio"
    },
    {
        title: "music",
        src: "/search/NewJeans"
    }
];

export const snsCate = [
    {
        title: "github",
        url: "https://github.com/webstoryboy",
        icon: <AiFillGithub />
    },
    {
        title: "youtube",
        url: "https://www.youtube.com/webstoryboy",
        icon: <AiFillYoutube />
    },
    {
        title: "codepen",
        url: "https://codepen.io/webstoryboy",
        icon: <AiOutlineCodepen />
    },
    {
        title: "instagram",
        url: "https://www.instagram.com/webstoryboy",
        icon: <AiOutlineInstagram />
    },
]

Header.jsx 파일은 다음과 같이 다시 정리하겠습니다.

import React from 'react'

import { categories, searchCate, snsCate } from "../../data/category";
import { Link } from 'react-router-dom';

const Header = () => {
    return (
        <header id='header' role='banner'>
            <h1 className='header__logo'>
                <a href="/">
                    <em></em>  
                    <span>webs<br />youtube</span>
                </a>
            </h1>
            <div className='header__cate'>
                <ul>
                    {categories.map((cate, key) => (
                        <li key={key}>
                            <Link to={cate.src}>
                                {cate.icon}{cate.title}
                            </Link>
                        </li>
                    ))}
                </ul>
                <ul className='keyword'>
                    {searchCate.map((cate, key) => (
                        <li key={key}>
                            <Link to={cate.src}>
                                {cate.title}
                            </Link>
                        </li>
                    ))}
                </ul>
            </div>
            <div className='header__sns'>
                <ul>
                    {snsCate.map((sns, key) => (
                        <li key={key}>
                            <a href={sns.url} target="_blank" rel="noopener noreferrer" aria-label={sns.title}>
                                <span>{sns.icon}</span>
                            </a>
                        </li>
                    ))}
                </ul>
            </div>
        </header>
    )
}

export default Header

4_4. FooterSection 컨퍼넌트 만들기

푸터 영역은 아이콘을 사용하겠습니다. 아이콘은 초반에 react-icon을 설치하였기 때문에 바로 사용하면 됩니다 https://react-icons.github.io/react-icons 여기에서 아이콘 모양을 확인 할 수 있습니다.

원하는 아이콘의 이름을 복사한 후 해당 이름에 맞추어 import 시키면 됩니다.

import React from 'react'

const Footer = () => {
    return (
        <footer id='footer' role="contentinfo"> 
            <a href="mailto:webstoryboy@naver.com" rel="noopener noreferrer">webstoryboy@naver.com</a>
        </footer>
    )
}

export default Footer

role은 HTML 요소의 역할을 명시하는 데 사용되는 속성으로, 웹 접근성을 향상시키는 데 도움을 주는 중요한 개념입니다. role 속성을 사용하면 스크린 리더 등의 보조 기술을 사용하는 사용자에게 웹 페이지의 구조와 콘텐츠의 의미를 더 명확하게 전달할 수 있습니다. role 속성을 사용하면 해당 요소가 어떤 역할을 하는지를 지정하며, 이는 시각적으로 보이지 않는 것과는 달리 스크린 리더 사용자에게 중요한 정보를 제공하는 역할을 합니다.

#footer {
    margin-top: 100px;
    background-color: #111;
    border-top: 1px solid #161616;
    font-size: 0.8rem;
    text-align: center;
    color: #666;
    padding: 20px 20px 20px 260px;

    a {
        padding: 10px 20px;
    }
}

@media (max-width: 800px){
    #footer {
        padding-left: 0;
    }
}

마무리

모든 페이지에 반복되는 헤더, 메인, 푸터 섹션을 정리하였습니다. 리액트 아이콘 사이트를 통해 만들어지 아이콘을 쉽게 가져 올 수 있었고, 변수도 외부 파일로 처리하여 쉽게 관리할 수 있도록 했습니다. 아직 데이터를 가져올 때 반복처리하는 부분이 미흡하지만 이건 다음 시간에 공부해보도록 하겠습니다. 그럼 오늘도 수고하셨습니다. 😆


예제 목록

댓글