본문 바로가기

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

[2024 React.js 스터디] 류상우 #4주차

반응형
01. 리액트는 어쩌다 만들어졌을까?

JavaScript를 사용해 HTML로 구성한 UI를 제어할 때, 특정 DOM을 선택한 후 어떤 이벤트가 발생했을 때 변화를 주도록 설정한다.

다음은 HTML과 JS로 만든 간단한 카운터이다.

<h2 id="number">0</h2>
<div>
  <button id="increase">+1</button>
  <button id="decrease">-1</button>
</div>
const number = document.getElementById('number');
const increase = document.getElementById('increase');
const decrease = document.getElementById('decrease');

increase.onclick = () => {
  const current = parseInt(number.innerText, 10);
  number.innerText = current + 1;
};

decrease.onclick = () => {
  const current = parseInt(number.innerText, 10);
  number.innerText = current - 1;
};

이 코드는 버튼이 눌리면 #number인 DOM을 선택하여 텍스트를 +1 혹은 -1을 하는 규칙이 있다.

위와 같이 사용자와 상호작용이 별로 없는 웹페이지가 아닌 다양한 상호작용이 발생하고 이에 따라 동적인 UI를 표현해야 하는 웹페이지일 때,  DOM을 직접 건드리면서 작업하면 코드가 난잡해지기 쉽다.

 

이를 해결하기 위해 만들어진 프레임워크가 Ember, Backbone, AngularJS 등이다. 이 프레임워크들은 자바스크립트의 특정 값과 특정 DOM의 속성을 연결하여 업데이트를 간소화하는 방식으로 웹개발의 어려움을 해결하였다.

 

리액트의 작동 방식은 다르다. 리액트는 어떠한 상태가 바뀌었을 때 DOM을 어떻게 업데이트할지 규칙을 만드는 것이 아니라, 처음부터 모든 걸 새로 만들어서 보여준다.

동적인 UI 를 보여주기 위해서 모든 걸 다 날려버리고 모든 걸 새로 만들게 된다면, 속도가 굉장히 느릴 텐데 이를 해결하기 위한 게 바로 Virtual DOM이다. Virtual DOM이란 실제 DOM이 아닌 메모리에 가상으로 존재하는 DOM으로 JavaScript객체이다. 리액트는 상태가 업데이트되면 필요한 곳을 Virtual DOM을 통해 렌더링 한 후 실제 DOM과 비교하여 바뀐 부분을 패치한다. 이를 통하여 코드의 간략화와 빠른 성능을 지켜낼 수 있게 된 것이다.

