React

React? 🖐️

아가프린 2024. 1. 28. 03:07

안녕하세요..ㅎ 티스토리 뉴비이자 프론트엔드 개발자 이운린이라고 합니다! 먼저 첫 글로는 무엇을 쓸까 고민을 많이 했는데 프론트엔드 개발자인만큼 프론트엔드 3대 프레임워크(라이브러리) 중 하나이자 가장 널리 쓰이는 React에 관한 글을 써보고 싶었습니다! 많이 미숙한 부분이 있을 수 있지만 글 쓰기 초보자의 성장기라고 생각해주세요!

 

 

React란 뭘까?

 

React의 공식문서 페이지를 들어가보면 React를 위 문장으로 정의합니다. (React의 개발사는 페이스북입니다!)

 

사실 처음 시작할 때는 React가 무엇인지도 모르고 "어 다른 프론트엔드 개발자들은 전부 React를 사용하네?" 같이 수동적으로 React를 시작하는 프론트엔드 개발자 분들이 대부분이라고 생각합니다. 저 역시도 학교에 재학하면서 학교의 흐름과 미디어의 흐름에 이것을 왜 사용할까? 같은 의문도 없이 무지성으로 HTML > CSS > Javascript > React 순으로 배웠습니다. React가 JS의 라이브러리인지도 모르고 말이죠.. 

 

React가 라이브러리?

 

React는 Vue나 Angular 처럼 프레임워크 아니었나? 라고 의문이 들 수 있습니다. 맞아요. React는 때때로 프레임워크로도 분류됩니다. 왜일까요? 물론 React는 처음에 뷰 라이브러리로 시작했습니다. 하지만 점점 프론트엔드 생태계가 성장하고 그에 따라 React는 더 많은 기능들이 추가되었습니다. 프레임워크적인 기능들이요. 여기서 짧게 라이브러리와 프레임워크의 차이를 알아볼까요?

 

1. 라이브러리

특정 기능을 수행하기 위한 개발 도구로 개발자가 필요할 때 가져다 사용합니다. 즉, 제어의 주도권을 개발자가 가집니다.

 

2. 프레임워크

전체적인 애플리케이션의 구조와 흐름을 제어하는 데 사용되는 특별한 구조를 갖춘 도구들의 집합입니다. 라이브러리는 프레임워크란 범주안에 속하죠. 전체적인 애플리케이션의 구조와 흐름을 제어하기에 제어의 주도권이 개발자가 아닌 프레임워크가 가지게됩니다.

 

간단하게 차이를 알아봤습니다! 마저 React가 왜 라이브러리이면서도 프레임워크로도 분류되는지 알아보겠습니다. 


React에는 Context API, Hooks, Router 등등의 기능들을 가지고 있습니다. 이 기능들은 React 애플리케이션의 전반적인 구조를 다룰 수 있습니다. 마치 프레임워크와 유사하죠?

 

하지만 React는 UI를 만드는 기능만을 제공합니다. 그리고 위 기능들을 무조건 사용해야 하는 것이 아닌 개발자가 사용하고 싶을 때 사용합니다. 개발의 제어 주도권이 누구에게 있죠? 개발자에게 있습니다. React의 라이프 사이클도 마찬가지죠. 논란이 있긴 하지만 확실하게 React는 라이브러리인거 명심해주세요!

 

🖼️등장 배경

이제 React가 무엇인지 아실 것 같나요? 그렇다면 React는 왜 등장했을까요?

 

React뿐 아니라 다른 수 많은 라이브러리, 프레임워크가 등장하기 이전에는
html, css, javascript만을 이용해 웹을 개발하였습니다. 하지만 점점 웹 애플리케이션이 늘어나고

웹 개이 복잡해짐에 따라 3가지 만을 이용한 개발에는 한계가 있었습니다. 거기다 web app의 크기도 늘어나

JS파일이 넘쳐나 이를 효율적으로 관리하기 위해 backbone.js 같은 라이브러리가 등장하면서 JS파일의

관리가 수월해졌지만 JS의 중요도는 점차 커져갔고 이에 따라 SPA(Single Page Application) 개념이 나왔습니다.

 

Single Page Application

옛날엔 웹 페이지는 모든 페이지마다 HTML, CSS, Javascript 파일을 가지고 있어야했고

페이지 간 이동을 할 때마다 위 파일들을 서버와 주고 받았기에 당연히 속도가 느렸습니다.

하지만 SPA는 위 파일들을 처음 1회만 로드한 후에는

JS 파일을 통해 DOM 또는 필요한 HTML 파일을 조작하는 방식이어서 혁신적이었죠.

 

 

