본문 바로가기

WINK-(Web & App)/React.js 스터디

[2024 여름방학 React.js 스터디] 이정욱 #3주차 리액트 다시 re act 해보자

반응형

그전에 리액트를 엄청 간단하게 배워본적은 있지만 이번 방학에 리액트를 좀 공부해보고 싶은 마음이 생겨서
공부를 하기로 결심했다.

늦었으니까 시작하자

이번 스터디에서는 벨로퍼트와 함께 하는 모던 리액트라는 교재로 스터디를 한다.
여기서 나오는 용어나 정의에 관련한 부분은 리액트 공식 문서를 참고하기로 했다.
https://react.dev/

 

React

React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript. React is designed to let you seamlessly combine components written by independent people, teams, and organizati

react.dev

1. 리액트는 어떻게 만들어졌을까?

기존의 Javascript 만으로 DOM을 변형하여 HTML을 변경하던 형태에서 점점 웹의 규모가 커져가면서

이벤트 핸들러와 DOM 의 구조가 복잡해지기 시작했다.

그렇게되면 관리해야할 요소가 너무 많아지기 때문에 자칫 아래와 같이 복잡한 형태가 될 수 있다.

 

그래서 Ember, Backbone, AngularJS 등의 프레임워크가 만들어졌는데
자바스크립트의 특정 값이 바뀌면 특정 DOM의 속성이 바뀌도록 연결을 해주어 업데이트 작업을 간소화 해준다.

그러나 리액트의 경우에는 어떠한 상태가 바뀌게 되면 아예 다 날리고 다시 처음부터 만든다!
그렇다면 항상 새롭게 화면을 랜더링한다면 느리지 않을까? 라는 의문이 든다

 

그러한 문제점을 Virtual DOM 이라는 개념을 통해 해결했다.

Virtual DOM 이란?

UI의 이상적인 가상의 형태를 메모리에 저장하고
ReactDOM과 같은 라이브러리에 의해 "실제" DOM과 동기화하는 프로그래밍 개념이다.

 

이러한 과정을 Reconiliation(재조정) 이라고 한다.

 

Virtual DOM은 실제 DOM이 아닌 JS 객체 형태로 메모리 안에 DOM과 같은 내용을 담고있다.
그렇다면 Virtual DOM을 어떻게 활용하여 효율적으로 실제 DOM을 조작하게 될까?

리액트는 항상 두 개의 Virtual DOM 객체를 가지고 있다.
1. 랜더링 이전 화면 구조를 나타내는 Vitual DOM

2. 랜더링 이후 화면에 보이는 구조를 나타내는 Virtual DOM

리액트는 State가 바뀔 때 마다 Re-Rendering이 발생하는데 그때마다 새로운 Virtual DOM을 생성한다.
그렇게 생겨난 두가지 Virtual DOM을 Diffing을 통해 비교하여 어떤 Element에 차이가 있는지를 파악한다.

그렇게 파악된 Element를 집단화하여 한번에 실제 DOM에 변경사항을 모두 적용하는 방식이다.

2. 작업환경 준비

나는 WebStorm을 사용하여 진행하기로 했다.
WebStorm은 각종 편리한 기능이 VScode에 비해 많지만 기능이 많은 만큼 무겁다 라는 단점이 있다.

짠 성공한 모습

3. 나의 첫번째 리액트 컴포넌트

리액트 컴포넌트는 클래스형함수형 두가지 방식으로 만들수 있다.

요즘에는 클래스 형이 아닌 함수형으로 사용하게 되는데 그 이유는 나중에..

import React from 'react';

const Hello = () => {
    return <div>안녕하세요</div>
}

export default Hello;

 

컴포넌트를 만들때는 두가지가 필수적인데

1. import React from 'react';

2. export default 함수명;

 

이 두가지가 꼭 들어가야한다.

 

export를 통해 내보낸 컴포넌트를

 

