AngularJS에서 React 사용하기

이유

처음 AngularJS를 사용하게 되었을 때 여러 기술을 비교하여 선택한 것은 아니었다. 기존 서비스가 이미 AngularJS로 개발되는 중이었다. 그다음 프로젝트에서라도 다른 기술과 비교했어야 했지만 그러지 못했다. 라이브러리를 선택하는 것은 고도의 기술이다. 기존에 사용하던 AngularJS와 React의 차이점을 알기 위해 AngularJS로 구현된 일부 기능을 React 코드로 바꾸는 연습을 했다. 그 과정을 정리했다.

시작하기 전에

AngularJS와 React의 차이점을 알기 위해 TodoMVC 코드를 비교해도 되지만 익숙한 내 코드를 사용하여 비교하겠다. 취미 프로젝트인 동네 책 찾기 코드의 일부를 React 코드로 바꾸겠다. 동네 책 찾기 서비스는 정말 짧은 코드다.

git clone https://github.com/afrontend/dlserver.git
cd dlserver
npm install
npm start

코드를 설치하고 서비스를 시작하면 브라우저로 localhost:3000/app 주소에 접속할 수 있다. 우선 기능만 확인하고 싶다면 https://dlserver.herokuapp.com/app를 방문하면 된다. 서버 로딩에 10초 정도 걸릴 수 있다. AngularJS로 구현된 동네 도서관 책 찾기 서비스는 두 개의 파일로 구성되어 있다. Node.js로 작성된 코드는 생략하겠다.

$ tree public
public
├── app.js
└── index.html

Index.html 파일 하나와 이 파일에서 읽어 들이는 app.js 자바스크립트 파일 한 개다. AngularJS는 아래 코드를 사용하여 읽어 들였다.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>

총 74라인의 index.html 파일 안에 9개의 AngularJS 디렉티브가 HTML 태그의 속성 형태로 선언되어 있다.

  • ng-app, AngularJS가 해석할 범위 결정
  • ng-bind, 단방향 데이터 바인딩
  • ng-click, 클릭 이벤트
  • ng-controller, 콘트롤러 스코프
  • ng-hide, HTML 엘리먼트 감추기
  • ng-model, 양방향 데이터 바인딩
  • ng-options, select의 option
  • ng-repeat, HTML 엘리먼트 반복
  • ng-show, HTML 엘리먼트 보이기

AngularJS를 사용하면 많은 디렉티브들을 사용해야 하므로 HTML 파일이 복잡해진다. 선언적이라서 이해하기 쉽다는 말은 AngularJS를 사용하던 사람이 하는 말이고 만약에 처음 대하는 개발자라면 불편할 수 있다. AngularJS가 이런 선언들을 해석하는 방식으로 서비스가 동작한다. 해석할 때 app.js를 사용한다. AngularJS가 HTML 파일 자체를 템플릿으로 사용하는 것과는 다르게 React는 ReactDOM을 사용하여 DOM을 업데이트한다. 상대적으로 AngularJS는 HTML 파일이 복잡해지고 React는 자바스크립트 파일이 복잡해진다. 자바스크립트 파일 내부에서 HTML 엘리먼트를 만들기 때문이다.

React 홈페이지 처음 화면에서 아래와 같은 설명을 볼 수 있는데 기존 코드가 어떤 기술을 사용하건 추가할 수 있다는 말이다. 그래서 AngularJS는 그대로 두고 React 라이브러리를 읽어 들이는 시도를 하겠다.

We don’t make assumptions about the rest of your technology stack, so you can develop new features in React without rewriting existing code.

AngularJS를 읽어 들이면 전역으로 angular 객체를 사용할 수 있는 것과 비슷하게 react.production.min.js를 읽어 들이면 React 객체에 접근할 수 있고 react-dom.production.min.js를 읽어 들이면 ReactDOM 객체를 사용할 수 있다. 아래 코드로 온라인에서 React를 읽어 들였다.

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

React를 동작시킬 비어 있는 reactApp.js 파일을 새로 만들고 읽어 들인다.

/* reactApp.js */
(function() {
})();
<!--index.html-->
<script src="reactApp.js"></script>

파일 한 개가 추가되었다. React로 동작할 코드는 reactApp.js 에 작성하겠다.

$ tree public
public
├── app.js
├── index.html
└── reactApp.js

도서관 목록 읽기

