Angular vs. React: 웹 개발에 어느 것이 더 낫습니까?

게시 됨: 2022-03-11

React 또는 Angular가 웹 개발에 더 나은 선택인지에 대해 토론하는 수많은 기사가 있습니다. 또 다른 것이 필요합니까?

내가 이 기사를 쓴 이유는 이미 게시된 기사 중 어느 것도 훌륭한 통찰력을 포함하고 있지만 실제 프론트 엔드 개발자가 어떤 기사가 자신의 필요에 맞는지 결정할 만큼 충분히 깊이 있지 않기 때문입니다.

이 기사에서는 Angular와 React가 모두 매우 다른 철학으로 유사한 프론트 엔드 문제를 해결하는 것을 목표로 하는 방법과 둘 중 하나를 선택하는 것이 단지 개인 취향의 문제인지 여부를 배우게 됩니다. 그것들을 비교하기 위해 우리는 동일한 애플리케이션을 두 번 빌드할 것입니다. 한 번은 Angular로 한 번은 React로 한 번은 다시 빌드합니다.

Angular의 시기적절한 발표

2년 전 나는 React 생태계에 대한 기사를 썼습니다. 무엇보다 기사는 Angular가 "사전 발표에 의한 사망"의 희생자가 되었다고 주장했습니다. 그 당시에는 Angular와 거의 모든 것 중에서 선택하는 것이 프로젝트가 구식 프레임워크에서 실행되는 것을 원하지 않는 사람에게는 쉬운 선택이었습니다. Angular 1은 구식이었고 Angular 2는 알파 버전에서도 사용할 수 없었습니다.

돌이켜보면 그 두려움은 어느 정도 정당화되었습니다. Angular 2는 극적으로 변경되었으며 최종 릴리스 직전에 주요 재작성까지 거쳤습니다.

2년 후, 우리는 이제부터 상대적인 안정성을 약속하는 Angular 4를 갖게 되었습니다.

이제 뭐?

Angular 대 React: 사과와 오렌지 비교

어떤 사람들은 React와 Angular를 비교하는 것이 사과를 오렌지에 비교하는 것과 같다고 말합니다. 하나는 뷰를 다루는 라이브러리이고 다른 하나는 본격적인 프레임워크입니다.

물론 대부분의 React 개발자는 React를 완전한 프레임워크로 만들기 위해 몇 가지 라이브러리를 추가합니다. 다시 말하지만 이 스택의 결과 워크플로는 여전히 Angular와 매우 다르기 때문에 비교 가능성은 여전히 ​​제한적입니다.

가장 큰 차이점은 상태 관리에 있습니다. Angular에는 데이터 바인딩이 번들로 제공되지만 현재 React는 일반적으로 단방향 데이터 흐름을 제공하고 변경할 수 없는 데이터로 작업하기 위해 Redux에 의해 보강됩니다. 그것들은 그 자체로 반대되는 접근 방식이며, 현재 변경 가능한/데이터 바인딩이 변경 불가능한/단방향보다 나은지 나쁜지에 대한 수많은 논의가 진행 중입니다.

A 수준의 경기장

React는 해킹하기 쉬운 것으로 유명하기 때문에 이 비교를 위해 Angular를 합리적으로 가깝게 미러링하여 코드 조각을 나란히 비교할 수 있는 React 설정을 구축하기로 결정했습니다.

눈에 띄지만 기본적으로 React에는 없는 특정 Angular 기능은 다음과 같습니다.

특징 앵귤러 패키지 리액트 라이브러리
데이터 바인딩, 의존성 주입(DI) @각도/코어 몹X
계산된 속성 rxjs 몹X
구성 요소 기반 라우팅 @각도/라우터 반응 라우터 v4
머티리얼 디자인 구성 요소 @각도/소재 반응 도구 상자
구성 요소로 범위가 지정된 CSS @각도/코어 CSS 모듈
양식 유효성 검사 @angular/forms 폼 상태
프로젝트 생성기 @각도/cli 반응 스크립트 TS

데이터 바인딩

데이터 바인딩은 단방향 접근 방식보다 시작하기 쉽습니다. 물론 완전히 반대 방향으로 가서 React에서는 Redux나 mobx-state-tree를, Angular에서는 ngrx를 사용하는 것도 가능합니다. 그러나 그것은 다른 게시물의 주제가 될 것입니다.

계산된 속성

성능이 염려되는 동안 Angular의 일반 getter는 각 렌더링에서 호출될 때 문제가 되지 않습니다. 작업을 수행하는 RsJS에서 BehaviorSubject를 사용할 수 있습니다.

React를 사용하면 MobX에서 @computed를 사용하는 것이 가능하며, 이는 틀림없이 조금 더 멋진 API로 동일한 목표를 달성합니다.

의존성 주입

의존성 주입은 기능 프로그래밍과 불변성의 현재 React 패러다임에 어긋나기 때문에 논란의 여지가 있습니다. 결과적으로 일종의 종속성 주입은 별도의 데이터 계층 아키텍처가 없는 디커플링(따라서 조롱 및 테스트)에 도움이 되기 때문에 데이터 바인딩 환경에서 거의 필수 불가결합니다.