App.js

import React from 'react';
import Hello from "./Hello";

function App() {
  return (
      <div>
        <Hello/>
      </div>
  );
}

export default App;


여기서 아까 내보낸 Hello 컴포넌트를 사용한다

한번 만들어낸 컴포넌트는 다음과 같이 여러번 재사용이 가능하다.

App.js

import React from 'react';
import Hello from "./Hello";

function App() {
  return (
      <div>
        <Hello/>
        <Hello/>
        <Hello/>
      </div>
  );
}

export default App;

 

이렇게 여러번 재사용하면

짜잔 잘 나온다..

 

근데 잘 보면 App또한 컴포넌트이다. 그렇다면 App은 어디서 사용되는 컴포넌트일까

 

Index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

App은 index.js에서 사용되는데 잘 보면 root라는 아이디로 부터 ReactDOM을 생성한다.

 

여기서 ReactDom을 생성해주고 거기서 render하여 root라는 id의 DOM Element에 랜더링한다

 

Index.js

<div id="root"></div>

4. JSX의 기본 규칙 알아보기

JSX는 아래 코드와 같이 얼핏보면 HTML 같지만 실제로는 Javascript로 동작하는 문법이다.

return <div>안녕하세요</div>;

 

이렇게 작성된 XML 형태의 코드를 babel이 JSX를 JavaScript로 변환해준다.

JSX 규칙들

1. 태그는 꼭 닫혀있어야 한다.

- 참고로 태그 사이에 내용이 들어가지 않는 br과 같은 태그도 <br/> 과 같이 Self Closing 태그를 사용해야 한다.

 

2. 두개 이상의 태그는 꼭 태그로 감싸져있어야 한다.

import React from 'react';
import Hello from './Hello';

function App() {
  return (
    <Hello />
    <div>안녕히계세요.</div>
  );
}

export default App;

 

다음과 같은 형태이면 오류가 발생한다.
단순히 div 태그로 감싸게 되면 불필요하게 복잡해지는 경우도 생기는데
이를 방지하기 위해 리액트의 Fragment 라는 것을 사용하면 된다.

 

import React from 'react';
import Hello from './Hello';

function App() {
  return (
    <>
      <Hello />
      <div>안녕히계세요</div>
    </>
  );
}

export default App;

 

Fragment란 아무 이름없는 빈 태그를 말한다.

3. JSX 안에 자바스크립트 값 사용

function App() {
  const name = 'react';
  return (
    <>
      <Hello />
      <div>{name}</div>
    </>
  );
}

이런식으로 {}로 감싸준다.

 

4.style 과 className

import React from 'react';
import Hello from './Hello';

const App = () => {
  const name = 'react';
  const style = {
    backgroundColor: 'black',
    color: 'aqua',
    fontSize: 24, // 기본 단위 px
    padding: '1rem' // 다른 단위 사용 시 문자열로 설정
  }

  return (
    <>
      <Hello />
      <div style={style}>{name}</div>
    </>
  );
}

 

인라인 스타일의 경우에는 객체 형태로 작성해야 하고
background-color 처럼 - 이 포함된 경우 camelCase로 작성해준다.

 

CSS class를 설정하는 경우에는 class= 가 아닌 className= 으로 설정해주어야 한다.

    <>
      <Hello />
      <div style={style}>{name}</div>
      <div className="gray-box"></div>
    </>

 

5.주석

JSX 내부의 주석은 {/* 이런 형태로 */} 작성한다.

  return (
    <>
      {/* 주석은 화면에 보이지 않습니다 */}
      /* 중괄호로 감싸지 않으면 화면에 보입니다 */
      <Hello 
      />
      <div style={style}>{name}</div>
      <div className="gray-box"></div>
    </>
  );

 

5. props 를 통해 컴포넌트에게 값 전달하기

props란 properties의 줄임말로 우리가 어떠한 값을 컴포넌트에게 전달해줘야 할 때 사용한다