DOM과 Virtual DOM의 업데이트 방식 (https://coding-medic.com/2020/11/10/the-virtual-dom/)


02. 작업환경 준비

리액트를 공부하기 앞서 작업환경을 준비하기 위해 다음 항목들을 설치한다.

우선 Windows에서 VS Code의 터미널로 Git Bash를 사용하기 위해  VS Code 에서 Ctrl + , 를 눌러 설정에 들어간 후, terminal 을 검색 후 Terminal > External > Windows Exec 부분에 Git Bash 의 경로를 입력해 주었다.

이후 터미널을 열고 다음 명령어를 실행한다.

$ npx create-react-app begin-react

$ cd begin-react
$ yarn start

VS Code에서 새 react 프로젝트를 실행한 모습.


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

리액트 컴포넌트란 간단히 말해 재사용 가능한 UI조각으로 다음과 같은 특징을 가진다.

  • 함수 혹은 클래스 형태로 만들 수 있다.
  • XML(eXtensible Markup Language) 형식의 값을 반환할 수 있는데 이를 JSX(JavaSCript XML)라고 부른다. 

그럼 첫 번째 리액트 컴포넌트를 만들어보자.

src 디렉터리 아래 Hello.js라는 파일을 다음과 같이 작성한다.

import React from 'react';

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

export default Hello;

 

리액트 컴포넌트를 만들 때는

import React from 'react';

를 통해 리액트를 불러와야 한다.

 

코드의 마지막 줄인

export default Hello;

이 코드는 Hello라는 컴포넌트를 내보낸다는 뜻으로 이렇게 해주면 다른 컴포넌트에서 불러와 사용할 수 있다.

 

이 컴포넌트를 App.js 에서 불러와 사용해 보자

App.js를 다음과 같이 작성한다.

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

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

export default App;

이처럼 컴포넌트를 여러 번 불러올 수도 있다.

 

이제 index.js를 열어보면

const root = ReactDOM.createRoot(document.getElementById('root'));

 

이런 코드가 보이는데 브라우저에 있는 실제 DOM 내부에 리액트 컴포넌트를 렌더링 하겠다는 것을 의미한다. #root인 DOM을 선택하고 있는데 이 DOM은 public/index.html에 있다. 즉, 리액트 컴포넌트가 렌더링 된 결과물이  div 내부에 렌더링 되는 것이다.


 

04. JSX

리액트 컴포넌트 파일에서 XML 형태로 코드를 작성하면 babel 이 JSX 를 JavaScript 로 변환해 준다.

  • JSX: 리액트에서 생김새를 정의하는 문법으로 HTML처럼 생겼지만 JavaScript이다.
  • bable: 자바스크립트의 문법을 확장해 주는 도구이다. 지원하지 않거나 실험적인 자바스크립트 문법들을 정식 자바스크립트 형태로 변환해 구형 브라우저 같은 환경에서도 제대로 실행할 수 있게 해 준다.

JSX가 JavaScript로 제대로 변환이 되려면 다음과 같은 규칙을 지켜야 한다.

  • 태그는 꼭 닫혀야 한다. HTML처럼 <input> 이나 <br>을 닫지 않고 사용하면 안 된다. 
    • Self Closing 태그: 태그 사이에 내용이 들어가지 않을 때 사용하며 열리고 바로 닫히는 태그를 의미한다.
      <br />같이 작성한다.
function App() {
  return (
    <div>
      <Hello></Hello>
      <br />
    </div>
  );
}
  • 두 개 이상의 태그는 무조건 하나의 태그로 감싸져야 한다.
    • Fragment: 단순히 감싸기 위한 div이 불필요할 때 사용한다. 태그를 작성할 때 이름이 없으면 Fragment가 생성되는데 이는 브라우저 상에서 별도의 엘리먼트로 나타나지 않는다.
function App() {
  return (
    <>
      <Hello />
      <Hello />
    </>
    );
}
  • JSX 내부에 자바스크립트 변수를 보여줘야 할 때에는 {}으로 감싸야한다.
function App() {
  const text = "Hello"
  return (
    <p>{text}</p>
  );
}
  • 인라인 스타일은 객체 형태로 작성을 해야 하며, background-color같이 -로 구분되어 있는 이름들은 camelCase 형태로 작성한다.
function App() {
  const style = {
    backgroundColor: 'black',
    color: 'aqua',
  }
  return (
    <div style = {style}>Hello</div>
  )
}

 

style 객체를 선언하지 않고 바로 태그 내에 작성하려면 중괄호로 두 번 감싸면 된다. javascript값인 객체이기 때문이다.

function Hello() {
    return <div style={{ backgroundColor: 'black', color: "aqua" }}>안녕하세요</div>
  }
  •  CSS class 를 설정할 때에는 class= 가 아닌 className= 으로 설정을 해주어야 한다.(이는 리액트에서 class가 예약어이기 때문이다. id는 그대로 작성해도 문제없다.)
.gray {
  background: gray;
}
#box {
  width: 64px;
  height: 64px;
}
function App() {
  return (
      <div id='box' className='gray'></div>
  );
}

 

  • 주석은 {/*주석*/} 혹은 //주석 처럼 작성한다. 또한 열리는 태그 내부에는 //주석 형태로 작성할 수도 있다.
function App() {
  return (
    <>
      {/*주
      석*/}
      <div //주석
      >Hello</div>
    </>
  );
}

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

props 는 properties(성질, 특징) 의 줄임말로 어떠한 값을 컴포넌트에게 전달해줘야 할 때 props 를 사용한다.

App 컴포넌트에서 Hello 컴포넌트를 사용할 때 name 이라는 값을 전달해주고 싶다고 하면, 이렇게 코드를 작성하면 된다.

//App.js
function App() {
  return <Hello name="react" />;
}
//Hello.js
function Hello(props) {
  return <div>Hello {props.name}</div>;
}

 

컴포넌트에게 객체 형태로 전달되는 props는 파라미터를 통해 조회할 수 있다.

  • 비구조화 할당: props내부의 값을 조회할 때 props. 을 작성하지 않고 함수의 파라미터에서 비구조화 할당(혹은 구조 분해) 문법을 사용하면 코드를 간결하게 작성할 수 있다. 파라미터에 전달받은 값의 이름을 중괄호로 감싸 작성하면 된다.