DI(Angular에서 지원됨)의 또 다른 장점은 서로 다른 저장소의 서로 다른 수명 주기를 가질 수 있다는 것입니다. 대부분의 현재 React 패러다임은 다른 구성 요소에 매핑되는 일종의 전역 앱 상태를 사용하지만 내 경험에 따르면 구성 요소 마운트 해제 시 전역 상태를 정리할 때 버그를 도입하기가 너무 쉽습니다.

구성 요소 마운트에서 생성되는 저장소를 갖는 것(그리고 이 구성 요소의 자식이 원활하게 사용할 수 있음)은 정말 유용하고 종종 간과되는 개념인 것 같습니다.

Angular에서 즉시 사용할 수 있지만 MobX에서도 쉽게 재현할 수 있습니다.

라우팅

구성 요소 기반 라우팅을 사용하면 구성 요소가 하나의 큰 글로벌 라우터 구성 대신 자체 하위 경로를 관리할 수 있습니다. 이 접근 방식은 마침내 버전 4의 react-router 에 적용되었습니다.

머티리얼 디자인

몇 가지 더 높은 수준의 구성 요소로 시작하는 것은 항상 좋은 일이며 머티리얼 디자인은 Google이 아닌 프로젝트에서도 보편적으로 허용되는 기본 선택과 같은 것이 되었습니다.

Material UI는 다음 버전에서 해결할 예정인 인라인 CSS 접근 방식에 심각한 성능 문제가 있다고 스스로 고백한 Material UI가 일반적으로 권장되는 Material UI보다 의도적으로 React Toolbox를 선택했습니다.

게다가 React Toolbox에서 사용되는 PostCSS/cssnext는 어쨌든 Sass/LESS를 대체하기 시작했습니다.

범위 CSS

CSS 클래스는 전역 변수와 같습니다. 충돌을 방지하기 위해 CSS를 구성하는 방법(BEM 포함)에는 여러 가지 접근 방식이 있지만 프론트 엔드 개발자가 정교한 CSS 명명 시스템을 고안할 필요 없이 이러한 충돌을 방지하기 위해 CSS를 처리하는 데 도움이 되는 라이브러리를 사용하는 분명한 추세가 있습니다.

양식 유효성 검사

양식 유효성 검사는 사소하지 않고 매우 널리 사용되는 기능입니다. 코드 반복 및 버그를 방지하기 위해 라이브러리에서 다루는 것이 좋습니다.

프로젝트 생성기

프로젝트에 대한 CLI 생성기가 있으면 GitHub에서 상용구를 복제하는 것보다 조금 더 편리합니다.

동일한 애플리케이션, 두 번 빌드

따라서 React와 Angular에서 동일한 애플리케이션을 만들 것입니다. 멋진 것은 없으며 누구나 공통 페이지에 메시지를 게시할 수 있는 Shoutboard입니다.

여기에서 응용 프로그램을 사용해 볼 수 있습니다.

  • 샤우트보드 앵귤러
  • Shoutboard 반응

홍보판 신청

전체 소스 코드를 갖고 싶다면 GitHub에서 얻을 수 있습니다.

  • Shoutboard Angular 소스
  • Shoutboard React 소스

React 앱에도 TypeScript를 사용했음을 알 수 있습니다. TypeScript에서 유형 검사의 장점은 분명합니다. 그리고 이제 더 나은 가져오기 처리, async/await 및 나머지 스프레드가 마침내 TypeScript 2에 도착하여 Babel/ES7/Flow를 먼지 속에 남겼습니다.

또한 GraphQL을 사용하기 위해 Apollo Client를 둘 다에 추가해 보겠습니다. REST는 훌륭하지만 10년 정도 지나면 낡아집니다.

부트스트랩 및 라우팅

먼저 두 애플리케이션의 진입점을 살펴보겠습니다.

모난

 const appRoutes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'posts', component: PostsComponent }, { path: 'form', component: FormComponent }, { path: '', redirectTo: '/home', pathMatch: 'full' } ] @NgModule({ declarations: [ AppComponent, PostsComponent, HomeComponent, FormComponent, ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes), ApolloModule.forRoot(provideClient), FormsModule, ReactiveFormsModule, HttpModule, BrowserAnimationsModule, MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule ], providers: [ AppService ], bootstrap: [AppComponent] })
 @Injectable() export class AppService { username = 'Mr. User' }

기본적으로 애플리케이션에서 사용하려는 모든 구성 요소는 선언으로 이동해야 합니다. 모든 타사 라이브러리를 가져오고 모든 글로벌 스토어를 제공자에게 제공합니다. 자식 구성 요소는 이 모든 것에 액세스할 수 있으며 더 많은 로컬 항목을 추가할 수 있습니다.

반응

 const appStore = AppStore.getInstance() const routerStore = RouterStore.getInstance() const rootStores = { appStore, routerStore } ReactDOM.render( <Provider {...rootStores} > <Router history={routerStore.history} > <App> <Switch> <Route exact path='/home' component={Home as any} /> <Route exact path='/posts' component={Posts as any} /> <Route exact path='/form' component={Form as any} /> <Redirect from='/' to='/home' /> </Switch> </App> </Router> </Provider >, document.getElementById('root') )

<Provider/> 구성 요소는 MobX에서 종속성 주입에 사용됩니다. React 구성 요소가 나중에 삽입할 수 있도록 컨텍스트에 저장소를 저장합니다. 예, React 컨텍스트는 (분명히) 안전하게 사용할 수 있습니다.