props의 기본 사용법

예를 들어, App 컴포넌트에서 Hello 컴포넌트를 사용 할 때 name이라는 값을 전달해주고 싶다면?

App -> Hello 로

 

App.js

import React from 'react';
import Hello from './Hello';

const App = () => {
  return (
    <Hello name="react" />
  );
}

export default App;

Hello.js

import React from 'react';

const Hello = (props) => {
  return <div>안녕하세요 {props.name}</div>
}

export default Hello;

 

전달된 props는 props.name 과 같은 형태로 조회할 수 있다.

여러개의 props, 구조 분해 할당

Hello 컴포넌트에 또 추가적으로 color 라는 props를 전달한다.

import React from 'react';

const Hello = ({ color, name }) => {
  return <div style={{ color }}>안녕하세요 {name}</div>
}

export default Hello;

그렇다면 기존에 props로 받아오던 파라미터를  { color, name } 과 같이 구조 분해 할당으로 받아온다.

그러면 props.color와 같은 형태가 아닌 color 와 같이 바로 사용할 수 있다.

 

defaultProps로 기본값 설정

커포넌트에 props를 지정하지 않았을 때 기본적으로 사용할 값을 설정하고 싶다면
defaulProps 라는 값을 설정하면 된다.

 

Hello.js 에서 defaultProps를 아래와 같이 설정해주고

import React from 'react';

const Hello = ({ color, name }) => {
  return <div style={{ color }}>안녕하세요 {name}</div>
}

Hello.defaultProps = {
  name: '이름없음'
}

export default Hello;

 

App.js 에서 아래와 같이 name이라는 props가 없이 전달하면

import React from 'react';
import Hello from './Hello';

const App = () => {
  return (
    <>
      <Hello name="react" color="red"/>
      <Hello color="pink"/>
    </>
  );
}

export default App;

이렇게 default 값으로 나오게 된다.

 

props.children

컴포넌트 태그 사이에 넣은 값을 조회하고 싶다면

props.childer 을 조회하면 된다.

 

Wrapper.js

import React from 'react';

const Wrapper = () => {
  const style = {
    border: '2px solid black',
    padding: '16px',
  };
  return (
    <div style={style}>

    </div>
  )
}

export default Wrapper;

 

다음과 같은 컴포넌트를 App에서 Hello 컴포넌트를 감싸게 되면 Hello 컴포넌트의 내용이 안보인다.

 

이 내용을 나타내기 위해서는 

import React from 'react';

const Wrapper = ({ children }) => {
  const style = {
    border: '2px solid black',
    padding: '16px',
  };
  return (
    <div style={style}>
      {children}
    </div>
  )
}

export default Wrapper;

다음과 같이 childern을 통해 보여준다.

6. 조건부 렌더링

조건부 렌더링이랑 말 그대로 특정 조건에 따라 다른 결과를 렌더링해주는 것이다.

 

App.js

const App = () => {
  return (
    <Wrapper>
      <Hello name="react" color="red" isSpecial={true}/>
      <Hello color="pink" />
    </Wrapper>
  )
};

 

다음과 같이 isSpecial 이라는 props의 값을 bool 값으로 해주었다.

이제 이걸 전달받아 화면에 어떻게 보여줄지 정하는데

여기서는 삼항 연산자를 통해 조건부 렌더링을 해준다.

 

Hello.js

const Hello = ({ color, name, isSpecial }) => {
  return (
    <div style={{ color }}>
      { isSpecial ? <b>*</b> : null }
      안녕하세요 {name}
    </div>
  );
}


삼항연산자를 활용하는 상황은 특정 조건에 따라 렌더링할 내용이 달라질 때이다.

그러나 방금은 상황에 따라 보여줄지 말지를 결정하는 것이므로

단축 평가 논리 계산법으로 하는 것이 적절하다.

 

Hello.js

