Blog/Library

[ReactJS] 2. 기능 연습 & 3. 앱 만들기

나나 (nykim) 2022. 2. 3. 22:14
320x100

Abstract vector created by fullvector - www.freepik.com

 

이 글은 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를 붙이면 됩니다 :D

 

const 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 문법을 기준으로 합니다)

 

사용하는 단계는 아래와 같아요!

👉 RouterRoutesRoute (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를 해주면...

 

status: 'ok'

 

참고

 

추후 뚝딱뚝딱은 미래의 나에게...

 

 


 

중간에 잠깐 텀을 두고 다시 공부하려고 들어갔는데, 내가 왜 이렇게 작성했는지 하나도 기억이 안 나서 놀랐다ㅋㅋㅋㅋㅋ
엙 내가 코멘트를 남겼다고요...? 이것이 나의 커밋...? 그땐 어이가 없었는데 지금보니 당연한 거였다!
그야 처음 보는 건데 한번에 뚝딱 이해가 가고 뚝딱 외워질 리가...

그래서 맘 편히 먹고 공부하기로 허허허 그래도 덕분에 나한테 맞는 학습 스타일을 알 수 있었다.

1) 난 그냥 여러 번 봐야 이해되는 스타일! 처음에 2배속으로 쫙 돌리거나 스크립트 먼저 읽어서 복습 회차 늘리는 것도 좋더라
2) 난 내 언어로 바꿔야 정리되는 스타일! (아아 이것이 찐 문과) 특히 복습할 때 인강은 여러 번 가볍게 보기가 힘든데, 노트라도 남기면 마따마따 하면서 볼 수 있는 게 장점

 

후후후...

(c) LINE+

 

728x90