YeonFlix day 01

thisisyjin·2022년 5월 11일
0

Dev Log 🐥

목록 보기
5/23
post-thumbnail

Day 01 - 220511

  • 환경세팅
  • 디렉터리 구성
  • 기본 컴포넌트 (App, index) 작성
  • 라우트 설정 (react-router-dom)
  • 컴포넌트 생성

📝 기획안 보기
🎨 디자인 보기


환경 세팅

  1. create-react-app
$ yarn create react-app yeonflix
  1. yarn start
$ cd yeonflix
$ yarn start
  1. yarn add (필요 라이브러리 설치)
$ yarn add react-router-dom
$ yarn add recoil
$ yarn add react-icons

디렉터리 구성

├── public
└── src
    ├── components
    │   ├── Load
    │   ├── Navbar
    │   └── Slide
    ├── routes
    │   ├── Detail
    │   ├── Group
    │   ├── Home
    │   └── Search
    ├── App
    └── index
  • 추후 Movie 관련 컴포넌트 추가 예정. (Detail, Group, Search, Slide)

라우트 설정

App.js

  1. react-router-dom
  2. RecoilRoot로 감싸기
  3. routes에 있는 컴포넌트 불러와서 Route 컴포넌트의 element로 집어넣기
  4. Router (=BrowserRouter) basename 설정
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import Home from './routes/Home';
import Group from './routes/Group';
import Detail from './routes/Detail';
import Search from './routes/Search';
import Navbar from './components/Navbar';

const App = () => {
  return (
    <RecoilRoot>
      <Router basename={process.env.PUBLIC_URL}>
        <Navbar />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/page/:group/:page" element={<Group />} />
          <Route path="/movie/:id" element={<Detail />} />
          <Route path="/search/:search" element={<Search />} />
        </Routes>
      </Router>
    </RecoilRoot>
  );
};

export default App;

기본 컴포넌트 작성

index.js

styles.css

  • Yeonflix 파비콘 제작
    (figma 로 16*16px 크기의 svg 파일 생성 후, 컨버터 프로그램으로 ico 파일로 변환했음.)

  • 적용 결과

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
<title>YeonFlix</title>
<!--  Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Secular+One&display=swap"
      rel="stylesheet"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
</body>
</html>

styles.css

  • index.js에 임포트할 style.css 생성
* {
  font-family: 'Secular One', sans-serif;
  margin: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Secular One', sans-serif;
  height: 100%;
  background-image: linear-gradient(
    to right,
    rgba(237, 193, 218, 1) 0%,
    rgba(136, 124, 211, 1) 50%,
    rgba(117, 174, 242, 1) 100%
  );
}

a {
  font-family: 'Secular One', sans-serif;
  text-decoration: none;
}

a:link {
  color: #000;
  text-decoration: none;
}
a:visited {
  color: #000;
  text-decoration: none;
}
a:active {
  color: #ff0000;
}
![](https://velog.velcdn.com/images/thisisyjin/post/7cf9b323-91f9-45fd-9185-1d6e99583d23/image.png)

test

  • 일반 텍스트, a태그, button 등 모든 요소에서 font-familybackground가 잘 적용됨.

Atom 생성

  • recoil 라이브러리의 atom을 생성함.
  • src 디렉터리에 atom 디렉터리를 생성하고, NavLink.js 라는 이름으로 파일 저장.
// prettier-ignore
const Group_obj = { "High Rating": "minimum_rating=8", "Romance": "genre=romance", "Music": "genre=music", "Animation": "genre=animation" };
const Group_key_arr = Object.keys(Group_obj);

export { Group_obj, Group_key_arr };roup_key_arr = Object.keys(Group_obj);

export { Group_obj, Group_key_arr };

✅ 참고 - Object.keys();

✅ 참고 2 - prettier ignore 주석

  • 아랫줄의 prettier 적용 무시 가능.
// prettier-ignore

컴포넌트 생성

1) Load.js

const Load = () => {
  return (
    <div>
      <h1>Loading ...</h1>
    </div>
  );
};

export default Load;
* * *

2) NavBar.js

import { Link } from 'react-router-dom';
import { MdSearch } from 'react-icons/md';
import { useState } from 'react';
import { Group_key_arr, Group_obj } from '../atom/NavList';

const Navbar = () => {
  const [search, setSearch] = useState('');

  const onChangeSearch = (e) => {
    const { target: value } = e;
    setSearch(value);
  };

  return (
    <div>
      {/*  Page Name */}
      <div>
        <Link to="/">YeonFlix</Link>
      </div>

      {/* Group Links */}
      <div>
        {Group_key_arr.map((groupName) => {
          return (
            <div key={groupName}>
              <div>
                <Link to={`/page/${Group_obj[groupName]}/1`}>{groupName}</Link>
              </div>
            </div>
          );
        })}
      </div>

      {/* Search Bar */}
      <div>
        <div>
          <form>
            <input
              type="text"
              placeholder="Search Movie"
              value={search}
              onChange={onChangeSearch}
            />
            <Link to={`/serach/${search}`}>
              <button>
                <MdSearch />
              </button>
            </Link>
          </form>
        </div>
      </div>
    </div>
  );
};

export default Navbar;
  • YeonFlix 로고에 해당함. 클릭시 홈으로 이동함.
    -> Link 컴포넌트에 to 프로퍼티를 '/'로 주면 됨. (어느 페이지에 있든 /로 이동함.)
    -> App.js에서 라우팅 한 결과에 의해 <Route path='/' element={<Home />} /> 에 의해 Home 컴포넌트가 렌더링 됨.

