ReactJS로 영화 웹 서비스 만들기


노마드 코더 강의 (ReactJS로 영화 웹 서비스 만들기)

👋 React, JSX 처음 접하기

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const root = document.getElementById('root');
const Title = () => (
<h3>Hi, it's title</h3>
);
// const h3 = React.createElement(
// 'h3',
// null,
// "Hi, it's title"
// );
const Button = () => (
<button
onClick={() => console.log('clicked!')}
onMouseEnter={() => console.log('entered!')}
>
click me!
</button>
);
// const btn = React.createElement(
// 'button',
// {
// onClick: () => console.log('clicked!'),
// onMouseEnter: () => console.log('entered!')
// },
// 'click me!'
// );
const Container = () => (
<div>
<Title />
<Button />
</div>
);
// const container = React.createElement(
// 'div',
// null,
// [Title, Button]
// );
ReactDOM.render(<Container />, root);
</script>
</html>
  • 실제 리액트를 사용할 때 사용하는 방법은 아니지만 약식으로 리액트에 대한 이해를 돕기 위해 우선 이렇게 진행하는 것 같다.
  • 각주 처리된 코드는 script에 babel을 넣기 전에 작성한 Vanilla JS코드인데, babel이 위 JSX를 각주 처리된 코드로 바꿔준다.
  • Container 안에 <Title />과 <Button />은 html태그가 아니라 컴포넌트이다. 첫번째 글자가 대문자여야 한다.
  • 여기서는 root안에 Container를 표시하고, Container 안에 Title과 Button이 들어있는 형태이다.
  • JSX 문법은 언뜻 보기에는 HTML과 비슷하지만 Button에서 한 것처럼 JS 문법이 통한다는 점에서 다르다.
  • 🫐 State

  • State는 컴포넌트가 가지는 상태를 말한다.
  • [참고] React와 바닐라 자바스크립트 동작의 차이

  • 클릭 한번에 counter가 1증가하고 이를 표시하는 작업을 한다고 해보자.
  • 바닐라 자바스크립트로 counter 변수를 선언하고 이를 innerText로 넣는 방식으로 이 작업을 수행하면 아래와 같이 동작한다.
  • 클릭 한번에 여러 요소가 함께 갱신되는 것을 볼 수 있다.
  • 그러나 리액트는 다음과 같이 동작한다.
  • counter 부분만 갱신되는 것을 볼 수 있다.
  • 배열 안 요소들의 변수명을 지정해주는 문법 (JS)

    const arr = [1, 2, 3];
    const [a, b, c] = arr;
    console.log(a); // 1
    console.log(b); // 2
    console.log(c); // 3

    React.useState()

  • React.useState()는 컴포넌트의 state를 생성하고 업데이트되게 할 수 있는 도구이다.
  • React에서 아래 코드를 입력하면
  • console.log(React.useState(0));
  • 다음과 같은 값이 나온다.
  • 보이는 것처럼 이는 배열이며 아래와 같은 문법이 가능하다.
  • const [state, setState] = React.useState(초기값);
  • setState는 state 값을 변경해 줄 함수이다. 예를들어,
  • setState(1); // 이렇게하면 state 값이 1이 된다.
  • 따라서 아래와 같이 함수를 선언해주고 button의 onClick 속성에 이벤트리스너로 추가해줄 수 있다.
  • const [state, setState] = React.useState(초기값);
    const onClick = () => {
    setState(state+1);
    };
    return (
    <div>
    <h3>Total clicks: {state}</h3>
    <button onClick={onClick}></button>
    </div>
    );
  • 위와 같이 작성하면 button이 클릭될 때마다 state 값이 1씩 증가하고 이것이 h3태그 안의 state에 Re-render된다.
  • State 실습-1 (클릭하면 숫자가 올라가는 Counter)

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    </head>
    <body>
    <div id="root"></div>
    </body>
    <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/babel">
    const root = document.getElementById("root");
    function App() {
    const [counter, setCounter] = React.useState(0);
    const onClick = () => {
    // setCounter(counter + 1);
    setCounter((current) => current + 1); // 위쪽 각주처리된 코드보다 이처럼 현재값으로 지정해주는 것이 더 안전하다.
    };
    return (
    <div>
    <h3>Total clicks: {counter}</h3>
    <button onClick={onClick}>Click me!</button>
    </div>
    );
    }
    ReactDOM.render(<App />, root);
    </script>
    </html>
    결과물
    결과물

    JSX와 HTML

  • JSX는 HTML의 문법과 매우 비슷하지만 자바스크립트가 선점한 용어는 html과 조금 다르게 표현해야 한다.
  • 예시는 다음과 같다.
  • <h1 className="hi">Super Converter</h1>
    <label htmlFor="minutes">Minutes</label>
    <input id="minutes" placeholder="Minutes" type="number" />
    <label htmlFor="hours">Hours</label>
    <input id="hours" placeholder="hours" type="number" />
  • classclassName으로, forhtmlFor로 사용하고 있다.
  • html에서 사용하는 원래의 명칭으로 사용하면 브라우저가 경고한다. (에러는 아님)
  • JS 문법을 쓰고싶다면 {} 안에 쓰면 된다.
  • State 실습-2 (Hours-Minutes Converter)

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    </head>
    <body>
    <div id="root"></div>
    </body>
    <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script type="text/babel">
    function App() {
    const [amount, setAmount] = React.useState(""); // State 초기값을 빈 값으로 설정
    const [inverted, setInverted] = React.useState(false); // State 초기값을 false로 설정
    const onChange = (event) => { // input 태그에 change가 발생하면 실행
    setAmount(event.target.value); // input 태그의 value로 amount 설정
    };
    const reset = () => { // Reset버튼 누르면 실행
    setAmount("");
    };
    const onInvert = () => { // Insert Hours(Minutes)버튼 누르면 실행
    reset(); // 우선 값을 비워준다.
    setInverted((current) => !current); // 이 또한 현재값 이용, Boolean을 반대로 전환
    };
    return (
    <div>
    <h1 className="hi">Super Converter</h1>
    <div>
    <label htmlFor="minutes">Minutes</label>
    <input
    value={inverted ? amount * 60 : amount} // 삼항연산자 사용
    id="minutes"
    placeholder="Minutes"
    type="number"
    onChange={onChange}
    disabled={inverted} // inverted가 true일때 disabled
    />
    </div>
    <div>
    <label htmlFor="hours">Hours</label>
    <input
    value={inverted ? amount : amount / 60}
    id="hours"
    placeholder="hours"
    type="number"
    onChange={onChange}
    disabled={!inverted} // inverted가 false일때 disabled
    />
    </div>
    <button onClick={reset}>Reset</button>
    <button onClick={onInvert}>{inverted ? "Insert Minutes" : "Insert Hours"}</button>
    // inverted가 true인지 false인지에 따라 버튼의 content가 변경됨
    </div>
    );
    }
    const root = document.getElementById("root");
    ReactDOM.render(<App />, root);
    </script>
    </html>
    결과물
    결과물

    Divide-and-Conquer (State 마무리)

    <script>
    function MinutesToHours() { // <MinutesToHours /> (컴포넌트)
    const [amount, setAmount] = React.useState("");
    const [inverted, setInverted] = React.useState(false);
    const onChange = (event) => {
    setAmount(event.target.value);
    };
    const reset = () => {
    setAmount("");
    };
    const onInvert = () => {
    reset();
    setInverted((current) => !current);
    };
    return (
    <div>
    <h3 className="hi">Minutes To Hours Converter</h3>
    <div>
    <label htmlFor="Minutes">Minutes</label>
    <input
    value={inverted ? amount * 60 : amount}
    id="Minutes"
    placeholder="Minutes"
    type="number"
    onChange={onChange}
    disabled={inverted}
    />
    </div>
    <div>
    <label htmlFor="Hours">Hours</label>
    <input
    value={inverted ? amount : (amount / 60).toFixed(2)}
    id="Hours"
    placeholder="Hours"
    type="number"
    onChange={onChange}
    disabled={!inverted}
    />
    </div>
    <button onClick={reset}>Reset</button>
    <button onClick={onInvert}>
    {inverted ? "Insert Minutes" : "Insert Hours"}
    </button>
    </div>
    );
    }
    function KmToMiles() { // <KmToMiles /> (컴포넌트)
    const [amount, setAmount] = React.useState("");
    const [inverted, setInverted] = React.useState(false);
    const onChange = (event) => {
    setAmount(event.target.value);
    };
    const reset = () => {
    setAmount("");
    };
    const onInvert = () => {
    reset();
    setInverted((current) => !current);
    };
    return (
    <div>
    <h3 className="hi">Km To Hours Converter</h3>
    <div>
    <label htmlFor="Km">Km</label>
    <input
    value={inverted ? (amount * 1.609).toFixed(2) : amount}
    id="Km"
    placeholder="Km"
    type="number"
    onChange={onChange}
    disabled={inverted}
    />
    </div>
    <div>
    <label htmlFor="Miles">Miles</label>
    <input
    value={inverted ? amount : (amount / 1.609).toFixed(2)}
    id="Miles"
    placeholder="Miles"
    type="number"
    onChange={onChange}
    disabled={!inverted}
    />
    </div>
    <button onClick={reset}>Reset</button>
    <button onClick={onInvert}>
    {inverted ? "Insert Km" : "Insert Miles"}
    </button>
    </div>
    );
    }
    function App() { // 두가지 컴포넌트를 select의 value에 따라 삼항연산자를 통해 랜더링
    const [index, setIndex] = React.useState("0");
    const onChange = (event) => {
    setIndex(event.target.value);
    };
    return (
    <div>
    <h1 className="hi">Super Converter</h1>
    <select onChange={onChange}>
    <option value="0">Minutes To Hours</option>
    <option value="1">Km To Miles</option>
    </select>
    <hr />
    {index === "0" ? <MinutesToHours /> : null}
    {index === "1" ? <KmToMiles /> : null}
    </div>
    );
    }
    const root = document.getElementById("root");
    ReactDOM.render(<App />, root);
    </script>
  • 결과적으로 리액트는 위 코드처럼 여러가지 컴포넌트를 만들고 상황에 따라 어떤 것을 랜더링할지 정할 수 있다.
  • 니코쌤은 이를 분할 정복이라고 표현했다.
  • 🍒 Props

  • props는 properties의 줄임말로, 컴포넌트에 어떠한 값을 전달할 때 사용한다.
  • 기본적인 사용법은 아래와 같다.
  • function Btn(props) {
    return (
    <button
    style={{
    backgroundColor: "tomato",
    border: "none",
    padding: 10,
    margin: 10,
    }}
    >
    {props.text}
    </button>
    );
    }
    function App() {
    return (
    <div>
    <Btn text="Confirm" />
    <Btn text="Submit" />
    </div>
    );
    }
    const root = document.getElementById("root");
    ReactDOM.render(<App />, root);
  • 버튼 컴포넌트들의 스타일은 모두 동일하게 가져가되, 버튼 안의 텍스트만 다르게 받아올 수 있는 코드이다.
  • Btn 함수에 props를 전달하고 만약 이를 console.log 하면 Btn 컴포넌트를 호출한 곳에 속성으로 작성한 값들이 Object 형태로 나오게 된다.
  • 이는 아래와 같은 형식으로도 사용할 수 있다. {} 안에 props 이름을 넣는 것이다. props 이름은 아무거나 가능하다.
  • function Btn({text, 아무거나}) {
    return (
    <button
    style={{
    backgroundColor: "tomato",
    border: "none",
    padding: 10,
    margin: 10,
    }}
    >
    {text}
    {아무거나}
    </button>
    );
    }
    function App() {
    return (
    <div>
    <Btn text="Confirm" 아무거나="ㅎㅇ"/>
    <Btn text="Submit" 아무거나="ㅎㅇ"/>
    </div>
    );
    }

    onClick에 전달하기 React.memo

  • props를 state와 함께 활용하면 다음과 같이 이벤트 리스너를 더 폭넓게 활용할 수 있다.
  • 아래 코드에서 text가 Submit인 Btn은 이벤트 리스너가 적용되지 않는다.
  • function Btn({text, changeValue}) {
    return (
    <button
    onClick={changeValue}
    style={{
    backgroundColor: "tomato",
    border: "none",
    padding: 10,
    margin: 10,
    }}
    >
    {text}
    </button>
    );
    }
    function App() {
    const [value, setValue] = React.useState("Save Changes");
    const changeValue = () => setValue("Revert Changes");
    return (
    <div>
    <Btn text={value} changeValue={changeValue} />
    <Btn text="Submit" />
    </div>
    );
    }

    React.memo()

  • React.memo()는 적용한 컴포넌트가 변화가 있을 때에만 해당 컴포넌트를 re-render한다. 만약 변경사항이 없다면 re-render하지 않고 그대로 사용한다.
  • function Btn({text, changeValue}) {
    return (
    <button
    onClick={changeValue}
    style={{
    backgroundColor: "tomato",
    border: "none",
    padding: 10,
    margin: 10,
    }}
    >
    {text}
    </button>
    );
    }
    const MemorizedBtn = React.memo(Btn);
    function App() {
    const [value, setValue] = React.useState("Save Changes");
    const changeValue = () => setValue("Revert Changes");
    return (
    <div>
    <MemorizedBtn text={value} changeValue={changeValue} />
    <MemorizedBtn text="Submit" />
    </div>
    );
    }

    propTypes

  • number로 보내야 하는 prop 값을 string으로 보내는 등의 실수가 있어도 react는 이를 알리지 않고 그대로 실행한다.
  • propTypes를 이용하면 이러한 부분을 경고를 띄울 수 있다.
  • Btn.propTypes = {
    text: PropTypes.string.isRequired, // text값 필수, 문자형
    fontSize: PropTypes.number, // 숫자형
    };
  • 위와 같이 정하면 text 값이 들어오지 않거나 string이 아닌 값으로 들어왔을 때, fontsize가 number가 아닌 값으로 들어왔을 때에 브라우저가 경고를 띄워준다.
  • 🔥 Create-React-App

    리액트 앱 생성하기

  • 리액트 앱을 생성하는 커맨드 라인
  • npx craete-react-app (생성할 앱 이름)
    cd (생성한 앱 이름)
    npm start
  • 이렇게 하면 초기 세팅이 된 리액트 앱이 생성된다.
  • CSS 적용해보기

  • create-react-app에서는 다음과 같이 css를 적용해줄 수 있다.
  • App.module.css
  • .title {
    font-family: 'Courier New', Courier, monospace;
    font-size: 18px;
    }
  • App.js
  • import Button from './Button';
    import styles from './App.module.css';
    function App() {
    return (
    <div>
    <h1 className={styles.title}>Welcome back!</h1>
    <Button text={'Continue'} />
    </div>
    );
    }
    export default App;
  • h1 className은 다음처럼 랜덤으로 생성된다.
  • 따라서 같은 이름의 클래스를 다른 module.css에서도 사용할 수 있게 된다.
  • 💥 Effect

  • State가 변경될 때마다 컴포넌트의 모든 코드들은 다시 실행된다.
  • 첫 번째 실행에서만 실행하고 싶은 코드들이 존재할 수 있다.
  • 이럴때 사용할 수 있는 것이 useEffect()이다.
  • import { useState, useEffect } from 'react';
    function App() {
    const [counter, setValue] = useState(0);
    const onClick = () => setValue((prev) => prev + 1);
    console.log('I run all the time');
    const iRunOnlyOnce = () => {
    console.log('I Run Only Once');
    }
    useEffect(iRunOnlyOnce, []);
    return (
    <div>
    <h1>{counter}</h1>
    <button onClick={onClick}>Click Me!</button>
    </div>
    );
    }
    export default App;
  • 다음과 같이 useEffect를 사용하면 iRunOnlyOnce 함수는 첫 번째 실행에서만 실행되고 State가 변경되어도 다시 실행되지 않는다.
  • useEffect의 두 번째 인자에는 배열이 들어가는 것을 볼 수 있는데, 해당 배열에 State 변수를 넣어주면 해당 State가 변화할 때만 코드가 실행된다. 위 코드처럼 배열에 아무 요소도 없으면 맨 처음에만 실행된다.
  • import { useState, useEffect } from 'react';
    function App() {
    const [counter, setValue] = useState(0);
    const [keyword, setKeyword] = useState('');
    const onClick = () => setValue((prev) => prev + 1);
    const onChange = (e) => setKeyword(e.target.value);
    useEffect(() => {
    console.log(`Keyword Changes! ${keyword}`);
    }, [keyword]);
    useEffect(() => {
    console.log(`Counter Changes! ${counter}`);
    }, [counter]);
    useEffect(() => {
    console.log(`Keyword or Counter Changes!`);
    }, [keyword, counter]);
    return (
    <div>
    <input type='text' onChange={onChange} />
    <h1>{counter}</h1>
    <button onClick={onClick}>Click Me!</button>
    </div>
    );
    }
    export default App;
  • 위와 같이 작성하면 Keyword Changes! 는 keyword가 변경되었을 때에만, Counter Changes! 는 counter가 변경되었을 때에만, Keyword or Counter Changes! 는 둘 중 하나라도 변경되었을 때에 실행된다.
  • import { useState, useEffect } from 'react';
    function App() {
    function Hello() {
    useEffect(() => {
    console.log('Hi :)');
    return () => {
    console.log('Bye :(');
    };
    }, []);
    return <h1>Hello!</h1>;
    }
    const [showing, setShowing] = useState(false);
    const onClick = () => {
    setShowing((prev) => !prev);
    };
    return (
    <div>
    <button onClick={onClick}>{showing ? 'Hide' : 'Show'}</button>
    {showing ? <Hello /> : null}
    </div>
    );
    }
    export default App;
  • 또한 위처럼 useEffect에서 실행하는 함수 return에 함수가 있으면 해당 컴포넌트가 사라질 때 이를 실행한다.
  • 📜 To-do List 만들기

  • Vanilla JS로 했던 To-do List 만들기를 리액트로 구현했다.
  • 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((currentArr) => [...currentArr, todo]);
    setTodo('');
    };
    const onClick = (index) => {
    setTodos((currentArr) => {
    currentArr.splice(index, 1);
    return [...currentArr];
    });
    };
    return (
    <div>
    <h1>My TODOS! ({todos.length})</h1>
    <form onSubmit={onSubmit}>
    <input
    onChange={onChange}
    value={todo}
    type='text'
    placeholder='Write To Do!'
    ></input>
    <button>Add To Do</button>
    </form>
    <hr />
    <ul>
    {todos.map((item, index) => {
    return (
    <li key={index}>
    {item}
    <button onClick={() => onClick(index)}>X</button>
    </li>
    );
    })}
    </ul>
    </div>
    );
    }
    export default App;
  • 강의는 추가하는 것 까지인데 지우는 기능은 내가 혼자 구현해 보았다.
  • 배운점들

  • setState()의 괄호 안에는 데이터를 직접 넣거나 함수를 넣을 수 있다.
  • state를 바꾸고 싶다고 해서 state에 할당하면 안된다. (const로 선언했다고 재할당할 수 없다고 뜸)
  • onClick 등 이벤트리스너에 매개변수를 전달하고 싶으면 위에 button onClick처럼 콜백으로 전달하면 된다.
  • state가 배열 형태일 때, 참조 주소가 같으면 state가 변화한 것으로 인식하지 않는다.