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

    2022. 2. 3.

    by. 나나 (nykim)

    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

    댓글