2. NavList(atom) 값 사용

  • 우선, atom(NavList)에서 Group_key_arr와 Group_obj를 가져온다.
  • Group_key_arr 배열을 map 함수를 이용하여 각각을 Link 컴포넌트로 렌더링한다.
  • Link 컴포넌트의 to 프로퍼티의 값으로는 아래와 같이 템플릿 리터럴을 활용.
<Link to={`/page/${Group_obj[groupName]}/1`}>{groupName}</Link>

Group_obj[groupName]의 값은 아래 객체 구조를 참고하면,
genre=romance와 같이 key-value쌍을 가진다.

-> 아직은 안했지만, 추후에 API에서 특정 데이터만 필터링하여 가져올 때 사용한다.

const Group_obj = { "High Rating": "minimum_rating=8", "Romance": "genre=romance", "Music": "genre=music", "Animation": "genre=animation" };
const Group_key_arr = Object.keys(Group_obj);

3. Search Bar 구현

  • Search Bar 구현도 마찬가지로 Link 컴포넌트의 to 프로퍼티를 동적으로 설정하는 방식을 이용함.
  • 참고로, input이 바뀔때마다 (onChange) setSearch에 의해 search(state)가 달라진다.
    -> Button 클릭시 (버튼에는 React-icon으로 돋보기 모양 svg를 넣어줬다.)
    -> /search/search(state)로 이동하게 함.
* * *

3) Slide.js

import { useEffect, useState } from 'react';
import MovieSlide from './MovieSlide';
import { MdArrowBackIos, MdArrowForwardIos } from 'react-icons/md';
import Load from '../components/Load';

// Home
const Slide = ({ movieApi }) => {
  const [loading, setLoading] = useState(true);
  const [movies, setMovies] = useState([]);
  const [trans, setTrans] = useState(0);

  const onClickLeft = () => {
    if (trans >= 0) {
      // trans가 0 이상이면
      return;
    }
    setTrans((prevTrans) => prevTrans + 460);
    // 한 영화 슬라이드의 width가 460px임
  };

  const onClickRight = () => {
    if (trans <= -1380) {
      // 460 * 3 = 1380
      return;
    }
    setTrans((prevTrans) => prevTrans - 460);
  };

  const getMovies = async () => {
    const json = await (await fetch(movieApi)).json();
    setMovies(json.data.movies);
  };

  useEffect(() => {
    setLoading(true);
    getMovies();
    setLoading(false);
  }, []);
  // 최초 한번만 API를 불러옴

  return (
    <div className="container">
      <div className="slides">
        {loading ? (
          <Load />
        ) : (
          <div
            className="slide"
            style={{ transform: `translateX(${trans}px)` }}
          >
            {movies.map((movie) => {
              if (movie.medium_cover_image) {
                return (
                  <MovieSlide
                    key={movie.id}
                    id={movie.id}
                    coverImg={movie.medium_cover_image}
                    rating={movie.rating}
                    runtime={movie.runtime}
                    genres={movie.genres}
                  />
                );
              }
            })}
          </div>
        )}
      </div>

      {/* Left, Right 버튼 */}
      {loading && (
        <div>
          <button onClick={onClickLeft}>
            <MdArrowBackIos />
          </button>
          <button onClick={onClickRight}>
            <MdArrowForwardIos />
          </button>
        </div>
      )}
    </div>
  );
};

export default Slide;

1. state 설정

  • loading
    -> fetch 전후로 true/false로 바뀌도록.
    (fetch완료시 false)

  • movies
    -> json.data.movies를 저장함.
    -> json(=response)의 data 필드에는 데이터가 담겨있음.

❗️ 참고 - 이전에 했던 axios.get을 이용한 방법으로도 해보자.

axios.get('https://jsonplaceholder.typicode.com/todos/1').then(response => {
      setData(response.data);
    });
  • trans
    -> translateX의 값으로 들어갈 state.
    -> 첫 슬라이드는 0이고, 다음 슬라이드로 넘길수록 -값이 되어야 함.

🔺 자세한 내용은 아래 슬라이드원리 참조하기.

<div
    className="slide"
    style={{ transform: `translateX(${trans}px)` }}
>

2. onClickLeft, onClickRight 로직

 const onClickLeft = () => {
    if (trans >= 0) {
      // trans가 0 이상이면
      return;
    }
    setTrans((prevTrans) => prevTrans + 460);
    // 한 영화 슬라이드의 width가 460px임
  };

  const onClickRight = () => {
    if (trans <= -1380) {
      // 460 * 3 = 1380
      return;
    }
    setTrans((prevTrans) => prevTrans - 460);
  };
  • 맨 첫 슬라이드(trans >=0) 이면 left버튼 클릭해도 더이상 X
  • 맨 마지막 슬라이드 (trans <= -1380) 이면 right 버튼 클릭해도 더이상 X
  • left 클릭시 왼쪽으로 가야하므로 inner은 오른쪽으로 움직여야 함.
    -> translate + 460 해줌.
  • right 클릭시 반대이므로, translate - 460 해줌.

3. async/await과 fetch

const getMovies = async () => {
    const json = await (await fetch(movieApi)).json();
    setMovies(json.data.movies);
  };

async 함수는 useEffect 내에서 쓸 수 없으므로, 따로 선언해서 호출해줘야 함.

4. useEffect

  • 단 한번만 실행되도록 하기 위해 deps를 빈 배열로 함.
  useEffect(() => {
    setLoading(true);
    getMovies();
    setLoading(false);
  }, []);
  // 최초 한번만 API를 불러옴

💡 슬라이드 원리 - translateX

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글