React 버전은 모듈 선언이 없기 때문에 조금 더 짧습니다. 일반적으로 가져오기만 하면 바로 사용할 수 있습니다. 때때로 이러한 종류의 강력한 종속성은 원치 않는(테스트)이므로 글로벌 싱글톤 저장소의 경우 수십 년 된 GoF 패턴을 사용해야 했습니다.

 export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }

Angular의 Router는 인젝션이 가능하여 컴포넌트 뿐만 아니라 어디서나 사용할 수 있습니다. 반응에서 동일한 결과를 얻기 위해 mobx-react-router 패키지를 사용하고 routerStore 를 삽입합니다.

요약: 두 응용 프로그램을 부트스트랩하는 것은 매우 간단합니다. React는 모듈 대신 가져오기를 사용하여 더 간단하다는 장점이 있지만 나중에 보게 되겠지만 이러한 모듈은 매우 편리할 수 있습니다. 싱글톤을 수동으로 만드는 것은 약간의 성가신 일입니다. 라우팅 선언 구문의 경우 JSON 대 JSX는 선호도의 문제일 뿐입니다.

링크 및 필수 탐색

따라서 경로를 전환하는 두 가지 경우가 있습니다. 선언적, <a href...> 요소를 사용하고 명령적, 라우팅(및 위치) API를 직접 호출합니다.

모난

 <h1> Shoutboard Application </h1> <nav> <a routerLink="/home" routerLinkActive="active">Home</a> <a routerLink="/posts" routerLinkActive="active">Posts</a> </nav> <router-outlet></router-outlet>

Angular Router는 어떤 routerLink 가 활성화되어 있는지 자동으로 감지하고 적절한 routerLinkActive 클래스를 배치하여 스타일을 지정할 수 있습니다.

라우터는 특별한 <router-outlet> 요소를 사용하여 현재 경로가 지시하는 모든 것을 렌더링합니다. 애플리케이션의 하위 구성요소를 더 깊이 파고들면 많은 <router-outlet> 을 가질 수 있습니다.

 @Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }

라우터 모듈은 모든 서비스에 주입될 수 있으며(TypeScript 유형에 의해 절반은 마술적임), private 선언은 명시적 할당 없이 인스턴스에 이를 저장합니다. navigate 방법을 사용하여 URL을 전환합니다.

반응

 import * as style from './app.css' // … <h1>Shoutboard Application</h1> <div> <NavLink to='/home' activeClassName={style.active}>Home</NavLink> <NavLink to='/posts' activeClassName={style.active}>Posts</NavLink> </div> <div> {this.props.children} </div>

React Router는 activeClassName 을 사용하여 활성 링크의 클래스를 설정할 수도 있습니다.

여기서 클래스 이름은 CSS 모듈 컴파일러에 의해 고유하게 만들어지고 style 도우미를 사용해야 하기 때문에 직접 제공할 수 없습니다. 나중에 자세히 설명합니다.

위에서 보았듯이 React Router는 <App> 요소 내부에 <Switch> 요소를 사용합니다. <Switch> 요소는 현재 경로를 래핑하고 마운트하므로 현재 구성 요소의 하위 경로가 바로 this.props.children 임을 의미합니다. 그래서 그것도 구성 가능합니다.

 export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } }

mobx-router-store 패키지를 사용하면 쉽게 주입하고 탐색할 수 있습니다.

요약: 라우팅에 대한 두 가지 접근 방식은 상당히 비슷합니다. Angular는 더 직관적인 반면 React Router는 구성이 좀 더 간단합니다.

의존성 주입

프레젠테이션 계층에서 데이터 계층을 분리하는 것이 유익한 것으로 이미 입증되었습니다. 여기서 DI를 사용하여 달성하려는 것은 데이터 계층의 구성 요소(여기서는 모델/저장소/서비스라고 함)가 시각적 구성 요소의 수명 주기를 따르도록 하여 전역 요소를 만질 필요 없이 이러한 구성 요소의 하나 이상의 인스턴스를 만들 수 있도록 하는 것입니다. 상태. 또한 호환되는 데이터 및 시각화 레이어를 혼합하여 일치시킬 수 있어야 합니다.

이 기사의 예제는 매우 간단하므로 모든 DI 항목이 과도하게 보일 수 있지만 애플리케이션이 성장함에 따라 편리합니다.

모난

 @Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } }

따라서 모든 클래스를 @injectable 로 만들 수 있으며 해당 속성과 메서드를 구성 요소에서 사용할 수 있습니다.

 @Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } }

HomeService 를 구성 요소의 providers 에 등록하여 이 구성 요소에서만 사용할 수 있도록 합니다. 지금은 싱글톤이 아니지만 구성 요소의 각 인스턴스는 구성 요소의 마운트에서 새로운 복사본을 받습니다. 이는 이전 사용의 오래된 데이터가 없음을 의미합니다.

대조적으로 AppServiceapp.module 에 등록되었으므로(위 참조) 단일 항목이며 애플리케이션의 수명 동안 모든 구성 요소에 대해 동일하게 유지됩니다. 구성 요소에서 서비스의 수명 주기를 제어할 수 있다는 것은 매우 유용하지만 과소 평가되는 개념입니다.