//App.js
function App() {
  return <Hello name="react" color="red"/>;
}
//Hello.js
function Hello({ color, name }) {
  return <div style={{ color }}>안녕하세요 {name}</div>
  //css 속성과 변수 이름이 같을 때 위와 같이 축약 가능하다.
}

 

모든 props의 이름을 파라미터에 작성하기 힘들 때는 ...props를 사용하면 된다. props에 남은 값들이 모두 할당된다.

//Hello.js
function Hello({ color, ...props }) {
    return <div style={{ color }}>안녕하세요 {props.name}</div>
  }
  • defalut props: 컴포넌트에 props 를 지정하지 않았을 때 기본적으로 사용할 값을 설정하고 싶다면 컴포넌트에 defaultProps 라는 값을 설정하면 된다.
//App.js
function App() {
  return <Hello color="red"/>;
}
//Hello.js
function Hello({ color, name }) {
  return <div style={{ color }}>안녕하세요 {name}</div>;
}

Hello.defaultProps = {
  name: '이름없음'
}
  • props.children: 컴포넌트 태그 사이에 넣은 값을 조회하고 싶을 때 사용한다.

우선 src에 Wrapper.js와 App.js를 다음과 같이 작성한다.

import React from 'react';

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

    </div>
  )
}

export default Wrapper;
//App.js
function App() {
  return (
    <Wrapper>
      <Hello color='red' name='react' />
    </Wrapper>
  );
}

 

Wrapper 태그 내부에 Hello 컴포넌트를 넣었지만 브라우저에는 Hello 컴포넌트가 표시되지 않는다. 내부의 컴포넌트를 표시하기 위해서는 Wrapper컴포넌트에서 props.children 을 렌더링해야 한다.

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

06. 조건부 렌더링

조건부 렌더링이란 특정 조건에 따라 다른 결과물을 렌더링 하는 것을 의미한다.

 

App.js 에서 Hello 컴포넌트를 사용할 때 isSpecial이라는 props를 설정해 보자

//App.js
function App() {
  return (
    <>
      <Hello isSpecial={true} />
      <Hello />
    </>
  );
}

 

이때 isSpecial의 값에 따라 Hello를 다르게 렌더링 하기 위해 Hello.js를 다음과 같이 작성해 보자

//Hello.js
function Hello({ isSpecial }) {
  return (
    <div>
        {isSpecial ? <b>!</b> : null}
        {isSpecial && <b>*</b>}
        Hello
    </div>
  );
}

출력물

이처럼 삼항 연산자나 단축 평가 논리 계산법을 이용해 isSpecial이 true일 때 앞에 !*를 작성하게 할 수 있다.

  • props의 값을 설정하지 않으면 true로 간주한다.
  • 단축 평가 논리 계산법: A && B 에서 A가 true라면 B가, A가 false라면 A가 결괏값이 된다. A || B 에서는 반대로 동작한다.
console.log(true && 'hello') //hello
console.log(false && 'hello') //false
console.log(true || 'hello') //true
console.log(false || 'hello') //hello

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

컴포넌트에서 보여줘야 하는 내용이 사용자 인터랙션에 따라 바뀌도록 구현할 수 있다.

  • Hooks: 클래스 없이 상태와 다른 리액트 기능을 사용할 수 있게 해준다.
  • useState: Hooks 중 하나로 함수형 컴포넌트에서 상태값을 관리하게 해준다.

useState 라는 함수를 사용해서 간단한 Counter를 만들어보자. 우선 Counter.js를 다음과 같이 작성 후 App.js에서 랜더링 한다.

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

