• [ReactJS] 1. 시작하기

    2022. 2. 1.

    by. 나나 (nykim)

    320x100

    Abstract vector created by fullvector - www.freepik.com

    이 글은 NomadCoders의 <ReactJS로 영화 웹 서비스 만들기> 강의 내용을 정리한 글입니다.
    넘나 좋은 강의 (게다가 무료!) 🙇🏻‍♀️ 감사합니다 🙇🏻‍♀️ 

    개인 스터디 글로, 맞지 않는 내용이나 더 나은 방법을 공유해 주신다면 복받으실 거예요 👼🙌
    1) 시작하기 ☀︎
    2) 기능 연습 & 3) 앱 만들기
    4) styled-components
    5) 𝒘𝒊𝒕𝒉 타입스크립트

     


     

    1. 시작하기

    1-1. 작업 환경 준비

     

    먼저 node.js 가 설치됐는지 확인!

     

    $ npx create-react-app {app-name}

    CLI로 프로젝트를 생성합니다.

     

    참고로 {app-name}에는 대문자를 표시할 수 없으므로 대시(-)로 단어를 연결합시다.

    (🙅‍♀️ myMovie vs. 🙆‍♀️ my-movie)

     

    npx는 npm의 자식으로, npm@5.2.0 이상 버전만 깔려있다면 npx 커맨드를 사용할 수 있어요!

    npm -g로 설치하면 무거운 패키지가 로컬 스토리지에 남기 때문에 npx를 쓰는 게 좋다고 합니다.

    설치될 때 이미 package.json에 명령어가 다 입력되어 있기 때문에,

    $ npm start를 뙇 쳐주면 알아서 localhost:3000으로 웹페이지가 열립니다 (붐!)

     

    편집기에서 폴더를 열어 봅시다.

    src/ 폴더 내 index.js와 App.js가 있는 걸 볼 수 있습니다.

    index.js에서는 React와 ReactDOM을 불러와 ReactDom.render();를 해주고 있습니다.

    (codesandbox에서 라이브러리로 가져와 작성했던 것과 동일합니다.)

     

    차이점이 있다면 우리는 react.js와 react-dom.js를 각각 불러와서 하나하나 작성했지만,

    CRA에서는 알아서 이걸 해준다는 거죠 ;)

    <script>
    function App() {
      return (
        <div>
          Hello, Nana
        </div>
      );
    };
    
    const root = document.getElementById('root');
    ReactDOM.render(<App />, root);
    </script>
    /* CRA */
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    
    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );
    

     

    React.createElement() 로 ReactDOM을 생성해 쓰면 되지만 보통은 JSX 문법을 쓴 뒤 Babel이 해석하게끔 합니다.
    (CRA는 알아서 다 해쥼!)

    <!-- 직접 리액트돔을 만드는 경우 -->
    
    <script type="text/babel">
    const btn = React.createElement(
      'button',
      {
        class: 'myBtn',
        onClick: () => console.log('click'),
        onMouseEnter: () => console.log('mouseenter')
      },
      'Click!'
    );
    const container = React.createElement('div', null, [Button]);
    </script>
    /* CRA */
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    ReactDOM.render(
    	<button className="myBtn" 
    	        onClick={()=>console.log('click')}
    	        onMouseEnter={()=>console.log('mouseenter')}>
    	Click!
    	</button>,
      document.getElementById('container')
    );

     


     

    1-2. state 다루기

     

    리액트에서 state는 ‘바뀌는 데이터’라고 생각하면 됩니다.

    우선 버튼 하나를 만들고, 버튼을 클릭할 때마다 클릭한 횟수가 화면에 보이도록 해봅시다.

    리액트 내에서 쓰이는 변수와 함수는 {}를 사용해 걸어줍니다.

     

    function App() {
      let counter = 0;
      function countUp() {
        counter = counter + 1;
        console.log(counter);
      }
      return (
        <div>
          <h1>{counter}</h1>
          <button onClick={countUp}>Click me</button>
        </div>
      );
    }
    
    export default App;
    

     

    Tip. JSX내에서 emmet을 쓰고 싶다면 VS CODE에서 설정해 줍니다.

     

    {
       "emmet.includeLanguages": {
          "javascript": "javascriptreact"
       }
    }
    

     

     

    이론(?)은 완벽하지만 아쉽게도 화면에 변화는 없습니다.

    하지만 콘솔엔 증가하는 counter의 값이 계속 찍히고 있긴 한데요?_?

    이건 왜 그럴까요? 거야... 화면을 다시 안 그리고 있으니까요!

    변한 걸 보여주려면 화면을 다시 그리는 ‘리렌더링’ 작업이 필요합니다.

     

    물론 클릭할 때마다 모든 돔 요소를 새로 그릴 수는 없습니다.

    하지만 리액트의 짱짱한 점은? 리액트는 ‘바뀐 부분’만 재빠르게 슉슈슉(닌자인듯) 바꿔채 줍니다!

    따라서 리액트에게 ‘counter 값 지켜보다가 이거 바뀌면 이게 연결된 부분을 다시 그려줘’라고 말해주면 돼요.

     

    이때 써먹는 말이 React.useState()입니다.

    이건 리액트의 ‘Hook(훅)’ 중 하나인데, 리액트가 제공하는 리미티드 스페셜 함수라고 생각합시다.

    useState()에는 ‘저장할 state의 초기값’을 인자로 넘길 수 있어요. 여기서는 counter가 되겠죠!

    그리고 useState()는 무엇을 뱉느냐면, ‘state 변수’와 ‘이를 갱신할 수 있는 함수’ 이렇게 2가지를 반환합니다 (퉤퉤)

    함 써보죠!

     

    import { useState } from 'react';
    
    function App() {
      const data = useState();
      function onClick() {
        console.log(data);
      }
      return (
        <div>
          <h1>{data}</h1>
          <button onClick={onClick}>Click me</button>
        </div>
      );
    }
    
    export default App;
    

     

    { useState } 로 가져왔기 때문에 React.useState()에서 앞을 생략하고 useState()로 사용한 모습입니다.

     

     

    Tip. 구조 분해 할당 (=객체 조지기)
    객체의 내용을 꺼내 해체 🔪 시킨 다음 그 값을 개별 변수에 담는 방법입니다.

    const [a, b, c, d] = ['spring', 'summer', 'autumn', 'winter'];
    console.log(d); //'winter'
    
    const coffee = { name: ‘Americano’, type: ‘Iced’, price: 4500 };
    const {name, type, price} = coffe;
    console.log(coffe.price); //4500
    console.log(price); //4500
    

     

    콘솔을 열어 뭘 뱉었는지 확인해 봅시다. 배열이 보입니다.

    [0]은 undefined고, [1]은 함수네요.

    만약 useState(’nana’)로 썼다면 [0]의 값은 ‘nana’가 되겠죠.

     

     

    아하! 그럼 useState에다가 counter를 넣어주고, data[0]으로 가져와 쓰면 되겠네요.

    그리고 data[1]로 함수를 가져와 써볼 수 있겠고요.

    하지만 [0]이니 [1]이니 하는 건 헷갈리고 구리니까 얘도 구조 분해 할당을 해줍니다.

    const [counter, setCounter] = useState(0);/code> 정도면 되겠네요 😉

    (modifier는 보통 set+{state이름}/code> 형태로 명명합니다)

     

     

    만약 setCounter 를 안 쓰고 counter를 바꾸면 어떨까요?

    잠깐 const를 let으로 바꿔서 직접 counter를 조작해 봅시다.

     

    import { useState } from 'react';
    
    function App() {
      let [counter, setCounter] = useState(0);
      function onClick() {
        counter = counter + 1;
        console.log(counter);
      }
      return (
        <div>
          <h1>{counter}</h1>
          <button onClick={onClick}>Click me</button>
        </div>
      );
    }
    
    export default App;
    

     

    아까랑 똑같이 콘솔에만 찍히고 화면은 변하지 않습니다.

    그럼 이제 놀고있는 modifier함수, setCounter()를 써먹을 차례입니다.

     

    import { useState } from 'react';
    
    function App() {
      const [counter, setCounter] = useState(0);
      function onClick() {
        setCounter(counter + 1);
      }
      return (
        <div>
          <h1>{counter}</h1>
          <button onClick={onClick}>Click me</button>
        </div>
      );
    }
    
    export default App;
    

     

    와, state(여기서는 counter)가 바뀌면서 동시에 화면을 다시 그려줍니다!

    현재 state값(최초 0)을 받아오고 거기에 1을 더한 다음 <h1>1</h1>이라고 그려준 거죠!

    또 클릭하면 현재 state 값(이제 1)에 1을 더해 <h1>2</h1>을 그리고... (하략)

     

    덧붙여 여기서 setCounter()의 인자로 counter++를 넣을 수 없는데, ++ 연산자는 값을 "바꾸기" 때문입니다.
    ex. let a = 0; let b = a++; // a === 1

    우리는 read-only 인 counter를 직접 변경하는 게 아니라, set 함수를 통해 변경해야 합니다.

    그래서 counter에 단순히 1을 더한 값을 넘겨주면 되는데 (counter +1)

    counter++를 쓰면 counter의 값을 +1한 것으로 "변경"하려고 하기 때문에 에러가 발생합니다.

     

     

    이쯤에서 리액트 익스텐션을 설치하고 렌더링 시 하이라이트 되도록 설정해 줍니다.

     

     

    그리고 버튼을 클릭하면 ‘버튼을 포함한 App() 컴포넌트 전체’가 리렌더링 영역인 걸 볼 수 있어요.

    테스트를 위해 Text라는 컴포넌트를 만들고 <App/>과 병렬되도록 배치했습니다.

     

    /* index.js */
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import Text from './Text';
    
    ReactDOM.render(
      <React.StrictMode>
        <App />
        <Text />
      </React.StrictMode>,
      document.getElementById('root')
    );
    

     

    그런 다음 버튼을 클릭하면 이 Text 컴포넌트는 리렌더링 영역에 포함되지 않는 게 보입니다.

    왜냐면 Text에선 counter라는 state를 쓰고 있지 않으니까요.

    이게 바로 ‘바뀐 부분만 슉슈슉 바꿔치는’ 리액트가 일하는 모습입니다.

     

     

     

    setCounter 함수 내에 counter를 고대로 넣어도 되긴 하지만, 함수를 쓰면 더 안전해 집니다.

    혹시 모르죠, 어디 다른 데서 counter를 가져다 쓰고 있을 수도 있잖아요?!

    안전빵으로 함수형태로 state를 업데이트해 봅시다.

    modifier 함수의 인자로 함수를 넣을 경우, 그 함수에 들어가는 첫 번째 인자는 ‘현재 state’가 됩니다. (그냥 그렇게 만들었다고 합니다 🤷‍♀️)

    그리고 이 함수가 리턴한 값이 ‘새로운 state’가 됩니다.

     

    import { useState } from 'react';
    
    function App() {
      const [counter, setCounter] = useState(0);
      function onClick() {
        **setCounter((counter) => counter + 1);**
      }
      return (
        <div>
          <h1>{counter}</h1>
          <button onClick={onClick}>Click me</button>
        </div>
      );
    }
    
    export default App;
    

     

    State 연습하기!

    강의에서는 Converter 만들기 실습을 통해 연습하며, 관련 코드는 Codesandbox 에서 확인할 수 있습니다.

    • JSX 내 일부 HTML 예약어는 사용할 수 없기에 대체해야 합니다. (className, htmlFor 등)
    • JSX 내 삼항 연산자나 단축 평가 논리를 사용할 수 있습니다.
      ex. <input value={ inverted ? amount * 60 : amount }/>
      <div> { index === "0" ? <MinutesToHours/> : null } </div>
      <div> {isAdmin && <b>admin</b>}{userName} </div>

     


     

    1-3. Props

     

    Props = Properties = 부모 컴포넌트가 자식 컴포넌트에게 이래라저래라 잔소리(?)

    실습을 위해 버튼 두 개를 만들어 봅니다.

     

    /* Button.js */
    
    function Button(){
      return <button>버튼</button>
    }
    
    export default Button;
    
    /* App.js */
    
    import Button from './Button';
    
    function App() {
      return (
        <div>
          <Button/>
          <Button/>
        </div>
      );
    }
    
    export default App;
    

     

    이때 첫 번째 버튼과 두 번째 버튼은 서로 유사하지만 조금 다르게 설정해 주고 싶습니다.

    예를 들어, 텍스트나 다르다거나 클래스가 다르다고 해보죠. 이러한 값들을 부모 컴포넌트(App)가 자식 컴포넌트(Button)에게 ‘잔소리’해서 바꿀 수 있습니다.

    이 잔소리가 바로 Props입니다.

     

    /* App.js */
    
    import Button from './Button';
    
    function App() {
      return (
        <div>
          <Button text="nana" color="red"/>
          <Button text="like" color="blue"/>
        </div>
      );
    }
    
    export default App;
    

     

    위와 같이 써두면 text와 color라는 props들이 Button 컴포넌트에게 전달되겠죠!

    자식 컴포넌트에서 넘겨받는 첫 번째이자 유일한 인자는 props입니다. 여기에 객체 형태로 쏙 들어갑니다.

     

    /* Button.js */
    
    function Button(props){
    	console.log(props)
      return <button>버튼</button>
    }
    
    export default Button;
    

    그럼 넘겨받은 이걸 버튼 내에 써먹을 수 있겠네요!

     

    /* Button.js */
    
    function Button(props) {
      return <button className={props.color}>{props.text}</button>;
    }
    
    export default Button;
    

     

    아, 구조 분해 할당을 해서 더 짧게 써먹으면 더 좋고요!

     

    /* Button.js */
    
    function Button({ text, color }) {
      return <button className={color}>{text}</button>;
    }
    
    export default Button;
    

     

     

     


     

     

    1-4. Memo

     

    그럼 props에 텍스트 말고 딴 것도 넣어봅시다. 함수를 넣어줄 수도 있겠네요.

    버튼을 클릭하면 텍스트가 “Hello!”으로 바뀌도록 설정해 볼게요.

    텍스트 상태를 관리하기 위해 우선 useState()를 사용합니다.

     

    /* App.js */
    
    import { useState } from 'react';
    import Button from './Button';
    import './styles.css';
    
    function App() {
      const [value, setValue] = useState('nana');
      const onClick = () => setValue('Hello');
    
      return (
        <div>
          <Button text={value} color="red" onClick={onClick} />
          <Button text="like" color="blue" />
        </div>
      );
    }
    
    export default App;
    

     

    이렇게 하면 당연히... 아무 일도 안 일어납니다!

    자식 컴포넌트(Button)에게 넘긴 onClick은 이벤트리스너가 아니고 그냥 평범한 props일 뿐입니다.

    이 props를 받아와서 어떻게 처리해야할지는 우리가 지정해줘야 합니다.

    만약 이 button이 단순한 구조가 아니고 button > p > span + i 이렇게 복잡한 마크업이라고 했을 때, props로 넘긴 onClick를 대체 누구한테 써먹어야할지 리액트는 알 수 없습니다.

     

    자식 컴포넌트로 넘어가서 onClick 이벤트를 붙여 줍니다.

     

    /* Button.js */
    
    function Button({ color, text, onClick }) {
      return (
        <button onClick={onClick} className={color}>
          {text}
        </button>
      );
    }
    
    export default Button;
    

     

    이때 콘솔에 찍거나 하이라이트를 살펴보면 버튼 클릭 시 ‘리렌더링’되는 모습을 볼 수 있습니다.

    부모 컴포넌트에서 state 변경이 일어났기 때문에 버튼이 다시 그려진 거죠.

    문제는 nana 버튼이 리렌더링되면 like 버튼도 리렌더링 됩니다. 굳이 그럴 필요가 없는데 말이죠.

     

    이럴 땐 ‘얘 props가 변경되는 게 아니라면 다시 그리지 말고 지금 상태를 기억해’라고 말해줄 수 있습니다.

    바로 React.memo()를 써서요.

     

    /* App.js */
    
    import { useState, memo } from 'react';
    import Button from './Button';
    import './styles.css';
    
    function App() {
      const [value, setValue] = useState('nana');
      const onClick = () => setValue('Hello');
      const MemorizedBtn = memo(Button);
    
      return (
        <div>
          <MemorizedBtn text={value} color="red" onClick={onClick} />
          <MemorizedBtn text="like" color="blue" />
        </div>
      );
    }
    
    export default App;
    

     

     

    여기서 중요한 점은 부모의 state가 변경되면 모든 자식 컴포넌트는 다시 그려진다는 것,
    그리고 props 변경이 일어난 게 아니라면 굳이 다시 그리지 않도록 컨트롤할 수 있다는 것이겠네요.

     

     


     

     

    1-5. PropTypes

     

     

    props를 제대로 된 타입으로 넘겨줬는지 아닌지 체크하는 단계가 있으면 참 좋을 텐데요.

    이럴 때는 PropTypes라는 패키지를 쓰면 됩니다.

     

    PropTypes를 설치해서 props가 올바른 타입으로 넘어왔는지 체크해 봅시다.

    npm으로 설치해 주자고요!

    $ npm i prop-types

     

     

    이번에는 이 버튼에 대한 스타일을 ‘CSS 모듈’로 만들어 적용해 보겠습니다.

    Button.module.css에 스타일을 작성 후 import해 줍니다.

     

    /* Button.module.css */
    
    .btn {
      background-color: tomato;
      color: #fff;
    }
    

     

    모듈 형태로 CSS를 사용할 때는, JSX 내 className 속성을 styles.{클래스명} 형태로 사용합니다.

     

    /* Button.js */
    
    import PropTypes from 'prop-types';
    import styles from './Button.module.css';
    
    function Button({ text }) {
      return <button className={styles.btn}>{text}</button>;
    }
    
    Button.propTypes = {
      text: PropTypes.string.isRequired
    };
    
    export default Button;
    

     

    컴포넌트명.propTypes 객체에 우리가 기대하는 타입을 넣어주면 됩니다.

    {props명}: PropTypes.{string || number || array...} 이런 식으로요!

    그리고 이게 필수값이면 .isRequired 를 붙여주면 되고요.

     

    이렇게 해두면 잘못된 props를 입력하더라도 친절하게 알려줍니다.

     

     

     

    defaultProps 프로퍼티를 통해 props의 초깃값을 정의할 수 있습니다.

     

    /* Button.js */
    
    import PropTypes from 'prop-types';
    import styles from './Button.module.css';
    
    function Button({ text }) {
      return <button className={styles.btn}>{text}</button>;
    }
    
    Button.propTypes = {
      text: PropTypes.string.isRequired
    };
    
    Button.defaultProps = {
      text: '나는 버튼이다'
    };
    
    export default Button;
    

     

     


    1-6. useEffect

     

    “state 변동이 일어나면 그 state를 사용하는 컴포넌트는 리렌더링 된다”고 배웠습니다.

     

    예를 들어 App() 컴포넌트 내에 stateA와 stateB가 있다고 해볼게요.

    stateA의 변동이 일어날 경우, App()은 다시 렌더링됩니다.

    stateB의 변동이 일어날 경우, App()은 다시 렌더링됩니다.

    넵, 둘은 다른 state인데도 불구하고요!

     

    /* App.js */
    
    import { useEffect, useState } from 'react';
    
    function App() {
      const [counter, setCounter] = useState(0);
      const [keyword, setKeyword] = useState('');
    
      const onClick = () => setCounter((prev) => prev + 1);
      const onChange = (e) => setKeyword(e.target.value);
      
      console.log(`SEARCH FOR ${keyword}`);
    
      return (
        <div>
          <input
            type="text"
            placeholder="Search here..."
            value={keyword}
            onChange={onChange}
          />
          <h1>{counter}</h1>
          <button onClick={onClick}>Click Me</button>
        </div>
      );
    }
    
    export default App;
    

     

    App 컴포넌트에는 2개의 state가 달려있습니다. counter와 keyword입니다.

     

    먼저 SEARCH FOR ${keyword} 콘솔은 인풋에 검색어를 입력할 때마다 출력됩니다.
    (➤ input에 달린 onChange가 keyword라는 state를 변경하기 때문)

    하지만 버튼을 클릭할 때도 쓸데없이 출력됩니다.
    (➤ button에 달린 onClick이 counter라는 state를 변경하기 때문)

     

    이렇게 컴포넌트 내 있는 모든 state 변화에 하나하나 반응해 리렌더링되는 대신,

    특정한 경우에만 렌더링되도록 하려면 useEffect를 사용합니다.

    여기엔 두 개의 인자를 넣을 수 있는데, 첫 번째는 실행시키고 싶은 코드, 두 번째는 dependencies(deps)입니다.

     

    useEffect(() => {
        console.log(`SEARCH FOR ${keyword}`);
      }, []);
    

     

    단, 이렇게 하면 최초 실행 시 딱 한 번만 실행됩니다.

    왜냐면 deps 배열 [ ] 이 비어져있기 때문이죠.

    여기에 state를 넣을 경우, 해당 state가 변화할 때 이 코드가 실행됩니다.
    (=”이거 잘 지켜보고 있다가, 얘가 변하면 실행해!”)

     

    useEffect(() => {
      console.log(`SEARCH FOR ${keyword}`);
    }, [keyword]);
    

     

    여기에는 조건을 걸어줄 수도 있겠죠 ;)

     

    useEffect(() => {
      if (keyword !== '' && keyword.length >= 5) {
        console.log(`SEARCH FOR ${keyword}`);
      }
    }, [keyword]);
    

     

    그럼 이제 버튼을 누를 때는 해당 콘솔이 찍히지 않게 됩니다.

     

     

     


     

     

    1-7. cleanup

     

    import React, { useEffect, useState } from 'react';
    
    function Hello() {
      return (
        <div>
          <br />
          <strong>Hello!</strong>
        </div>
      );
    }
    
    function Test() {
      const [showing, setShowing] = useState(false);
      const onClick = () => {
        setShowing((prev) => !prev);
      };
      return (
        <div>
          <button onClick={onClick}>{showing ? 'hide' : 'show'}</button>
          {showing ? <Hello /> : null}
          <hr />
        </div>
      );
    }
    
    export default Test;
    

     

    버튼을 토글할 때마다 컴포넌트가 보여지고 사라지도록 하는 코드입니다.

    여기에 useEffect()를 사용해 보겠습니다. 최초 렌더링 시 ‘Hello’를 말하게 해보죠!

     

    useEffect(() => {
      console.log('Hello!');
    }, []);
    

     

    그럼 버튼을 눌러 컴포넌트가 화면에 그려질 때 콘솔에 찍히게 됩니다.

     

     

    여기서 `{showing ? <Hello /> : null}` 코드가 실행될 때 <Hello/> 컴포넌트를 숨겼다 보여주는 게 아니고 매번 ‘새롭게’ 그려냅니다.

    즉, <Hello /> 컴포넌트가 필요없어지는 순간 와장창 파괴됩니다.

     

    그리고 이렇게 파괴될 때 실행되는 함수가 있으며 이를 cleanup 함수라고 부릅니다.

    ‘useEffect 내 함수가 리턴하는 함수’가 바로 cleanup 함수에요.

     

    useEffect(() => {
      console.log('Hello!');
      return () => console.log('Bye!'); // 컴포넌트가 파괴될 때 리턴한 함수가 실행됨 (=cleanup function)
    }, []);
    

     

     

     

    참고

     

     

    728x90

    댓글