DI는 TypeScript 유형으로 식별되는 구성 요소의 생성자에 서비스 인스턴스를 할당하여 작동합니다. 또한 public 키워드는 매개변수를 this 에 자동 할당하므로 지루한 this.homeService = homeService 줄을 더 이상 작성할 필요가 없습니다.

 <div> <h3>Dashboard</h3> <md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <br/> <span>Clicks since last visit: {{homeService.counter}}</span> <button (click)='homeService.increment()'>Click!</button> </div>

Angular의 템플릿 구문은 틀림없이 매우 우아합니다. 양방향 데이터 바인딩처럼 작동하는 [()] 바로 가기를 좋아하지만 실제로는 속성 바인딩 + 이벤트입니다. 서비스의 수명 주기에 따라 homeService.counter/home 에서 벗어날 때마다 재설정되지만 appService.username 은 그대로 유지되며 어디에서나 액세스할 수 있습니다.

반응

 import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } }

MobX를 사용하면 관찰 가능하게 만들고 싶은 속성에 @observable 데코레이터를 추가해야 합니다.

 @observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } }

수명 주기를 올바르게 관리하려면 Angular 예제보다 약간 더 많은 작업을 수행해야 합니다. 각 마운트에서 HomeStore 의 새로운 인스턴스를 수신하는 Provider 내부에 HomeComponent 를 래핑합니다.

 interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component<HomeComponentProps, any> { render() { const { homeStore, appStore } = this.props return <div> <h3>Dashboard</h3> <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>Clicks since last visit: {homeStore.counter}</span> <button onClick={homeStore.increment}>Click!</button> </div> } }

HomeComponent@observer 데코레이터를 사용하여 @observable 속성의 변경 사항을 수신합니다.

이것의 내부 메커니즘은 매우 흥미로우므로 여기에서 간단히 살펴보겠습니다. @observable 데코레이터는 객체의 속성을 getter 및 setter로 대체하여 호출을 가로챌 수 있습니다. @observer 증강 구성 요소의 렌더링 기능이 호출되면 해당 속성 getter가 호출되고 호출한 구성 요소에 대한 참조를 유지합니다.

그러면 setter가 호출되어 값이 변경되면 마지막 렌더에서 속성을 사용한 컴포넌트의 렌더 함수가 호출됩니다. 이제 업데이트된 위치에서 어떤 속성이 사용되는지에 대한 데이터와 전체 주기를 다시 시작할 수 있습니다.

매우 간단한 메커니즘이며 성능도 매우 뛰어납니다. 여기에 더 자세한 설명이 있습니다.

@inject 데코레이터는 appStorehomeStore 인스턴스를 HomeComponent 의 소품에 삽입하는 데 사용됩니다. 이 시점에서 각 상점의 수명 주기가 다릅니다. appStore 는 애플리케이션의 수명 동안 동일하지만 homeStore 는 "/home" 경로로 이동할 때마다 새로 생성됩니다.

이것의 이점은 모든 상점이 전역인 경우와 같이 속성을 수동으로 정리할 필요가 없다는 것입니다. 경로가 매번 완전히 다른 데이터를 포함하는 "세부 정보" 페이지인 경우 골치 아픈 일입니다.

요약: Angular의 DI 고유 기능에 있는 공급자 수명 주기 관리로서, 물론 거기에서 달성하는 것이 더 간단합니다. React 버전도 사용할 수 있지만 훨씬 더 많은 상용구가 필요합니다.

계산된 속성

반응

이것에 대한 React부터 시작하겠습니다. 더 간단한 솔루션이 있습니다.

 import { observable, computed, action } from 'mobx' export class HomeStore { import { observable, computed, action } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } @computed get counterMessage() { console.log('recompute counterMessage!') return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit` } }

따라서 counter 에 바인딩하고 적절하게 복수화된 메시지를 반환하는 계산된 속성이 있습니다. counterMessage 의 결과는 캐시되고 counter 가 변경될 때만 다시 계산됩니다.

 <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>{homeStore.counterMessage}</span> <button onClick={homeStore.increment}>Click!</button>

그런 다음 JSX 템플릿에서 속성(및 increment 메서드)을 참조합니다. 입력 필드는 값에 바인딩하고 appStore 의 메서드가 사용자 이벤트를 처리하도록 하여 구동됩니다.

모난

Angular에서 동일한 효과를 얻으려면 좀 더 독창적이어야 합니다.

 import { Injectable } from '@angular/core' import { BehaviorSubject } from 'rxjs/BehaviorSubject' @Injectable() export class HomeService { message = 'Welcome to home page' counterSubject = new BehaviorSubject(0) // Computed property can serve as basis for further computed properties counterMessage = new BehaviorSubject('') constructor() { // Manually subscribe to each subject that couterMessage depends on this.counterSubject.subscribe(this.recomputeCounterMessage) } // Needs to have bound this private recomputeCounterMessage = (x) => { console.log('recompute counterMessage!') this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`) } increment() { this.counterSubject.next(this.counterSubject.getValue() + 1) } }

계산된 속성의 기반이 되는 모든 값을 BehaviorSubject 로 정의해야 합니다. 계산된 속성 자체도 BehaviorSubject 입니다. 모든 계산된 속성이 다른 계산된 속성에 대한 입력으로 사용될 수 있기 때문입니다.