function Counter() {
  const onIncrease = () => {
    console.log('+1')
  }
  const onDecrease = () => {
    console.log('-1');
  }
  return (
    <div>
      <h1>0</h1>
      <button onClick={onIncrease}>+1</button> {/*onIncrease()로 작성 시 렌더링 도중 함수가 실행된다.*/}
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;
//App.js
import React from 'react';
import Counter from './Counter';

function App() {
  return (
    <Counter />
  );
}

export default App;

출력물

위 코드는 버튼을 눌렀을 때 콘솔에 출력하는 함수를 실행한다. 콘솔에 출력이 아니라 숫자를 바꿀 수 있도록 해보자

컴포넌트에서 동적인 값을 상태(state)라고 부른다. 리액트의 useState 함수를 사용하면 컴포넌트의 상태를 관리할 수 있다. useState 함수를 사용하려면 우선 리액트 패키지에서 useState를 불러와야 한다.

import React, { useState } from 'react';

 

useState 를 사용할 때는 기본값을 파라미터에 넣어 호출해야 한다. 함수를 호출하면 현재 상태와 Setter 함수를 원소로 가지는 배열을 반환한다.

const [number, setNumber] = useState(0); //비구조화 할당
//number = 0, setNumber = Setter

 

 

setNumber() 즉, Setter()는 파라미터로 전달받은 값을 최신 상태(state, 여기서는 number)로 설정하므로 Counter 컴포넌트의 onIncrease()와 onDecrease() 를 다음과 같이 바꾼다.

const onIncrease = () => {
    setNumber(number + 1);
}

const onDecrease = () => {
    setNumber(number - 1);
}

 

 

h1태그에서는 0이 아니라 {number} 값을 보여주도록 한다

<h1>{number}</h1>

이렇게 되면 버튼을 누르면 숫자가 바뀌는 Counter가 완성된다.

 

위에서는 Setter()를 사용할 때, 새로운 값을 파라미터에 넣었는데 그 대신에 기존 값을 어떻게 업데이트할 지에 대한 함수를 작성해도 된다.

//Counter.js
const onIncrease = () => {
    setNumber(prevNumber => prevNumber + 1);
}

const onDecrease = () => {
    setNumber(prevNumber => prevNumber - 1);
}
//Setter 함수의 파라미터에 함수를 넣으면 현재 상태 값을 전달한다.

 


08. input 상태 관리하기

input 에 입력하는 값이 하단에 나타나고, 초기화 버튼을 누르면 input 값이 비워지도록 해보자.

우선 inputSample.js를 다음과 같이 작성하고 App.js에서 렌더링 한다.

//inputSample.js
import React, { useState } from 'react';

function 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>
  );
}
//App.js
import React from 'react';
import InputSample from './InputSample';

function App() {
  return (
    <InputSample />
  );
}

export default App;

출력물

useState 함수와 input의 onChage라는 이벤트를 사용한다.

이벤트에 등록하는 함수에서는 이벤트 객체 e를 파라미터로 받아 사용할 수 있다. e.target은 이벤트가 발생한 DOM, 여기서는 input DOM을 가리킨다. 즉, e.target.value 으로 현재 input에 입력한 값을 조회할 수 있게 된다.

조회한 값을 Counter를 만들며 배운 것을 활용하여 useState를 통해 관리하면 된다.


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

여러 개의 input을 관리하려면 어떻게 해야 할까? useState 를 여러 번 사용하고 onChage 도 여러 개 만들어 구현할 수도 있다. 하지만 그보다 더 좋은 방법은 각 input에 name을 설정하고 이벤트가 발생했을 때 그 값을 참조하게 하는 것이다. 이때 useState에는 문자열이 아니라 객체 형태의 상태를 관리해주어야 한다.

 

우선 state가 여럿 존재하도록 useState를 사용한다.

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

 

이후 값을 추출해 name과 nickname에 넣어준다.

const { name, nickname } = inputs;

 

JSX를 다음과 같이 작성한다. pacleholder란 input이 비었을 때 나타나는 문자열이다.

<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>

 

onChange 이벤트가 발생했을 때 이벤트가 발생한 input의 name과 value를 추출하고 추출한 값을 통해 상태를 업데이트해주는 함수 onChange() 와 input들을 초기화하는 함수 onReset() 을 작성한다.

const onChange = (e) => {
  const { value, name } = e.target; // e.target의 name 과 value 추출
  setInputs({
    ...inputs, // input 객체 복사(spread 문법)
    [name]: value // name 키의 값을 value로 설정
  });
};

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

이 코드를 이해하기 위해 다음 사항들을 알아야 한다.

  • spread 문법: ...arr 과 같이 사용한다. 기존의 arr는 수정하지 않으면서 arr가 가지고 있는 내용을 모두 포함한 새로운 객체를 만들 수 있다.
  • 리액트에서 객체 수정: 리액트에서는 inputs[name] = value 처럼 객체를 직접 수정하면 안 된다. 그 대신에 변화를 준 새로운 객체를 생성하고 이를 상태로 사용해야 한다. 그 이유는 리액트 컴포넌트가 리렌더링 할 때 virtual DOM에서 이전 컴포넌트와 새 컴포넌트를 비교하여 차이가 있는 곳만 리렌더링 하는데, 기존 객체를 직접 수정해도 객체의 메모리 주소는 같기 때문에 리액트의 비교 알고리즘이 차이가 없다고 판단하기 때문이다.

 

반응형