[코뮤니티 모각코] 웹 리액트 과정 - 3주차

51768 단어 React모각코React

2022-01-03 MON~2022-01-07 FRI의 기록 💻

📌 11일차 미디어쿼리

✅ 오늘의 문제 | 메뉴 없애기 ②

⭐ 핵심 <미디어 쿼리>

@media screen and (max-width: 600px) {
// screen의 width가 <= 600px 일 때
    .container{
        background-color: black;
    }
} 
@media screen and (min-width: 600px) and (max-width: 1000px) {
// screen의 width가 >= 600px && <=1000px 일 때
    .container {
        background-color: red;
    }
}
@media screen and (min-width: 1000px) {
// screen width가 >=1000px 일 때
    .container {
        background-color: blue;
    }
}

cf) https://developer.mozilla.org/ko/docs/Learn/CSS/CSS_layout/Media_queries

🔽 src/components/shared/Layout.js

import { useState } from 'react';
import styles from './Layout.module.css';
import Header from './Header';
import Menu from './Menu';

function Layout({ children, activeMenu }) {  
    const [isMenuOn, setIsMenuOn] = useState(true);

    function menuOnOff() {
        setIsMenuOn(!isMenuOn);
    }

    return (
        <div className={styles.container}>
            <Header menuOnOff={menuOnOff}/>
            <div className={styles.layout}>
                {isMenuOn ? <Menu activeMenu={activeMenu}/> : null}
                /* 삼항 조건 연산자로 isMenuOn 변수의 값에 따라 클래스이름 다르게 지정 */
                <div className={isMenuOn ? styles['contents-menu-on'] : styles['contents-menu-off']}>{children}</div>
                /***************************************************************/
            </div>
        </div>
    );
}

export default Layout;

🔽 src/components/shared/Layout.module.css

.container {
    display: flex;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: overlay;
    flex-direction: column;
}

.layout {
    display: flex;
    margin-top: 56px;
    flex: 1;
}

/* .contents 클래스 삭제 .contents-menu-on, .contents-menu-off 클래스 추가 및 각각의 css 지정*/
.contents-menu-on {
    margin-left: 240px;
    flex: 1;
}

.contents-menu-off { // 메뉴가 없을 때 
    margin-left: 0px; // 컨텐츠의 왼쪽 여백 삭제
    flex: 1;
}

@media screen and (max-width: 911px) {
    .contents-menu-on { // 적용 클래스이름을 .contents -> .contents-menu-on 로 변경
        margin-left: 72px;
    }
}
/********************************************************************************************/

📌 12일차 map

✅ 오늘의 문제 | 필터 변경하기

⭐ 핵심 <includes 함수 활용>

// A : 'car', B : 'car, bus'
if (B.includes(A)) {
	...
} // A문자열이 B 내부에 있는 경우 true 리턴

⭐ CSS 관련 추가 공부

👉 가상요소 선택자 ::before & ::after
  • 선택한 요소의 앞 또는 뒤에 content 추가 (content 속성 반드시 지정해줄 것!)
  • 가상클래스(:focus, :hover, :active, :visited)와 구분
// ::after example
.uploader::after,
.view::after {
    content: '·';
    margin: 0 4px;
}
👉 -webkit-line-clamp
  • 필수로 함께 지정해줄 속성들
    display: -webkit-box 또는 -webkit-inline-box
    -webkit-box-orient: vertical
    overflow: hidden // 미설정 시 말줄임표는 노출되나 넘친 콘텐츠가 숨겨지지 않음
// -webkit-line-clamp example
.title,
.desc {
    /* 2줄이 넘을 경우 ... 표시 */
    display: -webkit-box;
    overflow: hidden;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

cf) https://developer.mozilla.org/ko/docs/Web/CSS/-webkit-line-clamp

🔽 src/pages/Home.js

import styles from './Home.module.css';
import Layout from '../components/shared/Layout';
import youtubeData from '../data/youtubeData.json';
import HomeFilter from '../components/home/HomeFilter';
import HomeCard from '../components/home/HomeCard';
import { useState } from 'react';

const target = ["전체", "BTS", "LISA", "아이폰"]; // 필터링 할 문자열(검색어) 수정

