본문 바로가기
React

React 사용 후기 및 개념 정리

by TheUphill 2020. 2. 28.

Core Concept

1. Virtual DOM

화면에 변화가 일어났을 때 더 적은 리소스로 빠르게 업데이트를 한다. 개념적으로는 브라우저에는 실제 DOM이 아닌 리액트가 생성한 가상 DOM을 렌더링한 후 엘리먼트가 변경될 경우 React가 변경 내용을 연산해서 가상 DOM에서 필요한 부분만 업데이트 한다.

2. Component

리액트의 모든 구성요소는 컴포넌트이다. 버튼, 폼, 레이아웃, 화면에 이르기까지 대부분의 구성요소들은 컴포넌트화 된다. 얼마나 작게 분해하냐, 큰 덩어리로 뭉치느냐 차이가 있을 뿐 전부 컴포넌트다. 그리고 적절한 컴포넌트로 코드 재사용성을 높인다.

3. 단방향 데이터 바인딩

먼저 양방향 데이터 바인딩과 단방향 데이터 바인딩의 차이는 HTML에서 변경된 내용이 데이터 영향을 미치는가이다. 예를들어 양방향 데이터 바인딩의 대표인 AngularJS는 엘리먼트에 데이터를 바인딩 하면 Javascript 코드로 데이터를 변경할수도 있고 엘리먼트의 값(input)을 수정해서 데이터를 변경할 수 있다. 하지만 React와 같은 단방향 데이터 바인딩은 Javascript -> HTML로 데이터 바인딩만 가능하다. 즉 HTML에 바인딩 한 데이터를 Javascript에서 수정할 경우 화면에는 반영이 되지만 화면에서 직접 해당 엘리먼트의 값을 바꿨을 때 Javacript의 데이터가 수정되도록 바인딩 하는 방법은 제공하지 않는다. 물론 양방향 바인딩과는 조금 다른 방법으로 화면의 데이터 수정을 Javascript 상 데이터로 가져올수 있다.
언뜻 보기에는 단방향이 불편해보일 수 있지만 그만큼 단방향 데이터가 가지는 장점은 모든 Javascript 코드가 데이터에 집중되며 일관된 데이터 관리 로직을 갖는다는 점이다.

양방향 데이터 vs 단반향 데이터

개발 환경

1. ES6

ES6없이 Javascript를 개발하는건 Generic이나 Enum없이 자바를 개발하는거와 동일하다고 생각한다. ES6에서는 Old Javascript 환경에서는 제공하지 않는 다양한 기능과 세련되고 가독성이 높은 코드를 지원한다. 주로 사용되는 기능으로는 Arrow function, Spread operator, Block scope(let, const), 모듈 시스템(import, export, default), Class, Promises 등이 있다.
ES6에서 제공하는 다양한 기능들은 여기에서 살펴볼 수 있다.

2. Node

Node는 Javascript 개발 환경에서 서버 개발뿐만 아니라 클라이언트 개발에도 필수 요소가 됐다. 특히 의존성 관리를 위한 NPM, Yarn은 다양한 의존성을 간편하게 해주며 프로젝트의 코드베이스 크기를 작게 만들다.

3. Webpack

Javascript 어플리케이션 모듈 번들 툴. 의존성을 가지고 있는 모듈들을 순회하면서 dependency graph를 생성한다. 그리고 이 모듈들을 하나 또는 여러개의 모듈로 패키징 한다. 또한 패키징 과정에서 다양한 플러그인들을 이용해서 배포하는데 필요한 다양한 동작들을 수행해 준다. 그 중 대표적인것이 transfile 작업인데, ES6로 작성한 코드를 이전 버전의 Javascript Engine에서 구동할 수 있도록 코드를 변환해주는 작업이다. 또한 DevServer 기능은 앱을 위한 별도의 서버를 띄우지 않고도 앱을 구동시킬 수 있게 해준다.(실제로 내부에는 Node기반의 서버가 자체적으로 뜨긴 한다)