SPA 개념이 등장한 후 Google이 Angular라는 프레임워크를 개발했습니다.

Angular는 작은 컨테이너들이 모여서 거대한 App을 구성하도록 설계된 프레임워크입니다.

당연히 너무 좋은 프레임워크였지만 web app이 커지고 사용자가 늘어날 수록 홈페이지 내에서

웹과 사용자의 상호작용, User Interaction이 너무나 빠르게 증가하는데

복잡성이 증가할수록 데이터의 흐름이 어디로 이어지는지 파악하기 어려웠고

디버깅도 어려워지는 문제가 생겼습니다.

 

React의 등장!

React는 Angular같은 프레임워크에서 발생했던 문제들을 해결하기 위해 나온 라이브러리였기에

데이터의 흐름을 빠르게 확인할 수 있으며 컴포넌트 기반 구조라는 장점들을 가지면서

정말 프론트엔드 생태계의 혁신적인 기술이었습니다. 현재까지도 선풍적인 인기를 유지하고있죠.

 

👍장점

1. Virtual DOM

 

원래 프론트엔드에서 DOM을 조작하는 방식은 명령적이었습니다. 이는 DOM에 보여지길 원하는

모든 요소들을 하나하나 정확하게 명령하는 것을 의미하는데 이는 여러가지 이벤트와 변화가 서로

어떤 연관성을 가지는지 파악하기 어렵다는 문제가 있습니다.

 

당연히 모든 요소들에 일일이 명령을 내리기에 성능에 무리가 가겠죠?

하지만 Reat는 가상 DOM(Virtual DOM)을 사용합니다. Virtual DOM 방식은

이벤트가 발생할 때마다 가상의 DOM 트리를 만들고 실제 DOM에 반영하기 전에

이전 VirtualDOM과 현재 Virtual DOM을 확인하고 변경된 요소만을 실제 DOM에 반영합니다.

일일이 직접 변경해주던 방식과 비교해보니 어떤가요? 물론 각각의 장단점들이 있겠지만

기존의 조작 방식의 효율성과 속도를 개선할 수 있습니다.

 

React가 채택한 패러다임 덕분에 데이터의 흐름과 요소들 간의 연관성을

파악하기 쉬워졌고 코드의 복잡성은 줄어들며 코드의 퀄리티는 올라가는 효과를 얻습니다.

 

2. Component Concept

React는 컴포넌트 단위로 개발을 한다는 말을 들어보셨을 겁니다! React는 재사용이 가능한

컴포넌트를 만들고 이 컴포넌트들이 모여 웹사이트를 구성하게 됩니다.

레고를 예시로 들어볼까요? 레고 블럭 하나하나가 모여 어떠한 건축물이나 생물을 구성합니다.

컴포넌트도 마찬가지죠.

 

React에 내장된 Component 라이브러리 기능을 불러오고 내장 함수 render()를

통해 react-dom에 렌더링 될 컴포넌트를 전달하여 최종적으로 현재 DOM과 전달받은

컴포넌트를 비교해 변경이 필요한 부분만 반영해 화면에 보여주게 됩니다.

 

컴포넌트는 기본적으로 state, props와 JSX로 구성되어있고 클래스형 컴포넌트와 함수형 컴포넌트로 나뉩니다.

 

아래는 함수형 컴포넌트의 예시입니다.

function MyComponent(props) {
  const [value, setValue] = useState('Hello, World!');
  return (
    <div>
      <h1>{value}</h1>
      <span>안녕하세요! {props.name}</span>
    </div>
  );
}

export default MyComponent;

3. Data Flow

React는 단방향 데이터 흐름을 가집니다. 등장배경에서 언급한 Angular의 문제점은

Angular가 양방향 데이터 흐름을 가져서 생긴 문제입니다. 물론 여러 장점이 있지만

애플리케이션의 규모가 커지면 데이터 흐름을 파악하기 어렵지만 단방향 데이터 흐름을 가진

React는 규모가 커져도 비교적 쉽게 흐름을 파악하기 쉽습니다.

 

 

가상 DOM은 DOM이기 때문에 tree 구조를 가집니다. 가상 DOM과 실제 DOM의 비교는

state로부터 시작되어 state를 기반으로 컴포넌트가 구성되고 컴포넌트를 기반으로

가상 DOM을 그리고 가상 DOM과 실제 DOM을 비교해 화면에 그리는 과정을 거칩니다.

 

state ➡️ Component ➡️ Virtual DOM ➡️ 실제 DOM과 비교 ➡️ 화면에 보여줌

 

어떤가요? 흐름이 단방향입니다!

 

 