function Home() {
    const [filter, setFilter] = useState("전체");

    function mapFunc(data, index) {
        return (
            <HomeFilter
                filter={filter}
                text={data}
                onClickFilter = {function() {
                    setFilter(data);
                }}
                key={`home-filter-${index}`}
            />
        );
    }

    function filterFunc(data) {
        if (filter === "전체" || data.title.includes(filter) || data.description.includes(filter)) {
        // filter가 "전체"이거나 영상의 제목 또는 설명에 filter가 포함된 경우
            return true;
        }
        return false;
    }

    return (
        <Layout activeMenu="home">
            <div className={styles.header}>
                {target.map(mapFunc)}
            </div>
            <div className={styles.container}>
                <div className={styles.grid}>
                    {youtubeData['data'].filter(filterFunc).map(HomeCard)}
                </div>
            </div>
        </Layout>
    );
}

export default Home;

📌 13일차 Home과 Filter

✅ 오늘의 문제 | input 태그 상태 관리

⭐ CSS 관련 추가 공부

👉 GRID layout
  • grid-template-rows(행의 배치), grid-template-columns(열의 배치)는 그리드의 형태를 정의
  • fr은 fraction으로 숫자의 비율대로 크기를 나눔
    ex) grid-template-columns: 1fr 1fr 1fr // 1:1:1 비율의 3개의 열 생성
  • repeat(반복횟수, 반복값)
    ex) repeat(5, 1fr) // 1fr 1fr 1fr 1fr 1fr
  • minmax 함수는 최솟값과 최댓값을 지정
    ex) minmax(100px, auto) // 최소 100px, 최대 자동으로 늘어나게 지정
  • auto-fill, auto-fit 개수를 미리 정하지 않고 설정된 너비가 허용되는 한 최대한 셀을 채움
    ex) // 한 행에 100%/20% = 5(개)의 셀이 들어감
    grid-template-columns: repeat(auto-fill, minmax(20%, auto) // auto-fill의 경우 셀 개수 < 5개일 경우 공간이 남음
    grid-template-columns: repeat(auto-fit, minmax(20%, auto) // auto-fit의 경우 셀 개수 < 5개 일 경우 자동으로 남은 공간을 채움
  • row-gap, column-gap은 그리드 셀 사이의 간격
// grid example
.grid {
    display: grid;
    width: 100%;
    row-gap: 40px;
    column-gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(300px, auto));
}

cf) https://studiomeal.com/archives/533

🔽 src/components/shared/Header.js

import styles from './Header.module.css';
import youtube_logo from '../../data/youtube_logo.png';
import { FiMenu } from 'react-icons/fi';
import { IoSearchOutline } from 'react-icons/io5';
import { BsGrid3X3Gap } from 'react-icons/bs';
import { HiOutlineDotsVertical } from 'react-icons/hi';
import { useState } from 'react';

function Header( {menuOnOff} ) {
    const [value, setValue] = useState("");

    function onChange(event) {
        setValue(event.target.value); // value 값을 event.target(input)의 value로 변경
    }

    function onClick() {
        console.log(value); // value 값을 출력
        setValue(""); // value 값 공백으로 초기화하여 input 비우기
    }

    return (
        <div className={styles.header}>
            <div className={styles.tab}>
                <FiMenu className={styles.icon} onClick={menuOnOff}/>
                <img src={youtube_logo} alt="로고" className={styles.logo} />
            </div>
            <div className={styles['center-tab']}>
                <input
                    className={styles.input}
                    onChange={onChange} // input의 값이 변할 때마다 event 객체가 onChange 함수에 전달
                    value={value}
                />
                <IoSearchOutline
                    className={styles['search-icon']}
                    onClick={onClick} // 검색 버튼에 onClick 함수 연동
                />
            </div>
            <div className={styles.tab}>
                <BsGrid3X3Gap className={styles.icon} />
                <HiOutlineDotsVertical className={styles.icon} />
            </div>
        </div>
    );
}

export default Header;

📌 14일차 Moment JS

✅ 오늘의 문제 | 시간 가공 함수 생성

⭐ 정답 예시 보충

  • ProcessUploadDate 함수의 return 부분 주석 참고

🔽 src/utils/index.js