4. 그 외의 React 생태계

React는 프레임워크가 아닌 UI Component 라이브러리가 말하는데 그만큼 React는 UI 조작에 특화돼있다. React는 화면조작만 담당하며 그 외에 프론트 엔드 개발에 필요한 다양한 기능들(예를들어 ajax 통신, immutable 객체 조작 같은)은 별도 라이브러리 도움을 받아야 한다. 하지만 React는 현재 프론트 엔드 기술중에 가장 큰 생태계를 가지고 있으며 그만큼 완성도 높은 라이브러리가 많으며 최근 Javascript또한 오픈소스 프로젝트 중 높은 비중을 차지하고 있어 좋은 오픈소스들이 공개돼 있다. 그 중 몇가지를 나열해보면 다음 정도이다.

  1. axios(ajax 통신)
  2. redux(어플리케이션 상태 관리), redux-thunk, redux-actions
  3. react-router(라우팅)
  4. immutable.js(immutable data library)

Main Features

Component

React의 모든 컴포넌트는 Component 클래스를 상속한다. 가장 간단한 React 컴포넌트를 살펴보자.

import React from 'react';

class App extends React.Component {
    render(){

        return (
                <h1>Hello React</h1>
        );
    }
}

export default App;

App 컴포넌트는 React.Component 클래스를 상속한다. Component는 라이프 사이클을 가지며 React가 화면을 구성하는 과정에서 호출되는 함수들이 정의돼 있고 개발자는 각 라이프사이클에서 필요한 코드를 구현한다. 위에 에제에서 구현한 render 함수는 React가 컴포넌트를 화면에 그릴때 호출되는 함수이며 리턴되는 내용을 기반으로 Virtual DOM을 생성한다.

JSX

React에서는 일반적인 Javascript 문법이 아닌 JSX를 사용해서 UI를 템플릿화 한다. 위에서 살펴봤던 예제에 몇가지 코드를 더 보강했다.

import React from 'react';

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            subTitle: "This is react tutorial"
        };
    }

    render() {
        return (
                <h1 className="Title">Hello React</h1>
                <h2>{this.state.subTitle}<h2>
                <button onclick={this.inform}>inform</button>
        );
    }

    inform() {
         alert('Welcome React');
    }
}

export default App;

가장 간단한 React 컴포넌트이다. render함수가 리턴하는 코드가 JSX 코드다. 얼핏 보기에는 HTML와 동일하지만 몇몇 JSX를 위한 기능들이 있다. 먼저 h1태그에 적용된 className은 기존 HTML에서 class 속성에 속한다. 코드를 자세히 살펴보면 다음과 같다.

  • import React from 'react': react 모듈에 의존성을 갖는다.
  • constructor(props) {}: 클래스 생성자.
  • this.state = { ... }: 클래스의 State인 subTitle를 정의한다. State에 대해서는 아래에서 자세히 다룬다.
  • <h1> ~ </h1>: Title이라는 클래스를 갖는 h1태그 사용.
  • <h2> ~ </h2>: 위에서 선언한 subTitle속성을 h2테그에 바인딩.
  • <button> ~ </button>: onclick에 내부 함수 inform()을 바인딩.
  • inform() { ... }: 내부 함수 정의.

State와 Props

React 컴포넌트의 속성은 크게 두가지로 분류되는데 state와 props가 있다. 두가지 모두 데이터를 저장하는 점만 같을뿐 목적 및 사용 방법은 완전히 다르다. 두 속성을 비교하는 표를 살펴보자.

일반적으로 props는 외부에서 주입되는 값을, state는 컴포터는가 주도적으로 데이터를 관리하는 목적으로 사용된다.

아래 예제는 간단한 카운터 앱으로 버튼을 누를때 마다 화면에 표시되는 숫자가 1씩 증가하는 앱이다.