이뿐만 아니라 컴포넌트의 구조도 단방향적인 흐름을 가집니다. 컴포넌트는 보통 부모와 자식의 관계를 가집니다.

컴포넌트는 부모 ➡️ 자식의 흐름입니다. 일반적으론 자식 ➡️ 부모같이 거슬러 올라가는 흐름은

잘 발생하지 않습니다.  부모에서 자식으로만 데이터가 전달되기에 데이터의 흐름을 쉽게 파악할 수 있죠? 

 

State가 뭘까?

state는 간단히 말해서 변수입니다. 하지만 기존 JS의 const, let, var 등으로 선언한 변수와 다르게

값이 변하면 해당 state와 관련되어있는 컴포넌트들이 리렌더링되어 화면이 바뀝니다.

 

state는 컴포넌트 내부에서 변경 가능한 데이터를 다루기 위해 사용되는 객체이기도 합니다.

React에서는 기본적으로 변경 가능한 데이터는 useState라는 Hooks를 사용해서 state라는

저장공간에 담아서 사용합니다!

const [state, setState] = useState();

 

useState는 배열을 반환하는데 첫 번째 요소는 상태 값, 두 번째 요소는 상태를 변경할 수 있는

setter 함수입니다. 참고로 state의 선언은 함수 안에서 선언되어야 합니다!

 

위와 같이 사용하는데 state는 변하는 값을 다룰 때 사용한다고 했습니다. 그런데 const로..?

순수 JS를 하다 React로 넘어온다면 당연히 가지는 의문입니다.

let이 아닌 const로 사용하는 이유는 개발자가 실수로 state를 직접 변경할

가능성이 있기 때문입니다. state는 직접 변경하면 안 되고 무조건 setter 함수를 이용해

변경해야 합니다. 따라서 재할당을 방지하고 상태가 불변하게 하기 위해 const를 사용합니다.

React의 Hooks 권장사항에도 const를 사용하길 권하고 있구요. ㅎ

 

✍️사용하는 이유

React에서는 변수를 사용하면 이 변수가 바뀌어도 자동으로 화면이 바뀌지 않습니다.

즉, 리렌더링이 일어나지 않습니다! 따라서 유동적인 변수를 사용할 때 화면에 그려지는

변수도 바뀌길 원한다면 state를 사용해야 합니다. 확 와닿지 않을 수 있습니다.

 

예시를 들어볼까요?

 

function Counter() {
  let count = 0;

  const plus = () => {
    count = count + 1;
  }
  
  const minus = () => {
    count = count - 1;
  }

  return (
    <div>
      <h2>{ count }</h2>
      <button onClick={plus}>Add</button>
      <button onClick={minus}>Minus</button>
    </div>
  );
}

export default Counter;

 

위는 변수를 사용한 Counter 컴포넌트입니다. 위 코드를 실행시키면

변수 count는 잘 변할 지 몰라도 화면의 count는 절대로 변하질 않습니다.

화면의 count도 변하게 하고 싶다면 state를 사용해야 되겠죠?

import { useState } from 'react';

functionCounter() {
  const [count, setCount] = useState(0);

  const plus = () => {
    setCount(count + 1);
  }
  const minus = () => {
    setCount(count - 1);
  }
  
  return (
    <div>
      <h2>{ count }</h2>
      <button onClick={plus}>Add</button>
      <button onClick={minus}>Minus</button>
    </div>
  );
}

export default Counter;

 

이제 화면의 count도 잘 변할겁니다!

Props가 뭘까?

부모 컴포넌트에서 자식 컴포넌트로 전달해주는 데이터입니다. 하지만 자식 컴포넌트에서

바꿀 수 없기에 읽기 전용 데이터라는 특징을 가지고 있습니다.

 

import MyComponent from './MyComponent';

function App() {
  const [color, setColor] = useState('red');
  return (
    <div>
      <MyComponent color={color} />
      <button onClick={() => setColor('red')}>
        Red
      </button>
       <button onClick={() => setColor('blue')}>
        Blue
      </button>
    </div>
  );
}

 

function MyComponent(props) {
  return <span style={{color: props.color}}>Color</span>;
}

 

물론 자식 컴포넌트에게 setter 함수를 넘겨줘서 부모의 state를 바꿀 순 있지만 React에서도 권장하지 않기에

setter 함수를 props로 주는 것은 사용하지 않는 게 좋습니다.

 

❓Hooks가 뭘까?

Hooks란 React의 16.8 버전부터 추가된 기능입니다. 본래 state와 라이프 사이클 기능은

클래스형 컴포넌트에서만 가능한 기술이었습니다. 하지만 여러가지 문제들이 있어