const Hello = ({ color, name, isSpecial }) => {
  return (
    <div style={{ color }}>
      {isSpecial && <b>*</b>}
      안녕하세요 {name}
    </div>
  );
}

 

만약 props 값 설정을 생략해주고 그냥 isSpecial 이라는 변수명만 전달한다면

이는 기본적으로 true로 간주한다.

 

7. useState 를 통해 컴포넌트에서 바뀌는 값 관리하기

지금까지는 모두 정적인 페이지를 생성하는 일만 있었다.

 

동적인 페이지를 생성할 때는 useState 라는 함수를 사용해아하는데

이것이 리액트의 Hooks 중 하나이다.

 

우선 버튼을 누르면 숫자가 바뀌는 Counter 예제를 작성하기 위해 Counter.js를 작성하고

 

Counter.js

import React from 'react';

const Counter = () => {
    return (
        <div>
            <h1>0</h1>
            <button>+1</button>
            <button>-1</button>
        </div>
    )
}

export default Counter;

 

App.js 에서 렌더링해준다

import React from 'react';
import Counter from "./Counter";

const App = () => {
  return (
      <div>
        <Counter/>
      </div>
  );
}

export default App;

 

이제 여기에 이벤트가 발생(버튼이 클릭)했을때 특정 함수가 호출되도록 설정한다

 

Counter.js

import React from 'react';

const Counter = () => {
    const onIncrease = () => {
        console.log('+1')
    }
    const onDecrease = () => {
        console.log('-1');
    }
    return (
        <div>
            <h1>0</h1>
            <button onClick={onIncrease}>+1</button>
            <button onClick={onDecrease}>-1</button>
        </div>
    );
}

export default Counter;

 

주의할 점은 onClick에 함수를 지정해줄 때 함수를 실행하는 것이 아닌 함수명을 입력해준다.

 

컴포넌트에서는 이 카운터에서는 0과 같이 동적인 값을 state라고 한다.

리액트에서는 이 state를 관리하기 위해 useState를 사용한다.

import React, { useState } from 'react';

- 우선 useState를 사용하기위해 리액트 패키지에서 useState를 불러온다

 

const [number, setNumber] = useState(0);

- 그리고 state인 값들을 useState를 통해 호출해주면
첫 번째 원소는 현재 상태
두번째 원소는 Setter 함수로 사용할 함수이다.

 

그리고 

<h1>{number}</h1>

를 통해 화면이 렌더링 해준다

 

8. input 상태 관리하기

사용자가 입력할 수 있는 input 상태를 관리하는 법을 알아본다.

우선 InputSample.js 라는 파일을 만들어준다

import React from 'react';

const InputSample = () => {
    return (
        <div>
            <input />
            <button>초기화</button>
            <div>
                <b>값: </b>
            </div>
        </div>
    );
}

export default InputSample;

입력창과, 버튼 하나와 값을 띄워주는 창이 있다.

여기에 input에 값을 입력하면 하단 값 부분에 뜨게 하고

초기화 버튼을 누르면 값이 비워지도록 useStateonChange를 통해구현을 해본다.

 

InputSample.js

import React, { useState } from 'react';

const InputSample = () => {
    const [text, setText] = useState('');

    const onChange = (e) => {
        setText(e.target.value);
    };

    const onReset = () => {
        setText('');
    };

    return (
        <div>
            <input onChange={onChange} value={text}  />
            <button onClick={onReset}>초기화</button>
            <div>
                <b>값: {text}</b>
            </div>
        </div>
    );
}

export default InputSample;

 

여기서 주의깊게 봐야할 봐야할 부분은 e 즉 이벤트 객체이다

onChange 라는 함수의 파라미터인 e는 이 객체의 e.target 은 input 태그이다.

 

왜냐? input 태그에 onChange 이벤트의 값으로 onChange 라는 함수명을 줬기 때문이다.

 