물론, RxJS 는 이것보다 훨씬 더 많은 일을 할 수 있지만 그것은 완전히 다른 기사의 주제가 될 것입니다. 사소한 단점은 계산된 속성에만 RxJS를 사용하는 것이 반응 예제보다 약간 더 장황하며 구독을 수동으로 관리해야 한다는 것입니다(예: 생성자에서).

 <md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <span>{{homeService.counterMessage | async}}</span> <button (click)='homeService.increment()'>Click!</button>

RxJS 주제를 | async | async 파이프. 구성 요소를 구독해야 하는 것보다 훨씬 짧습니다. input 구성 요소는 [(ngModel)] 지시문에 의해 구동됩니다. 이상해 보이지만 실제로는 상당히 우아합니다. appService.username 에 대한 값의 데이터 바인딩 및 사용자 입력 이벤트의 값 자동 할당을 위한 구문 설탕입니다.

요약: 계산된 속성은 Angular/RxJS보다 React/MobX에서 구현하기가 더 쉽지만 RxJS는 나중에 평가할 수 있는 좀 더 유용한 FRP 기능을 제공할 수 있습니다.

템플릿 및 CSS

템플릿이 서로 어떻게 스택되는지 보여주기 위해 게시물 목록을 표시하는 Posts 구성 요소를 사용하겠습니다.

모난

 @Component({ selector: 'app-posts', templateUrl: './posts.component.html', styleUrls: ['./posts.component.css'], providers: [ PostsService ] }) export class PostsComponent implements OnInit { constructor( public postsService: PostsService, public appService: AppService ) { } ngOnInit() { this.postsService.initializePosts() } }

이 구성 요소는 HTML, CSS 및 삽입된 서비스를 연결하고 초기화 시 API에서 게시물을 로드하는 함수도 호출합니다. AppService 는 애플리케이션 모듈에 정의된 싱글톤인 반면 PostsService 는 구성 요소가 생성될 때마다 새로운 인스턴스가 생성되는 일시적입니다. 이 구성 요소에서 참조되는 CSS는 이 구성 요소로 범위가 지정됩니다. 즉, 콘텐츠가 구성 요소 외부에 영향을 줄 수 없습니다.

 <a routerLink="/form" class="float-right"> <button md-fab> <md-icon>add</md-icon> </button> </a> <h3>Hello {{appService.username}}</h3> <md-card *ngFor="let post of postsService.posts"> <md-card-title>{{post.title}}</md-card-title> <md-card-subtitle>{{post.name}}</md-card-subtitle> <md-card-content> <p> {{post.message}} </p> </md-card-content> </md-card>

HTML 템플릿에서는 대부분 Angular Material의 구성 요소를 참조합니다. 사용할 수 있게 하려면 app.module 가져오기에 포함해야 했습니다(위 참조). *ngFor 지시문은 각 게시물에 대해 md-card 구성 요소를 반복하는 데 사용됩니다.

로컬 CSS:

 .mat-card { margin-bottom: 1rem; }

로컬 CSS는 md-card 구성 요소에 있는 클래스 중 하나를 확장합니다.

글로벌 CSS:

 .float-right { float: right; }

이 클래스는 전역 style.css 파일에 정의되어 모든 구성 요소에서 사용할 수 있습니다. 표준 방식인 class="float-right" 로 참조할 수 있습니다.

컴파일된 CSS:

 .float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; }

컴파일된 CSS에서 [_ngcontent-c1] 속성 선택기를 사용하여 로컬 CSS의 범위가 렌더링된 구성 요소로 지정되었음을 알 수 있습니다. 렌더링된 모든 Angular 구성 요소에는 CSS 범위 지정을 위해 이와 같이 생성된 클래스가 있습니다.

이 메커니즘의 장점은 클래스를 정상적으로 참조할 수 있고 범위 지정이 "후드에서" 처리된다는 것입니다.

반응

 import * as style from './posts.css' import * as appStyle from '../app.css' @observer export class Posts extends React.Component<any, any> { postsStore: PostsStore componentWillMount() { this.postsStore = new PostsStore() this.postsStore.initializePosts() } render() { return <Provider postsStore={this.postsStore}> <PostsComponent /> </Provider> } }

React에서 다시 PostsStore 종속성을 "일시적"으로 만들기 위해 Provider 접근 방식을 사용해야 합니다. 또한 JSX에서 해당 CSS 파일의 클래스를 사용할 수 있도록 styleappStyle 로 참조되는 CSS 스타일을 가져옵니다.

 interface PostsComponentProps { appStore?: AppStore, postsStore?: PostsStore } @inject('appStore', 'postsStore') @observer export class PostsComponent extends React.Component<PostsComponentProps, any> { render() { const { postsStore, appStore } = this.props return <div> <NavLink to='form'> <Button icon='add' floating accent className={appStyle.floatRight} /> </NavLink> <h3>Hello {appStore.username}</h3> {postsStore.posts.map(post => <Card key={post.id} className={style.messageCard}> <CardTitle title={post.title} subtitle={post.name} /> <CardText>{post.message}</CardText> </Card> )} </div> } }