class Counter from React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0 };
    }

    onClick() {
        this.setState({count: this.state.count + 1});
    }

    render() {
        return (
              <div>
                  <h1>{this.props.title}</h1>
                <div>count:{this.state.count}</div>
                <button onClick={this.onClick}>click!</button>
              </div>
        );
    }
};

코드를 살펴보면 다음과 같다.

  • constructor() ...: 외부로부터 props를 주입받는다. props는 Counter를 호출하는 코드에서 title 속성을 주입한다. 그리고 카운트 값을 저장하고 증가시킬 count 변수를 state로 선언했다.
  • onclick() {...}: count state를 1씩 증가시키는 함수다.
  • render() {...}: 먼저 h1태그에 props인 title 속성을 바인딩 했다. 그리고 div태그에 state인 count 속성을 바인딩 했으며, 마지막으로 button을 클릭했을 때 onClick함수를 호출하도록 해 데이터가 1씩 증가시키도록 했다.

코드에서 볼 수 있듯이 state 속성은 컴포넌트 내부에서 변경이 가능하다. 하지만 props는 데이터 변경이 불가능하다. 추가적으로 컴포넌트에 props를 주입하는 코드는 다음과 같다.

ReactDOM.render(<Counter title={'Counter Page'}/>, document.getElementById("container"));

<Counter> 컴포넌트를 생성하면서 title속성에 특정 값을 생성해서 전달하고 있다.

컴포넌트 라이프 사이클

위에서 설명했듯이 컴포넌트는 정해진 라이프 사이클을 가지며 화면에 그려지고 없어지는 동안 일련의 함수들이 호출된다. 이 컴포넌트가 가지는 라이프사이클을 알고 있어야 원하는 화면을 그릴거나 불필요한 리소스가 낭비되지 않는다.

위에 예제에서 사용됐던 constructor 함수는 컴포넌트가 생성될때 한번 호출되며, render() 함수는 컴포넌트 생성 및 props나 state가 변경될때 호출된다. 위에 다이어그램에서 보여주듯이 특정 라이프 사이클에서는 state를 변경하면 안된다. 만약 그럴 경우 state 변경 직후 다시 state를 변경하게 되므로 무한 루프에 빠지게 된다.

Form 조작

React는 단방햔 데이터 바인딩을 사용한다. 때문에 화면 요소에 데이터를 바인딩하기는 간편하지만 반대로 화면에서 데이터를 가지고 오는 방법은 상대적으로 복잡할 수 있다. 단 상대적일 뿐 기존에 Javascript로 form 구성요소를 다뤄봤다면 쉽게 적응할 수 있다.

import React, { Component } from 'react';

class FormComp extends Component {
    constructor(props) {
        super(props);
        this.state = {
            value: 'Initial Value'
        }
    }

    render() {
        return (
            <div>
                <input type="text"
                       value={this.state.value}
                        onChange={this.handleChange}/>
                <button onClick={this.submit}>submit</button>
            </div>
        )
    }

    handleChange = (e) => {
        this.setState({
            value: e.target.value
        }, () => console.log(this.state.value));
    };

    submit = () => {
        alert(this.state.value);
    };
}

export default FormComp;
  • constructor() {...}: value라는 이름의 속성을 선언.
  • render() {...}: 화면에 그릴 요소들은 정의.
    • <input>: value 속성에 value state를 바인딩 했다. value state가 변경될때 마다 자동으로 input의 value속성이 갱신된다. 그리고 onChange 속성으로 input 엘리먼트가 변경될때 마다 처리할 핸들러를 등록해줬다.
    • <button>: 버튼을 생성하고 클랙했을 때 submit 함수를 호출하도록 설정했다.
  • handleChange() {...}: <input> 엘리먼트의 onChange 속성에 바인딩한 함수다. 매개변수로 이벤트를 전달 받아 input의 값을 읽어와 value state를 갱신한다. 이때 value state의 값을 직접 수정하지 않고 새로운 state 객체를 생성해서 교체하는 방식인데 이는 react에서 강조하는 immutable한 객체를 사용해야 하기 때문이다. immutable한 객체를 사용해야 되는 이유는 여기를 참조. 그리고 마지막으로 setState 함수는 비동기이다. 즉 함수를 호출과 실제 동작이 비동기로 수행되기 때문에 state가 변경된 결과를 정확히 조회하기 위해서는 콜백을 사용해야 한다. 그래서 변경값을 출력하는 로그를 콜백으로 설정했다.
  • submit() {...}: 단순히 value state를 alert로 보여주는 함수다.