React로 변경할 부분을 정하기 전에 사용자 관점의 시나리오를 보자. 도서관을 선택하고 찾고 싶은 책 제목을 입력한다. 그리고 버튼을 클릭하면 책 목록을 볼 수 있다. 이 단순한 기능을 더 쪼개서 우선 도서관 목록을 보여주는 기능을 React로 변경하겠다. AngularJS의 디렉티브가 선언된 HTML 코드는 아래와 같다. 이 부분이 도서관 목록을 보여주는 HTML 코드다.

<!--index.html-->
<select
ng-options="option.name for option in libraryNames track by option.id"
ng-model="libraryName"></select>

AngularJS의 선언은 직관적이다. libraryNames 배열을 id로 정렬하고 정렬된 여러 name을 리스트로 만들어 옵션으로 보여준다는 말이다. 검색할 도서관 이름을 선택하는 기능이다. 그렇다면 libraryNames는 어디서 오는가? app.js 파일의 일부를 보자.

    /* app.js */
      LibraryService.getLibraryNames(function (list) {
        $log.log(list.length);
        $scope.libraryNames = _.map(list, function (value, index) {
          return {
            id: index,
            name: value
          };
        });
      });

LibraryService 객체의 getLibraryNames 함수를 사용하여 $scope.libraryNames 값을 업데이트한다. 이 값이 변경되면 AngularJS가 이를 감지하여 HTML 파일을 업데이트한다. 자바스크립트 코드 $scope.libraryNames과 HTML 파일의 ng-options 문자열 안에 libraryNames는 같은 변수로 AngularJS가 연결시켜준다. 바인딩되었다고 말하기도 한다. AngularJS는 DOM을 업데이트를 대신해 준다. 우리가 프레임워크를 사용하는 이유다. 선언만 하면 된다. DOM은 HTML 코드로 형태로 볼 때 아래처럼 업데이트된다. 브라우저 개발자 툴 F12에서 확인할 수 있다.

<!--index.html DOM 업데이트 후 -->
<select ng-options="option.name for option in libraryNames track by option.id" ng-model="libraryName" class="ng-pristine ng-untouched ng-valid ng-not-empty">
   <option label="경기도립중앙도서관" value="0" selected="selected">경기도립중앙도서관</option>
   <option label="경기도립평택도서관" value="1">경기도립평택도서관</option>
   <option label="경기도립광주도서관" value="2">경기도립광주도서관</option>
   <option label="경기도립여주도서관" value="3">경기도립여주도서관</option>
   ......
</select>

React 렌더링

AngularJS는 HTML 자체를 템플릿으로 사용하지만 React는 조금 다르다. 자바스크립트를 사용하여 컴포넌트 내부에 HTML 템플릿을 내장하는 것 같은? 방법을 사용한다. 화면에 표시하는 최소 구성 요소는 자바스크립트 객체인 엘리먼트고 엘리먼트를 만드는 구성단위는 컴포넌트다. React는 컴포넌트를 기본 단위로 사용하여 서비스를 만든다. React 컴포넌트는 함수형과 클래스형 두 종류가 있으며 모두 JSX 문법에서 tagName으로 사용할 수 있으며 태그에서 속성 값을 전달하면 React 컴포넌트에게 전달하고 그 결과로 엘리먼트를 돌려준다. 전달하는 모든 속성들은 하나의 객체에 포함되어 전달된다. 함수형 컴포넌트는 인자로 받고 클래스형 컴포넌트는 this.props 참조로 받는다. 함수형 컴포넌트는 리턴 값으로 엘리먼트를 돌려주고 클래스형 컴포넌트는 엘리먼트를 돌려주는 내부 함수 render를 제공한다. 이런 컴포넌트를 사용해서 엘리먼트를 만들고 엘리먼트를 ReactDOM 객체의 render 함수를 사용하여 브라우저의 DOM을 업데이트한다. 이런 DOM 업데이트 과정을 렌더링이라고 한다. 자바스크립트 객체인 React 엘리먼트를 DOM에 적용하는 과정이다. React는 컴포넌트는 직접 렌더링 할 수 없고 엘리먼트만 렌더링 할 수 있는 점이 특이하다. 간단하게 정리하면 React 엘리먼트와 React 컴포넌트는 각각 두 가지 방법으로 만들 수 있고 React 컴포넌트는 React 엘리먼트를 생성한다. 이 React 엘리먼트를 렌더링 하는데 ReactDOM.render() 함수를 사용한다. 렌더링 된 결과는 브라우저에서 볼 수 있다. 이와는 조금 다르게 AngularJS는 명시적으로 렌더링을 명령하지 않는다. 다만, HTML 코드에 ng-* 속성으로 변경될 부분을 지정하고 이 부분과 연결된 자바스크립트 변수를 업데이트할 뿐이다.