당연히 JSX는 Angular의 HTML 템플릿보다 훨씬 더 JavaScript 같은 느낌이 들며 취향에 따라 좋거나 나쁠 수 있습니다. *ngFor 지시어 대신에 map 구성을 사용하여 게시물을 반복합니다.

이제 Angular는 TypeScript를 가장 선전하는 프레임워크일 수 있지만 실제로 TypeScript가 정말 빛나는 것은 JSX입니다. 위에서 가져온 CSS 모듈을 추가하면 템플릿 코딩이 코드 완성 기능으로 바뀝니다. 모든 것이 유형 검사됩니다. 구성 요소, 속성, 심지어 CSS 클래스( appStyle.floatRightstyle.messageCard , 아래 참조). 물론 JSX의 린(lean) 특성은 Angular의 템플릿보다 구성 요소와 조각으로 분할하는 것을 권장합니다.

로컬 CSS:

 .messageCard { margin-bottom: 1rem; }

글로벌 CSS:

 .floatRight { float: right; }

컴파일된 CSS:

 .floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }

보시다시피 CSS 모듈 로더는 고유성을 보장하는 임의의 접미사로 각 CSS 클래스를 접미사로 지정합니다. 충돌을 피하는 간단한 방법. 그런 다음 클래스는 웹팩에서 가져온 개체를 통해 참조됩니다. 이것의 가능한 단점 중 하나는 Angular 예제에서와 같이 클래스로 CSS를 생성하고 확장할 수 없다는 것입니다. 반면에 이것은 스타일을 적절하게 캡슐화하도록 강제하기 때문에 실제로 좋은 것일 수 있습니다.

요약: 저는 개인적으로 Angular 템플릿보다 JSX가 더 좋습니다. 특히 코드 완성 및 유형 검사 지원이 있기 때문입니다. 정말 죽이는 기능입니다. Angular는 이제 몇 가지를 발견할 수 있는 AOT 컴파일러를 가지고 있습니다. 코드 완성도 거기에 있는 작업의 약 절반에 대해 작동하지만 JSX/TypeScript만큼 완전하지는 않습니다.

GraphQL - 데이터 로드

그래서 우리는 GraphQL을 사용하여 이 애플리케이션의 데이터를 저장하기로 결정했습니다. GraphQL 백엔드를 만드는 가장 쉬운 방법 중 하나는 Graphcool과 같은 일부 BaaS를 사용하는 것입니다. 그게 우리가 한 일입니다. 기본적으로 모델과 속성을 정의하기만 하면 CRUD를 사용할 수 있습니다.

공통 코드

GraphQL 관련 코드 중 일부는 두 구현 모두에서 100% 동일하므로 두 번 반복하지 않습니다.

 const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `

GraphQL은 기존 RESTful 엔드포인트에 비해 더 풍부한 기능 세트를 제공하는 것을 목표로 하는 쿼리 언어입니다. 이 특정 쿼리를 분석해 보겠습니다.

  • PostsQuery 는 나중에 참조할 이 쿼리의 이름일 뿐이며 이름은 무엇이든 지정할 수 있습니다.
  • allPosts 는 가장 중요한 부분입니다. 'Post' 모델로 모든 레코드를 쿼리하는 함수를 참조합니다. 이 이름은 Graphcool에서 만들었습니다.
  • orderByfirstallPosts 함수의 매개변수입니다. createdAtPost 모델의 속성 중 하나입니다. first: 5 는 쿼리의 처음 5개 결과만 반환함을 의미합니다.
  • id , name , titlemessage 는 결과에 포함하려는 Post 모델의 속성입니다. 다른 속성은 필터링됩니다.

이미 보다시피 상당히 강력합니다. 이 페이지를 확인하여 GraphQL 쿼리에 대해 자세히 알아보세요.

 interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }

예, 훌륭한 TypeScript 시민으로서 GraphQL 결과에 대한 인터페이스를 만듭니다.

모난

 @Injectable() export class PostsService { posts = [] constructor(private apollo: Apollo) { } initializePosts() { this.apollo.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }).subscribe(({ data }) => { this.posts = data.allPosts }) } }

GraphQL 쿼리는 RxJS 관찰 가능 항목이며 구독합니다. 그것은 약간의 약속처럼 작동하지만 완전히는 아니기 때문에 async/await 를 사용하는 것은 운이 좋지 않습니다. 물론 여전히 toPromise가 있지만 어쨌든 Angular 방식은 아닌 것 같습니다. fetchPolicy: 'network-only' 를 설정했습니다. 이 경우 데이터를 캐시하고 싶지 않고 매번 다시 가져오기 때문입니다.

반응

 export class PostsStore { appStore: AppStore @observable posts: Array<Post> = [] constructor() { this.appStore = AppStore.getInstance() } async initializePosts() { const result = await this.appStore.apolloClient.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }) this.posts = result.data.allPosts } }

React 버전은 거의 동일하지만 여기에서 apolloClient 는 promise를 사용하므로 async/await 구문을 활용할 수 있습니다. React에는 GraphQL 쿼리를 고차 구성 요소에 "테이프"하는 다른 접근 방식이 있지만 데이터와 프레젠테이션 레이어를 너무 많이 섞는 것 같았습니다.

요약: RxJS subscribe 대 async/await의 아이디어는 실제로 매우 동일합니다.

GraphQL - 데이터 저장

공통 코드

다시 말하지만, 일부 GraphQL 관련 코드:

 const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `

돌연변이의 목적은 레코드를 생성하거나 업데이트하는 것입니다. 따라서 돌연변이가 있는 일부 변수를 선언하는 것은 데이터를 전달하는 방법이기 때문에 유용합니다. 따라서 우리는 이 돌연변이를 호출할 때마다 채워야 하는 String 으로 입력된 name , titlemessage 변수를 가지고 있습니다. createPost 함수는 다시 Graphcool에 의해 정의됩니다. 우리는 Post 모델의 키가 외부 돌연변이 변수의 값을 갖도록 지정하고 새로 생성된 Post의 id 만 반환하도록 지정합니다.

모난

 @Injectable() export class FormService { constructor( private apollo: Apollo, private router: Router, private appService: AppService ) { } addPost(value) { this.apollo.mutate({ mutation: AddPostMutation, variables: { name: this.appService.username, title: value.title, message: value.message } }).subscribe(({ data }) => { this.router.navigate(['/posts']) }, (error) => { console.log('there was an error sending the query', error) }) } }

apollo.mutate 를 호출할 때 호출하는 돌연변이와 변수도 제공해야 합니다. subscribe 콜백에서 결과를 얻고 주입된 router 를 사용하여 게시물 목록으로 다시 이동합니다.

반응

 export class FormStore { constructor() { this.appStore = AppStore.getInstance() this.routerStore = RouterStore.getInstance() this.postFormState = new PostFormState() } submit = async () => { await this.postFormState.form.validate() if (this.postFormState.form.error) return const result = await this.appStore.apolloClient.mutate( { mutation: AddPostMutation, variables: { name: this.appStore.username, title: this.postFormState.title.value, message: this.postFormState.message.value } } ) this.goBack() } goBack = () => { this.routerStore.history.push('/posts') } }

"수동" 종속성 주입과 async/await 사용의 차이가 있다는 점에서 위와 매우 유사합니다.

요약: 여기에서도 큰 차이가 없습니다. subscribe 대 async/await는 기본적으로 다릅니다.

양식

이 애플리케이션의 양식을 사용하여 다음 목표를 달성하고자 합니다.

  • 모델에 필드 데이터 바인딩
  • 각 필드에 대한 유효성 검사 메시지, 여러 규칙
  • 전체 양식이 유효한지 확인 지원

반응

 export const check = (validator, message, options) => (value) => (!validator(value, options) && message) export const checkRequired = (msg: string) => check(nonEmpty, msg) export class PostFormState { title = new FieldState('').validators( checkRequired('Title is required'), check(isLength, 'Title must be at least 4 characters long.', { min: 4 }), check(isLength, 'Title cannot be more than 24 characters long.', { max: 24 }), ) message = new FieldState('').validators( checkRequired('Message cannot be blank.'), check(isLength, 'Message is too short, minimum is 50 characters.', { min: 50 }), check(isLength, 'Message is too long, maximum is 1000 characters.', { max: 1000 }), ) form = new FormState({ title: this.title, message: this.message }) }

따라서 formstate 라이브러리는 다음과 같이 작동합니다. 양식의 각 필드에 대해 FieldState 를 정의합니다. 전달된 매개변수는 초기값입니다. validators 속성은 값이 유효하면 "거짓"을 반환하고 값이 유효하지 않을 때 유효성 검사 메시지를 반환하는 함수를 사용합니다. checkcheckRequired 도우미 함수를 사용하면 모두 멋지게 선언적으로 보일 수 있습니다.

전체 양식에 대한 유효성 검사를 수행하려면 해당 필드를 FormState 인스턴스로 래핑한 다음 집계 유효성을 제공하는 것이 좋습니다.

 @inject('appStore', 'formStore') @observer export class FormComponent extends React.Component<FormComponentProps, any> { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return <div> <h2> Create a new post </h2> <h3> You are now posting as {appStore.username} </h3> <Input type='text' label='Title' name='title' error={postFormState.title.error} value={postFormState.title.value} onChange={postFormState.title.onChange} /> <Input type='text' multiline={true} rows={3} label='Message' name='message' error={postFormState.message.error} value={postFormState.message.value} onChange={postFormState.message.onChange} />

FormState 인스턴스는 모든 프런트 엔드 구성 요소에서 쉽게 사용할 수 있는 value , onChangeerror 속성을 제공합니다.

 <Button label='Cancel' onClick={formStore.goBack} raised accent /> &nbsp; <Button label='Submit' onClick={formStore.submit} raised disabled={postFormState.form.hasError} primary /> </div> } }

When form.hasError is true , we keep the button disabled. The submit button sends the form to the GraphQL mutation presented earlier.

Angular

In Angular, we are going to use FormService and FormBuilder , which are parts of the @angular/forms package.

 @Component({ selector: 'app-form', templateUrl: './form.component.html', providers: [ FormService ] }) export class FormComponent { postForm: FormGroup validationMessages = { 'title': { 'required': 'Title is required.', 'minlength': 'Title must be at least 4 characters long.', 'maxlength': 'Title cannot be more than 24 characters long.' }, 'message': { 'required': 'Message cannot be blank.', 'minlength': 'Message is too short, minimum is 50 characters', 'maxlength': 'Message is too long, maximum is 1000 characters' } }

First, let's define the validation messages.

 constructor( private router: Router, private formService: FormService, public appService: AppService, private fb: FormBuilder, ) { this.createForm() } createForm() { this.postForm = this.fb.group({ title: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(24)] ], message: ['', [Validators.required, Validators.minLength(50), Validators.maxLength(1000)] ], }) }