함수형 컴포넌트에서도 state와 라이프사이클을 사용할 수 있게 한 것이 Hook이라고 할 수 있습니다!

 

🚨문제점

1. 재사용

클래스형 컴포넌트에서는 state와 라이프 사이클 함수들을 사용할 때 로직의 재사용이

힘들었습니다. 때문에 Render Props나 고차 컴포넌트인 HOC를 이용한 방법을 사용했으나

사용해본 개발자들은 느낄 것 같습니다. 가독성이 좋지 않고 유지보수 비용도 드는 방법입니다.

 

2. this

클래스형 컴포넌트이기 때문에 this 바인딩을 명시적으로 처리해줘야 합니다. 규모가 커지면

this가 많아지게 되고 this가 많아지면 코드의 복잡성도 같이 올라가게 됩니다.

 

3. 구조

저는 함수형 컴포넌트를 먼저 접해서 그런지 클래스형 컴포넌트를 작성하다보면 대부분

비교적 구조가 복잡했습니다. GPT에게 타이머 기능을 클래스형과 함수형으로

구현해달라고 하였습니다. 아래 2개의 코드는 같은 기능을 가집니다!

 

클래스형 컴포넌트

import React, { Component } from 'react';

class Timer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      seconds: 0,
      isRunning: false,
    };
    this.intervalId = null;
  }

  componentDidMount() {
    this.startTimer();
  }

  componentWillUnmount() {
    this.stopTimer();
  }

  startTimer = () => {
    if (!this.state.isRunning) {
      this.setState({ isRunning: true });
      this.intervalId = setInterval(() => {
        this.setState((prevState) => ({ seconds: prevState.seconds + 1 }));
      }, 1000);
    }
  };

  stopTimer = () => {
    if (this.state.isRunning) {
      this.setState({ isRunning: false });
      clearInterval(this.intervalId);
    }
  };

  resetTimer = () => {
    this.setState({ seconds: 0 });
  };

  render() {
    return (
      <div>
        <p>Seconds: {this.state.seconds}</p>
        <button onClick={this.startTimer}>Start</button>
        <button onClick={this.stopTimer}>Stop</button>
        <button onClick={this.resetTimer}>Reset</button>
      </div>
    );
  }
}

export default Timer;

 

함수형 컴포넌트

import React, { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    startTimer();
    
    return () => stopTimer();
  }, []);

  const startTimer = () => {
    if (!isRunning) {
      setIsRunning(true);
      const intervalId = setInterval(() => {
        setSeconds((prevSeconds) => prevSeconds + 1);
      }, 1000);
      
      return () => {
        clearInterval(intervalId);
        setIsRunning(false);
      };
    }
  };

  const stopTimer = () => {
    setIsRunning(false);
    setSeconds(0);
  };

  const resetTimer = () => {
    setSeconds(0);
  };

  return (
    <div>
      <p>Seconds: {seconds}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
      <button onClick={resetTimer}>Reset</button>
    </div>
  );
};

export default Timer;

 

무조건 함수형 컴포넌트가 좋다는 것은 아닙니다. 저도 클래스형 컴포넌트의 장점을 겪어봤으니까요.

개발자의 취향, 팀의 규칙에 따라 작성하는 것이 정직합니다!

 

🫡 규칙

반복문, 조건문, 중첨된 함수 내에서 호출하면 안 됩니다. 이 규칙을 준수한다면 컴포넌트가

렌더링 될 때마다 항상 동일한 순서로 훅이 호출되는 것이 보장됩니다!

만약 조건문으로 훅이 호출될 수도 있고 안 될 수도 있는 상황을 만든다면 동일한 순서로

호출되는 것이 보장되지 않기 때문에 예상치 못한 오류를 맞닥뜨릴수도 있습니다.

또 훅은 순수 JS 함수와 클래스형 컴포넌트에서 호출할 수 없습니다.

📖 마무리

그래도 제가 수도 없이 많이 보고 외우고 접하던 내용들이고 깊은 내용들은 넣지 않았기에

시간이 오래 걸리지 않을 것 같았지만 쓰다보니 간단한 내용도 모르는 지식이  있어 찾아보기도 하다보니까

꽤 많은 시간을 소요했습니다..ㅎ 글을 쓰면서 다시 React에 대해 다시 한 번 공부해서 좋았고 티스토리에서의

첫 글을 작성해 뿌듯했습니다!

 

🔗 Ref

https://defineall.tistory.com/900

https://velog.io/@niboo/React-React.js%EB%9E%80

https://velog.io/@hamham/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-state%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

https://ko.legacy.reactjs.org/

https://soldonii.tistory.com/100