Redux

위에 예제들에 서 봤듯이 react에서는 상태가 변하는 값들을 각 component의 state로 관리하며 다른 컴포넌트로 사용하기 위해서는 props 속성으로 전달한다. 하지만 이런 방식은 규모가 점점 커지는 어플리케이션에서 비효율적인 구조를 낳는다.

[그림1]

왼쪽이 다양한 컴포넌트들이 구성됐을 때 나올 수 있는 구조다. 한곳에서 선언된 state를 다른 컴포넌트들이 가져다 쓰려면 그 중간에 컴포넌트들을 거쳐 전달하는 형태가 되는데, 구조적으로나 코드상으로 불필요한 요소들이다. 또한 상태 추적또한 매우 힘들어져서 각 state들이 어느 컴포넌트에 있는지 숙지하지 않는한 디버깅이 매우 어렵다. 이런 불편함을 없애기 위해 dan abramov라는 개발자 앱의 상태를 효율적으로 관리하기 위한 라이브러리를 공개 했는데 그것이 redux다.

redux는 FLUX 패턴 구현체인데, 그에 대한 설명은 다음과 같다.

시스템에서 어떠한 Action 을 받았을 때, Dispatcher가 받은 Action들을 통제하여 Store에 있는 데이터를 업데이트한다. 그리고 변동된 데이터가 있으면 View 에 리렌더링한다. 그리고, View에서 Dispatcher로 Action을 보낼 수도 있다.
출처 - https://velopert.com/1225

[그림2]

데이터는 Store라는 한 곳에서 관리되며 특정 UI요소가 변경 됐을 때 변경 Action 이벤트를 생성하고 Reducer가 이벤트를 처리해서 Store에 저장된 State 변경하는 구조다. 전체적으로 봤을 때 상태를 위한 데이터를 Component가 아닌 별도 저장소를 운영하고 필요한 Component들이 저장소에서 필요한 데이터를 가져다쓰면 된다. 결과적으로는 위에 [그림2]에서 오른쪽 그림과 같은 구조가 된다. 이때 Stoore는 하나가 될수도 있지만 편의상 기능이나 도메인 별로 따로 관리하는게 일반적이다.

Redux 적용 예제

위에서 살펴 보았던 <FormComp> 예제에 redux를 적용했다. 예제 코드가 많아서 코드 중간에 관련된 설명을 코멘트로 추가했다.
예제 코드는 크게 상단에는 컴포넌트 코드, 하단에는 Redux 코드로 분리했다.

import React, { Component } from 'react';
// redux로부터 connect 함수를 import 한다. 차후 이 컴포넌트를 redux store, reducer에 연결하는데 사용된다.
import { connect } from 'react-redux';

// Component
class FormComp extends Component {
    // constructor 함수가 없어졌다. 
    // state나 props를 컴포넌트가 관리하지 않고 redux가 관리하기 때문이다. 
    // 만약 state, props 선언 이외의 초기화 동작이 필요하면 추가할 수 있다.

    render() {
        return (
            <div>
                // state로 부터 바인딩 했던 속성 및 함수가 모두 props로 변경됐다.
                // 값 및 함수는 모두 redux로 부터 props로써 주입된다.
                <input type="text"
                       value={this.props.value}
                        onChange={this.props.handleChange}/>
                <button onClick={this.submit}>submit</button>
            </div>
        )
    }

    submit = () => {
        alert(this.props.value);
    };
}

