-
320x100
이 글은 NomadCoders의 <ReactJS로 영화 웹 서비스 만들기> 강의 내용을 정리한 글입니다.
넘나 좋은 강의 (게다가 무료!) 🙇🏻♀️ 감사합니다 🙇🏻♀️
개인 스터디 글로, 맞지 않는 내용이나 더 나은 방법을 공유해 주신다면 복받으실 거예요 👼🙌
1) 시작하기
2) 기능 연습 & 3) 앱 만들기 ☀︎
4) styled-components
5) 𝒘𝒊𝒕𝒉 타입스크립트
2. 연습하기
2-1. To-do App
지금까지 배운 걸 활용해 간단한 투두리스트를 만들어 봅니다!
우선 투두를 입력할 인풋이 있어야겠죠!
/* App.js */ function App() { return ( <div> <input type="text" placeholder="Write your to do..." /> </div> ); } export default App;
이 인풋의 value는 state로 관리하려고 합니다. useState를 쓰윽 써줄 차례네요.
const [todo, setTodo] = useState('');
그리고 인풋에 변화가 일어나면(onChange) 인풋의 value를 받아와 state로 설정합니다.
const onChange = e => setToDo(e.target.value);
/* App.js */ import { useState } from 'react'; function App() { const [toDo, setToDo] = useState(''); const onChange = (e) => setTodo(e.target.value); return ( <div> <input type="text" placeholder="Write your to do..." value={toDo} onChange={onChange} /> </div> ); } export default App;
이제 이 input을 form안에 넣고 button도 추가해 줍니다.
그리고 버튼 클릭 시 입력된 값이 찍히도록 합니다.
버튼의 기본 타입은 submit이기 때문에, form 내의 버튼 클릭 시 submit 이벤트가 발생합니다.
그러니 form에 onSubmit 이벤트 핸들러를 걸어주면 됩니다.
/* App.js */ import { useState } from 'react'; function App() { const [toDo, setToDo] = useState(''); const onChange = (e) => setToDo(e.target.value); const onSubmit = (e) => { e.preventDefault(); console.log(toDo); }; return ( <div> <h1>My To Dos</h1> <form onSubmit={onSubmit}> <input type="text" placeholder="Write your to do..." value={toDo} onChange={onChange} /> <button>Add To Do</button> </form> </div> ); } export default App;
이제 입력된 값을 toDos라는 곳에 차곡차곡 쌓아보죠!
toDos도 state로 만들어야겠죠. 초기값은 빈 배열로 해줍시다.
const [toDos, setTodos] = useState([]);
이때 배열인 state를 변경하려면 어떻게 해야 할까요? push() 같은 메서드를 써야할까요?
놉, 우리는 state를 직접 건드릴 수 없습니다.
그래서 우린 setTodos()를 쓸 건데, 이때 새롭게 변경된 배열을 넘겨주면 됩니다.
빈 배열인 상태에서 ‘아침 먹기’란 할 일을 추가했다고 해봅시다.
그럼 [’아침 먹기’]를 넘겨주면 됩니다.
여기에 ‘점심 먹기’를 추가하면 [’아침 먹기’, ‘점심 먹기’] 를 넘기고,
‘저녁 먹기’까지 추가하면 [’아침 먹기’, ‘점심 먹기’, ‘저녁 먹기’] 를 넘겨주면 됩니다.
여기서 써먹을 수 있는 게 바로 spread 문법입니다.
이미 존재하는 배열을 일부 가지고 새로운 배열을 생성할 수 있습니다.
const list = ['딸기', '포도']; const newList = ['바나나', ...list]; //['바나나', '딸기', '포도']
투두리스트의 경우 기존의 할 일 배열에 하나씩 추가하는 형태이고,
가장 마지막에 추가한 일이 배열의 첫 번째로 보여지길 원하니 아래와 같이 써먹을 수 있겠네요.
const oldToDo = ['3번째 일', '2번째 일', '1번째 일']; const newToDo = '새로운 일'; const myTodo = [newToDo, ...oldToDo]; //['새로운 일', '3번째 일', '2번째 일', '1번째 일']
항시 최신의 값을 가져오려면 함수를 쓰는 게 안전하다고 했으니 함수 형태로 써봅시다!
setToDos((toDos) => [toDo, ...toDos]);
함수에 들어가는 첫 번째 인자 = 현재 state
이 함수가 리턴한 값 = 새로운 state클릭 시 setTodo('')로 인풋이 자동으로 비워지도록 하고, 빈 값일 땐 submit되지 않도록 기능도 추가해 줍시다.
/* App.js */ import { useState } from 'react'; function App() { const [toDo, setToDo] = useState(''); const [toDos, setToDos] = useState([]); const onChange = (e) => setToDo(e.target.value); const onSubmit = (e) => { e.preventDefault(); if (toDo === '') { return; } setToDos((toDos) => [toDo, ...toDos]); setToDo(''); }; console.log(toDos); return ( <div> <h1>My To Dos ({toDos.length})</h1> <form onSubmit={onSubmit}> <input type="text" placeholder="Write your to do..." value={toDo} onChange={onChange} /> <button>Add To Do</button> </form> </div> ); } export default App;
이제 쌓여있는 toDos를 화면에 뿌려줄 차례입니다.
이럴 때는 map() 메서드를 쓰면 아주 편해요! 이 함수는 주어진 요소 각각에 함수를 실행하고 그 결과들을 모아 새로운 배열로 “반환”합니다.
JSX 내에 { } 로 map 함수를 써봅시다!
<ul>{toDos.map((todo) => ( <li>{todo}</li>))}</ul>
이렇게 해두면 일단 화면에 잘 나오지만 콘솔이 뭐라고 하는데,
모든 리스트의 자식 요소는 반드시 ‘고유한 키’를 가져야 한다고 합니다.다행히 map 함수의 두 번째 매개변수는 처리할 현재 요소의 인덱스이기 때문에 이 값을 key로 넣어줄 수 있습니다.
<ul>{toDos.map((todo, idx) => ( <li key={idx}>{todo}</li>))}</ul>
2-2. Coin Tracker
로드 시 1번만 데이터를 불러올 것이므로 아래와 같이 fetch API 코드를 작성합니다.
useEffect(() => { fetch('<https://api.coinpaprika.com/v1/tickers>') .then((response) => response.json()) .then((json) => { setCoins(json); setLoading(false); }); }, []);
그리고 이런저런 기능을 추가해 봅니다 ;)
import { useState, useEffect } from 'react'; import styles from './App.module.css'; function App() { const topCoins = ['BTC', 'ETH', 'BNB', 'KLAY']; const [loading, setLoading] = useState(true); const [coins, setCoins] = useState([]); const [currentUnit, setCurrentUnit] = useState(topCoins[0]); const [value, setValue] = useState(''); const onSelect = (e) => { setCurrentUnit(e.target.value); setValue(''); }; const onChange = (e) => { if (!e.target.value.replace(/^[1-9]\d*(\d+)?$/i, '')) { setValue(e.target.value); } }; const hideDecimal = (number, length) => { return Math.floor(number * 10 ** length) / 10 ** length; }; useEffect(() => { fetch('https://api.coinpaprika.com/v1/tickers') .then((response) => response.json()) .then((json) => { setCoins(json); setLoading(false); }); }, []); return ( <div> <h1>My Coins! ({coins.length})</h1> {loading ? ( <strong>Loading...</strong> ) : ( <div> <div className={styles.currentPrice}> <h2>Current Price</h2> <p></p> {topCoins.map((elem, idx) => ( <p key={idx}> <strong>1 {elem} =</strong> <span> {hideDecimal( coins.filter((coin) => coin.symbol === elem)[0].quotes.USD .price, 3 )} </span> <i>USD</i> </p> ))} </div> <div className="convertPrice"> <h3>Convert Price</h3> <select onChange={onSelect}> {topCoins.map((elem, idx) => ( <option key={idx} defaultValue={idx === 0}> {elem} </option> ))} </select> <div> <input type="text" value={value} placeholder="Enter here..." onChange={onChange} ></input> <span className="unit">USD</span> </div> <div> <input type="text" disabled value={ value ? hideDecimal( value / coins.filter((coin) => coin.symbol === currentUnit)[0] .quotes.USD.price, 6 ) : '' } onChange={onChange} ></input> <span className="unit">{currentUnit}</span> </div> <button onClick={() => setValue('')}>Clear</button> </div> </div> )} </div> ); } export default App;
3. 무비 앱 만들기
3-1. 데이터 받아오기
일단 API를 통해 우리가 필요한 데이터를 받아와 보죠!
요청을 하고
fetch()
⇒ 그게 끝나면then()
⇒ 그 결과를 JSON 형태로 만들고response.json()
⇒ 그게 끝나면then()
⇒ 그 결과를 콘솔에 찍도록 합니다console.log()
fetch() .then((response) => response.json) .then((json) => console.log(json))
하지만 이렇게 then.then... 으로 이어가는 대신 async-await를 사용하는 게 훨씬 편합니다.
함수 앞에
async
를 붙이고 기다리는 대상에await
를 붙이면 됩니다 :Dconst getMovies = async() => { const response = await fetch(); const json = await response.json(); console.log(json); } // 더 짧게 한다면 const getMovies = async () => { const json = await(await fetch()).json(); console.log(json); }
참고
이렇게 받아온 정보는 movies라는 상태로 관리하려고 합니다.
const [movies, setMovies] = useState([]);
아, 그리고 API를 불러오는 중이라면 화면을 가릴 수 있도록 loading이란 상태도 만들어 두죠!
const [loading, setLoading] = useState(true);
이제 getMovies 함수 내에서 불러온 데이터를 setMovies 로 넣어주고 그 후 loading 상태를 false로 바꿉니다.
getMovies는 로드 후 딱 한 번만 실행되도록 useEffect 설정도 해줘야겠죠 ;)
const [loading, setLoading] = useState(true); const [movies, setMovies] = useState([]); const getMovies = async () => { const json = await ( await fetch( `https://yts.mx/api/v2/list_movies.json?minimum_rating=9&sort_by=year` ) ).json(); setMovies(json.data.movies); setLoading(false); }; useEffect(() => { getMovies(); }, []);
JSX 부분을 작성할 차례입니다.
받아온 movies는 map 함수를 통해 뿌려줍니다. 이때, movies.genres는 배열 형태이기 때문에 map 내에 map 함수를 사용합니다.
map 함수를 쓸 때는 key 값을 빼먹지 않도록 주의!
return ( <div> {loading ? ( <h1>Loading...</h1> ) : ( <div> {movies.map((movie) => ( <div key={movie.id}> <h2><a href="">{movie.title}</a></h2> <img src={movie.cover} alt={movie.title} /> <p>{movie.summary}</p> <ul> {movie.genres.map((g) => ( <li key={g}>{g}</li> ))} </ul> </div> ))} </div> )} </div> );
짠! 데이터를 잘 받아왔네요!
/* App.js */ import { useState } from 'react'; import { useEffect } from 'react/cjs/react.development'; function App() { const [loading, setLoading] = useState(true); const [movies, setMovies] = useState([]); const getMovies = async () => { const json = await ( await fetch( `https://yts.mx/api/v2/list_movies.json?minimum_rating=9&sort_by=year` ) ).json(); setMovies(json.data.movies); setLoading(false); }; useEffect(() => { getMovies(); }, []); console.log(movies); return ( <div> {loading ? ( <h1>Loading...</h1> ) : ( <div> {movies.map((movie) => ( <div key={movie.id}> <h2>{movie.title}</h2> <img src={movie.medium_cover_image} alt={movie.title} /> <p>{movie.summary}</p> <ul> {movie.genres.map((g) => ( <li key={g}>{g}</li> ))} </ul> </div> ))} </div> )} </div> ); } export default App;
3-2. 컴포넌트 만들기
저 movies.map() 부분을 아예 컴포넌트화 시켜서 뚝 떼다쓸 수 있도록 바꿔 봅시다.
components/ 폴더 내에 Movie.js를 만들고 내용을 분리해 줍니다.
/* components/Movie.js */ function Movie() { return ( <div key={movie.id}> <h2>{movie.title}</h2> <img src={movie.medium_cover_image} alt={movie.title} /> <p>{movie.summary}</p> <ul> {movie.genres.map((g) => ( <li key={g}>{g}</li> ))} </ul> </div> ); } export default Movie;
여기서 key 값은 필요 없고, 부모 컴포넌트에서 props로 내려주면 되는 부분이므로 삭제합니다.
그 외에 movie.title, movie.summary 등도 props로 받아올 것이므로 매개변수 자리에 구조분해 할당하여 자리를 만들어 주자고요!
완성도를 높이기 위해 propTypes도 지정해주면 더욱 좋고요.
추가로 화면에 보여질 글자 수를 제한하고 싶다면 이런 식으로 설정하면 됩니다.
<p>{summary.length > 235 ? ${summary.slice(0, 235)}... : summary}</p>
/* components/Movie.js */ import PropTypes from 'prop-types'; function Movie({ title, cover, summary, genres }) { return ( <div> <div> <h2>{title}</h2> <img src={cover} alt={title} /> <p>{summary.length > 235 ? `${summary.slice(0, 235)}...` : summary}</p> <ul> {genres.map((g) => ( <li key={g}>{g}</li> ))} </ul> </div> </div> ); } Movie.propTypes = { cover: PropTypes.string.isRequired, title: PropTypes.string.isRequired, summary: PropTypes.string.isRequired, genres: PropTypes.arrayOf(PropTypes.string).isRequired }; export default Movie;
그럼 이제 부모 컴포넌트인 App으로 올라가 props를 하나하나 내려줍니다.
/* App.js */ // ... 상략 ... return ( <div> {loading ? ( <h1>Loading...</h1> ) : ( <div> {movies.map((movie) => ( <Movie key={movie.id} title={movie.title} cover={movie.medium_cover_image} summary={movie.summary} genres={movie.genres} /> ))} </div> )} </div> ); export default App;
3-3. 라우팅
지금은 App.js에서 화면을 다 그리고 있는데 구조를 바꿔보죠!
우선 App.js의 내용은 복사해서 Home.js로 변경합니다. App.js는 다른 역할을 할 거에요.
우리가 만들 구조는 대충 이렇게 됩니다.
- App.js : 여기서 경로를 안내해요.
/
→ Home 화면,/movies
→ Detail 화면으로 가라고 알려주는 역할! - routes/ : 이 폴더 내에 Home.js, Detail.js 등 각 라우트 파일이 들어갑니다.
- components/ : 실제 화면을 렌더링하는 컴포넌트들이에요. Home과 Detail 등이 불러와 사용하게 됩니다.
폴더 정리가 끝났으면 우릴 도와줄 라우트 라이브러리를 아묻따 설치!
$ npm i react-router-dom
우선 라우터 객체들을 쏙쏙 꺼내와 줍니다.
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
(※ 여기선 v6 문법을 기준으로 합니다)
사용하는 단계는 아래와 같아요!
👉 Router ⇒ Routes ⇒ Route (path, element)
그러니 이렇게 써주면 됩니다.
App.js는 들어온 path에 따라 ‘저기로 가세요’라고 안내해주는 역할을 합니다.
/* App.js */ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Home from './routes/Home'; import Detail from './routes/Detail'; function App() { return ( <Router> <Routes> <Route path="/" element={<Home />} /> <Route path="/movie" element={<Detail />} /> <Route path="/hello" element={<p>Hello ;)</p>} /> </Routes> </Router> ); } export default App;
여기서 체크할 점. Movie 컴포넌트 내에는 하이퍼링크가 하나 있었죠.
<a href="/movie">{title}</a>
이걸 누르면 이동은 되지만 그때마다 리로드 됩니다.
하지만 이때! 라이브러리가 제공하는 Link 컴포넌트를 쓰면 어떨까요!
import { Link } from 'react-router-dom';
<Link to="/movie">{title}</Link>
그럼 이제 영화를 클릭할 때마다 /movie 로 슈슈슉 이동합니다.
/hello 로 접근하면 Hello 컴포넌트의 내용이 잘 나오고요 ;)
3-4. 상세정보 불러오기
지금은 클릭해도 Detail 이라는 글자밖에 나오지 않습니다.
이제 클릭한 영화의 id값을 알아내서 그에 맞게 API 요청을 해 정보를 뿌려주는 작업을 해봅시다.
리액트 라우터는 다이나믹 로ㄷ... 다이나믹 URL을 지원합니다.
그래서 클릭한 영화의 id값을 URL로 넘겨줄 수 있습니다.
사용자를 Detail로 넘기기 전, id 값도 함께 보내줍니다. 이렇게 점 두개(콜론) 찍어주면 돼요!
<Route path="/movie/:id" element={<Detail />} />
/* App.js */ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Home from './routes/Home'; import Detail from './routes/Detail'; function App() { return ( <Router> <Routes> <Route path="/" element={<Home />} /> <Route path="/movie/:id" element={<Detail />} /> <Route path="/hello" element={<p>Hello ;)</p>} /> </Routes> </Router> ); } export default App;
하지만 지금 id라는 props를 넘겨주고 있지 않기 때문에, Home 컴포넌트(부모)에서 Movie 컴포넌트(자식)으로 id란 props를 넘겨줍니다.
/* Home.js */ //... return ( <div> {loading ? ( <h1>Loading...</h1> ) : ( <div> {movies.map((movie) => ( <Movie id={movie.id} key={movie.id} title={movie.title} cover={movie.medium_cover_image} summary={movie.summary} genres={movie.genres} /> ))} </div> )} </div> ); //...
그리고 Movie 컴포넌트에서 id를 받아오고, Link 컴포넌트의 경로도 변경해 줍니다.
/* Movie.js */ import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; function Movie({ id, title, cover, summary, genres }) { return ( <div> <div> <h2> <Link to={`/movie/${id}`}>{title}</Link> </h2> <img src={cover} alt={title} /> <p>{summary}</p> <ul> {genres.map((g) => ( <li key={g}>{g}</li> ))} </ul> </div> </div> ); } Movie.propTypes = { id: PropTypes.number.isRequired, cover: PropTypes.string.isRequired, title: PropTypes.string.isRequired, summary: PropTypes.string.isRequired, genres: PropTypes.arrayOf(PropTypes.string).isRequired }; export default Movie;
짜라잔- 해당 movie.id가 URL 파라미터에 잘 들어가 있네요!
이제 이걸 이용해 해당 id의 영화 정보를 받아와 뿌려주면 되겠습니다. Detail.js를 수정할 차례입니다.
API 요청을 1번만 하기 위해 useEffect를 씁니다.
/* Detail.js */ import { useEffect } from 'react'; function Detail() { const getMovie = () => {}; useEffect(() => { getMovie(); }, []); return <h1>Detail</h1>; } export default Detail;
getMovie의 함수 내용은 Home.js에서 썼던 것과 유사합니다.
차이가 있다면 id 값을 담아 요청해야한다는 건데, 이건 useParams()로 받아옵니다.
/* Detail.js */ import { useParams } from 'react-router-dom'; function Detail() { console.log(useParams()); return <h1>Detail</h1>; } export default Detail;
id 값이 넘어온 게 보입니다.
그럼 만약
<Route path="/movie/:id/:nana" element={<Detail />} />
처럼 쓴다면 nana 라는 값도 받아볼 수 있겠네요!이제 객체 조지기를 통해 { id } 로 꺼내와주고 이 값을 fetch 할 때 ${id}로 넣어 요청합니다.
import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; function Detail() { const { id } = useParams(); const getMovie = async () => { const json = await ( await fetch(`https://yts.mx/api/v2/movie_details.json?movie_id=${id}`) ).json(); console.log(json); }; useEffect(() => { getMovie(); }, []); return <h1>Detail</h1>; } export default Detail;
3-5. 배포하기
이렇게 만든 프로젝트를 github pages에 업로드해볼 차례입니다!
npm을 통해 gh-pages 란 패키지를 설치합니다.
$npm i gh-pages
pacakge.json을 보면 이미 지정된 명령문이 있는 걸 볼 수 있습니다.
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" },
우리는 npm run build를 치기만 하면 CRA가 알아서 파일들을 최적화해 줍니다.
이제 이걸 github에 올릴 수 있도록 설정해 봅니다.
먼저 package.json 내에 homepage 내용을 추가합니다.
{ "homepage" : "https://{GitHub ID}.github.io/{Repository name}" }
$ git remote -v
를 치면 저장소 이름을 알 수 있어요.저는 https://nana-like.github.io/react-movie 가 되겠네요!
다음으로 deploy라는 script 를 추가할 차례입니다.
이 명령어를 통해 gh-pages를 실행시키고 빌드된 파일들을 가져가도록 명령 합니다.
"deploy": "gh-pages -d build"
gh-pages 야 / 이 디렉토리 안에 있는 거 가져가 / build라는 이름의
음... 생각해보니 배포를 하려면 빌드된 파일이 먼저 존재해야 할 텐데요.
그럼 deploy 명령문을 실행시키면 먼저 npm run build가 돌아가도록 하면 편하겠네요.
스크립트 앞에 pre가 붙으면 해당 스크립트가 먼저 실행되기 때문에, npm run build를 predeploy란 이름으로 만들어주면 되겠죠!
{ "script": { "predeploy": "npm run build", "deploy": "gh-pages -d build" } }
그리고 npm run deploy를 치면... predeploy가 먼저 실행되고 ‘Published’란 내용이 뜹니다.
잠시 기다렸다가 homepage에 적어준 주소로 이동하면...
잘 안 나옵니다....엙?
우리는 CRA 작업할 때 루트(/ ) 기준으로 했지만, github pages는 저장소 이름(react-movie)을 갖고 있기 때문이죠.
실제로 이 상태에서 localhost:3000으로 들어가면 자동으로 /react-movie 가 URL에 붙습니다.
따라서 라우터의 basename을 슥 바꿔주는 작업이 필요합니다.
<Router basename={process.env.PUBLIC_URL}>
/* App.js */ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Home from './routes/Home'; import Detail from './routes/Detail'; function App() { return ( <Router basename={process.env.PUBLIC_URL}> <Routes> <Route path={`/`} element={<Home />} /> <Route path="/movie/:id" element={<Detail />} /> <Route path="/hello" element={<p>Hello ;)</p>} /> </Routes> </Router> ); } export default App;
이제 다시 npm run deploy를 해주면...
참고
추후 뚝딱뚝딱은 미래의 나에게...
중간에 잠깐 텀을 두고 다시 공부하려고 들어갔는데, 내가 왜 이렇게 작성했는지 하나도 기억이 안 나서 놀랐다ㅋㅋㅋㅋㅋ
엙 내가 코멘트를 남겼다고요...? 이것이 나의 커밋...? 그땐 어이가 없었는데 지금보니 당연한 거였다!
그야 처음 보는 건데 한번에 뚝딱 이해가 가고 뚝딱 외워질 리가...그래서 맘 편히 먹고 공부하기로 허허허 그래도 덕분에 나한테 맞는 학습 스타일을 알 수 있었다.
1) 난 그냥 여러 번 봐야 이해되는 스타일! 처음에 2배속으로 쫙 돌리거나 스크립트 먼저 읽어서 복습 회차 늘리는 것도 좋더라
2) 난 내 언어로 바꿔야 정리되는 스타일! (아아 이것이 찐 문과) 특히 복습할 때 인강은 여러 번 가볍게 보기가 힘든데, 노트라도 남기면 마따마따 하면서 볼 수 있는 게 장점후후후...
728x90'Blog > Library' 카테고리의 다른 글
[ReactJS] 5. 𝒘𝒊𝒕𝒉 타입스크립트 (2) 2022.02.13 [ReactJS] 4. styled-components 💅🏾 (2) 2022.02.09 [ReactJS] 1. 시작하기 (7) 2022.02.01 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (4) (2) 2020.06.03 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (3) (0) 2020.05.30 댓글
- App.js : 여기서 경로를 안내해요.