Day 01 - 220511
- 환경세팅
- 디렉터리 구성
- 기본 컴포넌트 (App, index) 작성
- 라우트 설정 (react-router-dom)
- 컴포넌트 생성
$ yarn create react-app yeonflix
$ cd yeonflix
$ yarn start
$ 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
- react-router-dom
- RecoilRoot로 감싸기
- routes에 있는 컴포넌트 불러와서 Route 컴포넌트의 element로 집어넣기
- 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
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>
* {
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)
font-family
랑 background
가 잘 적용됨.recoil
라이브러리의 atom
을 생성함.// 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
const Load = () => {
return (
<div>
<h1>Loading ...</h1>
</div>
);
};
export default Load;
* * *
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;
<Route path='/' element={<Home />} />
에 의해 Home 컴포넌트가 렌더링 됨.<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);
search
(state)가 달라진다.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;
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); });
translateX
의 값으로 들어갈 state.🔺 자세한 내용은 아래 슬라이드원리 참조하기.
<div
className="slide"
style={{ transform: `translateX(${trans}px)` }}
>
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);
};
async 함수는 useEffect 내에서 쓸 수 없으므로, 따로 선언해서 호출해줘야 함.
useEffect(() => {
setLoading(true);
getMovies();
setLoading(false);
}, []);
// 최초 한번만 API를 불러옴