// Redux
// 초기값 선언.
const initialState = {
    value: 'Initial value from redux store'
};

// Action 선언. input의 값이 변경됐을 때 발생할 액션이다.
const HANDLE_CHANGE = 'HANDLE_CHANGE';

// HANDLE_CHAGE 액션을 발생하는 액션 함수다. 
// type으로 HANDLE_ACTION을 설정하고 액션과 함께 전달할 데이터를 설정한다.
// 이때 type은 필수 파라미터이며 그 외에 필드는 자유롭게 선언이 가능하다.
function handleChange(value) {
    return {
        type: HANDLE_CHANGE,
        value: value
    }
}

// Action을 처리할 Reducer 선언. Action의 type에 따라 처리 로직을 분리한다.
// 간단하게 HANDLE_CHANGE 액션과 함께 전달된 데이터를 state에 업데이트 한다. 
export const formReducer = (state = initialState, action) => {
    console.log(action);
    switch(action.type) {
        case HANDLE_CHANGE: {
            // 새로운 state를 반환. 이때도 마찬가지로 state는 immutable 해야 한다.
            return {
                // ...state는 spread operator로 es6에 추가된 기능이다. {} 안에 포함된 state 속성들을 펼친다.
                // 그리고 뒤에 이어지는 속성들 중 state와 중복되는(key 키준) 속성이 있으면 뒤에 속성 값으로 갱신한다.
                ...state,
                value : action.value
            }
        }
        default: {
            return state;
        }
    }
};

// Store의 state를 이 컴포넌트의 props로 바인딩해주는 함수. 아래에서 redux와 컴포넌트를 연결하는 옵션으로 사용된다.
const mapStateToProps = (state) => {
    return {
        value: state.value
    }
}

// 컴포넌트에서 발생한 Action을 redux의 Reducer로 전달해주는 함수. 아래서 redux와 컴포넌트를 연결하는 옵션으로 사용된다.
const mapDispatchToProps = (dispatch) => {
    return {
        handleChange: (e) => dispatch(handleChange(e.target.value))
    }
}

// connect 함수를 이용해 컴포넌트와 redux를 연결한다.
// connect 함수의 리턴은 또 다른 함수를 리턴하는데 이 함수의 매개변수는 컴포넌트이며 내부 동작은 전달된 컴포넌트의 props로 특정 state를 전달한다. 그래서 connect() 호출 이후 바로 FormComp를 매개변수로 해서 리턴된 함수를 호출한다.
// 마지막으로 redux에 연결된 컴포넌트를 export한다.

export default connect(mapStateToProps,mapDispatchToProps)(FormComp);

예제만 보면 이전 redux를 적용하기 전 보다 코드가 많아진것 같다. 하지만 일반적으로는 redux의 Action, Redcuer 관련 코드들은 외부로 빠지고 컴포넌트 관련 코드만 남게 된다. 가장 중요한건 앱의 규모가 커지면서 중복되는 코드가 줄어들고 어플리케이션 상태가 한 곳으로 집중된다는 점이다.

Store의 갯수

redux의 store는 하나 이상 생성할 수 있다. 그래서 도메인(주요 관심사)별로 store를 별도로 생성해서 관리한다. 예를들어 쇼핑몰 앱을 만든다고 했을 때 대략적으로 분리될 수 있는 store는 다음과 같다.

  • 로그인 정보
  • 상품 목록
  • 상품 상세
  • 장바구니
  • 결제
  • 즐겨찾기

이렇게 하는 편이 유지보수 측면에서 훨씬 효율적이기 때문에 redux의 store가 반드시 하나일 필요는 없다(참고로 redux와는 다르게 FLUX 패턴은 하나의 store를 지향한다)

참고 자료

react 튜토리얼 - https://reactjs.org/tutorial/tutorial.html

한글 튜토리얼 - https://velopert.com/reactjs-tutorials

react redux 튜토리얼 - https://redux.js.org/basics/usage-with-react

댓글