Using FormBuilder , it's quite easy to create the form structure, even more succintly than in the React example.

 get validationErrors() { const errors = {} Object.keys(this.postForm.controls).forEach(key => { errors[key] = '' const control = this.postForm.controls[key] if (control && !control.valid) { const messages = this.validationMessages[key] Object.keys(control.errors).forEach(error => { errors[key] += messages[error] + ' ' }) } }) return errors }

To get bindable validation messages to the right place, we need to do some processing. This code is taken from the official documentation, with a few small changes. Basically, in FormService, the fields keep reference just to active errors, identified by validator name, so we need to manually pair the required messages to affected fields. This is not entirely a drawback; it, for example, lends itself more easily to internationalization.

 onSubmit({ value, valid }) { if (!valid) { return } this.formService.addPost(value) } onCancel() { this.router.navigate(['/posts']) } }

Again, when the form is valid, data can be sent to GraphQL mutation.

 <h2> Create a new post </h2> <h3> You are now posting as {{appService.username}} </h3> <form [formGroup]="postForm" (ngSubmit)="onSubmit(postForm)" novalidate> <md-input-container> <input mdInput placeholder="Title" formControlName="title"> <md-error>{{validationErrors['title']}}</md-error> </md-input-container> <br> <br> <md-input-container> <textarea mdInput placeholder="Message" formControlName="message"></textarea> <md-error>{{validationErrors['message']}}</md-error> </md-input-container> <br> <br> <button md-raised-button (click)="onCancel()" color="warn">Cancel</button> <button md-raised-button type="submit" color="primary" [disabled]="postForm.dirty && !postForm.valid">Submit</button> <br> <br> </form>

The most important thing is to reference the formGroup we have created with the FormBuilder, which is the [formGroup]="postForm" assignment. Fields inside the form are bound to the form model through the formControlName property. Again, we disable the “Submit” button when the form is not valid. We also need to add the dirty check, because here, the non-dirty form can still be invalid. We want the initial state of the button to be “enabled” though.

Summary: This approach to forms in React and Angular is quite different on both validation and template fronts. The Angular approach involves a bit more “magic” instead of straightforward binding, but, on the other hand, is more complete and thorough.

번들 크기

아, 한 가지 더. The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular.

  • Angular: 1200 KB
  • React: 300 KB

Well, not much surprise here. Angular has always been the bulkier one.

When using gzip, the sizes go down to 275kb and 127kb respectively.

Just keep in mind, this is basically all vendor libraries. The amount of actual application code is minimal by comparison, which is not the case in a real-world application. There, the ratio would be probably more like 1:2 than 1:4. Also, when you start including a lot of third-party libraries with React, the bundle size also tends to grow rather quickly.

Flexibility of Libraries vs. Robustness of Framework

So it seems that we have not been able (again!) to turn up a clear answer on whether Angular or React is better for web development.

It turns out that the development workflows in React and Angular can be very similar, depending on which libraries we chose to use React with. Then it's a mainly a matter of personal preference.

If you like ready-made stacks, powerful dependency injection and plan to use some RxJS goodies, chose Angular.

If you like to tinker and build your stack yourself, you like the straightforwardness of JSX and prefer simpler computable properties, choose React/MobX.

Again, you can get the complete source code of the application from this article here and here.

Or, if you prefer bigger, RealWorld examples:

  • RealWorld Angular 4+
  • RealWorld React/MobX

Choose Your Programming Paradigm First

Programming with React/MobX is actually more similar to Angular than with React/Redux. There are some notable differences in templates and dependency management, but they have the same mutable/data binding paradigm.

React/Redux with its immutable/unidirectional paradigm is a completely different beast.

Don't be fooled by the small footprint of the Redux library. It might be tiny, but it's a framework nevertheless. Most of the Redux best practices today are focused on using redux-compatible libraries, like Redux Saga for async code and data fetching, Redux Form for form management, Reselect for memorized selectors (Redux's computed values). and Recompose among others for more fine-grained lifecycle management. Also, there's a shift in Redux community from Immutable.js to Ramda or lodash/fp, which work with plain JS objects instead of converting them.

A nice example of modern Redux is the well-known React Boilerplate. It's a formidable development stack, but if you take a look at it, it is really very, very different from anything we have seen in this post so far.

I feel that Angular is getting a bit of unfair treatment from the more vocal part of JavaScript community. Many people who express dissatisfaction with it probably do not appreciate the immense shift that happened between the old AngularJS and today's Angular. In my opinion, it's a very clean and productive framework that would take the world by storm had it appeared 1-2 years earlier.

Still, Angular is gaining a solid foothold, especially in the corporate world, with big teams and needs for standardization and long-term support. Or to put it in another way, Angular is how Google engineers think web development should be done, if that still amounts to anything.

As for MobX, similar assessment applies. Really great, but underappreciated.

In conclusion: before choosing between React and Angular, choose your programming paradigm first.

mutable/data-binding or immutable/unidirectional , that… seems to be the real issue.