도서관 목록 읽기 React로 변경

React로 DOM을 업데이트하기 위해서 ReactDOM.render 함수를 사용할 텐데 두 개의 인자를 전달해야 한다. 처음 인자는 업데이트할 React 엘리먼트이고 그다음 인자는 업데이트 대상이 될 DOM 엘리먼트다. 기존 index.html의 코드를 변경한다.

<!--index.html 변경 전 AngularJS 스타일-->
<select
ng-options="option.name for option in libraryNames track by option.id"
ng-model="libraryName"></select>
<!--index.html 변경 후 React 스타일-->
<span id="root">
</span>

AngularJS에서는 select 태그에 적용된 ng-options, ng-model 선언을 사용하여 DOM을 업데이트한다. React는 조금 다르게 “root” 아이디를 가지는 태그를 지정하고 이 내부에 DOM을 업데이트 방식을 사용한다. 아이디 이름과 태그 이름은 정하기 나름이다. 이제 reactApp.js를 변경할 때다.

/* reactApp.js */
(function() {
  ReactDOM.render(
    React.createElement('select'),
    document.getElementById('root')
  );
})();
<!--index.html DOM 업데이트 후 -->
<span id="root">
  <select></select>
</span>

아직 select 태그 내부에 도서관 목록이 나타나지 않고 있다. AngularJS에서 도서관 목록을 읽어오는 함수를 재 사용할 예정인데 reactApp.js 코드에 도서관 목록을 읽어오는 함수를 호출하는 것보다 app.js 에서 ReactDOM.render 호출하는 편이 간편해서 코드 위치를 옮기겠다. 우선 reactApp.js 코드를 주석 처리한다.

/* reactApp.js 
(function() {
  ReactDOM.render(
    React.createElement('select'),
    document.getElementById('root')
  );
})();
*/

그리고 주석처리한 코드를 app.js에 삽입한다.

    /* app.js */
      LibraryService.getLibraryNames(function (list) {
        $log.log(list.length);
        $scope.libraryNames = _.map(list, function (value, index) {
          return {
            id: index,
            name: value
          };
        });
    
        ReactDOM.render(
          React.createElement('select'),
          document.getElementById('root')
        );
        ......
      });

우선 아래 데스트 데이터를 넣어서 기능을 확인하겠다.

<option label="경기도립중앙도서관" value="0" selected="selected">경기도립중앙도서관</option>
    /* app.js */
      LibraryService.getLibraryNames(function (list) {
        ......
        ReactDOM.render(
          React.createElement('select', {}, React.createElement('option', {
            'label': '경기도립중앙도서관',
            'value': '0',
            'selected': 'selected'
          }, '경기도립중앙도서관')),
          document.getElementById('root')
        );
        ......    
      });

React.createElement 함수를 사용하는 것은 JSX에 비해 조금 복잡하지만 변환 과정이 필요 없음으로 바로 사용할 수 있는 장점이 있다. 이젠 테스트 데이터 말고 $scope.libraryNames 변수를 사용하자 이 변수는 도서관 코드와 이름을 가지고 있는 객체의 배열이다. 이 변수를 사용하여 도서관 목록의 여러 option 태그를 만들겠다.

    /* app.js after */
      LibraryService.getLibraryNames(function (list) {
        ......
        var options = _.map($scope.libraryNames, function (lib) {
          return React.createElement('option', {
            'label': lib.name,
            'value': lib.id
          }, lib.name);
        })
    
        ReactDOM.render(
          React.createElement('select', {}, options),
          document.getElementById('root')
        );
        ......    
      });

Lodash_.map 함수를 사용하여 여러 도서관 이름을 React 엘리먼트로 만들어서 표시했다. 자바스크립트로 React 엘리먼트를 만들고 ReactDOM.render를 사용하여 React 엘리먼트를 브라우저의 DOM 엘리먼트로 변경했다. 아래는 업데이트 된 HTML 코드다.

<!--index.html React로 DOM 업데이트 후 -->
<span id="root">
   <select>
      <option label="경기도립중앙도서관" value="0">경기도립중앙도서관</option>
      <option label="경기도립평택도서관" value="1">경기도립평택도서관</option>
      <option label="경기도립광주도서관" value="2">경기도립광주도서관</option>
      <option label="경기도립여주도서관" value="3">경기도립여주도서관</option>
      ......
   </select>
</span>

선택된 도서관 이름을 React 에서 읽기