// Moment JS 관련 라이브러리 import
import 'moment/locale/ko';
import moment from 'moment';

function ProcessViewCount(viewCount) {
    if (viewCount < 1000) {  // ex. 조회수 100회
        return `조회수 ${viewCount}`;
    }
    else if (viewCount < 10000) { // ex. 조회수 1.1천회
        return `조회수 ${(viewCount / 1000).toFixed(1)}천회`;
    }
    else if (viewCount < 100000) { // ex. 조회수 1.3만회
        return `조회수 ${(viewCount / 10000).toFixed(1)}만회`;
    }
    else if (viewCount < 100000000) { // ex. 조회수 103만회
        return `조회수 ${(viewCount / 10000).toFixed(0)}만회`;
    }
    else { // 조회수 3억회
        return `조회수 ${(viewCount / 100000000).toFixed(0)}억회`;
    }
}

function ProcessUploadDate(date) {
    const time = "2021-09-16T13:15:02"; // 기준이 되는 특정 시간 지정
    return `${moment(date).from(time)}`; // return moment(date).from(time) 처럼만 작성해도 됨
}

export { ProcessViewCount, ProcessUploadDate };

🔽 src/components/shared/HorizontalCard.js

import styles from './HorizontalCard.module.css';
import 'moment/locale/ko';
//import moment from 'moment';
import { ProcessUploadDate, ProcessViewCount } from '../../utils';

function HorizontalCard({ data }) {
    return (
        <a href={`https://www.youtube.com/watch?v=${data.id}`}>
            <div className={styles.card}>
                <img
                    className={styles.thumbnail}
                    src={data.thumbnail}
                    alt={`${data.title}의 썸네일`}
                />
                <div className={styles.info}>
                    <div className={styles.title}>{data.title}</div>
                    <div className={styles.meta}>
                        <a
                            href={`https://www.youtube.com/channel/${data.channelId}`}
                            className={styles.uploader}
                        >
                            {data.channelTitle}
                        </a>
                        <div className={styles.view}>{ProcessViewCount(data.viewCount)}</div>
                        <div className={styles.time}>{ProcessUploadDate(data.date)}</div> // 수정된 부분 
                    </div>
                    <div className={styles.desc}>{data.description}</div>
                </div>
            </div>
        </a>
    );
}

export default HorizontalCard;

📌 15일차 호스팅

✅ 오늘의 문제 | 호스팅 페이지 업로드

➕ 추가 기능 구현 <검색 기능>

  • HOME page

    • 검색 기능 추가
      -> setFilter 함수를 Home - Layout - Header 컴포넌트 간에 props 로 넘겨주고 넘겨준 setFilter 함수를 활용하여 검색어를 입력하면 filter 값이 해당 검색어로 바뀔 수 있도록 함
    • 검색 버튼을 클릭할 때 뿐만 아니라 검색어 입력 후 엔터 키를 눌렀을 때에도 검색이 이루어질 수 있도록 함
      -> Header 컴포넌트의 input 태그에 발생한 키 이벤트가 "Enter"일 시에 검색 버튼 클릭 시 실행되는 함수가 동일하게 실행될 수 있도록 onKeyPress 속성 추가
    • 기존의 Home 화면과 달리 검색 결과 화면은 Explore 컴포넌트의 레이아웃을 활용하여 필터링된 결과를 표시
      -> useState 활용하여 isSearchMode 변수(초기값은 false) 추가한 후 해당 변수의 참/거짓 값에 따라 Home 페이지를 다른 레이아웃으로 출력
    • Menu에서 [홈] 메뉴 클릭 시 원래의 Home 컴포넌트 레이아웃으로 화면 초기화
      -> setFilter, setIsSearchMode 함수를 Home - Layout - Menu 컴포넌트 간에 props 로 넘겨주고 넘겨준 함수들을 활용하여 각각 filter, isSearchMode 값 초기화
  • EXPLORE & SUBSCRIPTION page

    • 검색 기능 미지원
      -> activeMenu !== "home"일 경우 Header의 input 비활성화(disabled 속성)+검색 기능을 지원하지 않는다는 문구 출력(placeholder 속성)

🔽 깃허브 호스팅 페이지 바로가기

좋은 웹페이지 즐겨찾기