이 말은 input 태그에 이벤트가 발생하면 해당 input 태그를 타겟으로 하여 onChagne 함수에게 알려준다는 의미이다.

9. 여러개의 input 상태 관리하기

만약에 input이 여러개라면? 어떻게 해야할까

다음과 같이 이름과 닉네임을 동시에 입력받아 화면에 보여주는 홈페이지를 만드려한다.

 

InputSample.js

import React, { useState } from 'react';

const InputSample = () => {
    const onChange = (e) => {
    };

    const onReset = () => {
    };


    return (
        <div>
            <input placeholder="이름" />
            <input placeholder="닉네임" />
            <button onClick={onReset}>초기화</button>
            <div>
                <b>값: </b>
                이름 (닉네임)
            </div>
        </div>
    );
}

export default InputSample;

 

input의 개수가 여러개가 된다면 useState를 여러번 사용하면 될까?

가능은 하지만 좋은 방법이 아니다.

이를 위해서 각 input 태그에 name을 지정해준 뒤 이벤트가 발생하면 name 값을 참조하도록 한다.

 

전체 코드는 아래와 같다

 

InputSample.js

import React, { useState } from 'react';

const InputSample = () => {
    const [inputs, setInputs] = useState({
        name: '',
        nickname: ''
    });

    const { name, nickname } = inputs; 

    const onChange = (e) => {
        const { value, name } = e.target; 
        setInputs({
            ...inputs, 
            [name]: value 
        });
    };

    const onReset = () => {
        setInputs({
            name: '',
            nickname: '',
        })
    };


    return (
        <div>
            <input name="name" placeholder="이름" onChange={onChange} value={name} />
            <input name="nickname" placeholder="닉네임" onChange={onChange} value={nickname}/>
            <button onClick={onReset}>초기화</button>
            <div>
                <b>값: </b>
                {name} ({nickname})
            </div>
        </div>
    );
}

export default InputSample;

 

바뀐 부분을 자세히 보면

  const [inputs, setInputs] = useState({
    name: '',
    nickname: ''
  });

우선 name과 nickname을 useState를 통해 지정해준다

 

그리고 inputs 를 구조 분해 할당으로 name과 nickname으로 읽어온다

 

  const onChange = (e) => {
    const { value, name } = e.target; // 우선 e.target 에서 name 과 value 를 추출
    setInputs({
      ...inputs, // 기존의 input 객체를 복사한 뒤
      [name]: value // name 키를 가진 값을 value 로 설정
    });
  };

 

이부분이 가장 이해하기 어려웠는데 

 

setInputs 함수는 상태를 업데이트하는 역할을 한다

  • ...inputs: 스프레드 연산자(...)를 사용하여 inputs 객체의 모든 속성을 복사한다.
    예를 들어, inputs 객체가 { name: 'Alice', nickname: 'Ally' }라면, { ...inputs }는 { name: 'Alice', nickname: 'Ally' }가 된다..
  • [name]: value: 대괄호 표기법을 사용하여 객체의 키를 동적으로 설정한다.
    여기서 name은 현재 이벤트가 발생한 <input> 요소의 name 속성값이고, value는 그 <input> 요소의 현재 값입니다. 예를 들어, name이 "nickname"이고 value가 "Bob"이라면, [name]: value는 { nickname: 'Bob' }이 된다

따라서 setInputs 함수가 호출되면, inputs 객체는 다음과 같이 업데이트된다:

  1. 기존의 inputs 객체를 복사하고
  2. name 키를 가진 값을 value로 업데이트한다

예를 들어, inputs가 { name: 'Alice', nickname: 'Ally' }였고, name이 "nickname", value가 "Bob"라면,
업데이트된 inputs 객체는 { name: 'Alice', nickname: 'Bob' }가 된다.

이렇게 함으로써 onChange 함수가 호출될 때마다 특정 input 요소의 값이 상태 객체에 반영된다.

반응형