생각해보니 React는 DOM을 잘 업데이트하는 또 다른 방식의 라이브러리라고 간단하게 설명할 수 있겠다. 도서관 목록을 표시하고 도서관을 선택하기 위해 React 코드를 사용하고 있다. 아직 도서관 목록을 서버로부터 읽는 기능은 AngularJS를 사용하고 있다. 이렇게 읽어온 도서관 목록을 화면에 표현하기 위해 React를 사용했다. 위에서 React 엘리먼트 하나를 만들어서 렌더링 했다. React는 렌더링 하려는 데이터를 React 엘리먼트라는 객체에 저장한다. 이 객체를 HTML 엘리먼트로 만들어서 브라우저의 DOM으로 이동시켜주는 동작을 React에서의 렌더링이라고 할 수 있겠다. 렌더링을 위해 ReactDOM 객체가 사용된다. 도서관 목록을 표시했지만 아직 사용자가 선택한 값을 읽을 수 없다. AngularJS에서는 HTML 코드에 ngModel을 사용하여 $scope.libraryName이라는 변수에 연결시켰다. 이 변수를 읽으면 된다. 이 변수는 양방향 바인딩되어 AngularJS가 서로의 값을 일치시킨다. React 이런 자동 작업이 없음으로 개발자가 수동으로 작업을 해야 한다. 생성한 select react 엘리먼트의 값이 변할 때 그 값을 읽을 수 있는 자바스크립트 함수를 작성해야 한다. 엘리먼트를 만들 때 선택된 도서관 이름을 가져올 자바스크립트 함수를 속성의 형태로 전달할 수 있다.

    /* app.js */
        var options = _.map($scope.libraryNames, function (lib) {
          return React.createElement('option', {
            'label': lib.name,
            'value': lib.value
          }, lib.name);
        })
    
        ReactDOM.render(
          React.createElement('select', {
            'onChange': function (e) {
              console.log(e.target.value);
            }
          }, options),
          document.getElementById('root')
        );

도서관이 선택된 이후에 도서관 이름이 사용된다. 사용자가 도서관을 선택했을 때 이 이름을 나중에 사용하기 위해서 저장해 두어야 한다. 어디에 저장하고 어떻게 읽을까? 선택의 문제다. 바닐라 자바스크립트를 사용해서 저장해도 되고 AngularJS의 $scope 객체에 저장해도 된다. 정말 간단하게 $scope.reactLibraryName에 저장하겠다.

    /* app.js */
        ReactDOM.render(
          React.createElement('select', {
            'onChange': function (e) {
              console.log(e.target.value);
              $scope.reactLibraryName = e.target.value;
            }
          }, options),
          document.getElementById('root')
        );

이제 이 이름을 사용하여 검색하는 AngularJS 코드를 수정한다. $scope.reactLibraryName을 사용할 수 있도록 2줄을 추가했다.

    /* app.js */
    $scope.search = function () {
      $scope.libraryName = {};
      $scope.libraryName.name = $scope.reactLibraryName ? $scope.reactLibraryName : '';
      $scope.books = [];
      $log.log("search " + $scope.libraryName.name);
      ......

코드는 https://github.com/afrontend/dlserver/releases/tag/v1.1 에서 확인할 수 있다. 간단하게 React의 스타일을 조금 느낄 수 있었다. 앞으로 아래 작업을 진행할 예정이다.

  • ReactElement 대신 React의 컴포넌트를 사용하겠다.
  • 그 다음 JSX 사용하겠다.
  • Webpack 사용하여 빌드하겠다.

AngularJS or React?

AngularJS가 하는 일은 React로도 할 수 있습니다.
React는 프레임워크가 아니라 라이브러리라면서요?
둘 다 비슷한 의미구요 React는 친구들이 많아요.
Redux를 같이 사용해야 한다면서요?
맞아요 한 가족 같은 거예요.
JSX도 사용해야 한다면서요?
맞아요 모두 같은 거라고요
React-router를 사용한다면서요?
그건 AngularJS도 ngRoute 사용하잖아요. 그리고 AngularJS는 느리잖아요?
인정하죠 뭐든 그렇게 빠를 필요는 없어요.
Angular가 나와서 AngularJS는 사라질 거라는데요?

AangulaJS 사용하는 나와 React 배우는 나 자신이 서로 대화했다고 상상해 봤다. 언어나 프레임워크는 특정 문제를 해결하는데 적합하다. 사용법도 다르고 권장하는 방법도 따로 있다. 선택할 수 있다면 테스트를 해야 하고 선택할 수 없다면 차이점을 이해해야 한다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

This site uses Akismet to reduce spam. Learn how your comment data is processed.