도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

누락된 stateful과 stateless 컴포넌트 메뉴얼

이 글의 목적은 상태기반(stateful)과 비상태기반(stateless) - 똑똑한(smart)과 멍청한(dumb), 또는 컨테이너(container)와 표현(presentationl) 컴포넌트로도 불리는- 가 무엇인지에 대해 정의하기 위함입니다. 이 글의 목적인 stateful/stateless의 컨셉에 대해 설명하기 위해 우리는 Angular 2의 컴포넌트를 사용 할 것 입니다. 이러한 컨셉은 Angular와 리액트같은 프레임워크에만 제한되지 않음을 유념하시기 바랍니다.

목차

용어

  • Stateful
  • Stateless
  • 컴포넌트

비순수 함수 대 순수한 함수

  • 비순수 함수 (stateful)
  • 순수한 함수 (stateless)

Stateful 컴포넌트

  • Stateful Todo 컴포넌트

Stateless 컴포넌트

  • Stateless TodoForm 컴포넌트
  • Statless TodoList 컴포넌트
  • 최종 코드

Angular 1.x 버전?

  • 최종 코드

참고 목록




용어

시작하기 전에, "stateful"과 "stateless"가 프로그래밍 용어에서 실제로 어떤 의미를 갖는지 명확하게 하겠습니다. 

Stateful

어떤 것이 "stateful"하다고 할때, 이것은 메모리에 앱/컴포넌트의 상태에 대한 정보들을 저장하는 중심 입니다. 이것은 또한 상태 값을 변경 할 수도 있습니다. 이것은 본질적으로 과거, 현재, 그리고 잠재적인 미래의 상태 변경에 대해 알고있는  "살아있는(living)" 것 입니다.

Stateless

어떤 것이 "stateless"하다고 할때, 이 것이 의미하는 것은 상태 값을 내부에서 계산을 하지만 절대 직접적으로 그 값을 변형시키지는 않는 것을 의미합니다. 이것은 완벽한 값의 투명성을 보장합니다. 이것이 의미하는 것은 입력값이 같을때, 결과값 역시 항상 같다는 것을 의미합니다. 

Components

우리가 웹 개발에서 stateful과 stateless에 대해서 얘기 할 때, 우리는 이러한 컨셉들을 컴포넌트 패러다임에 적용 할 수 있습니다. 그렇다면, 컴포넌트는 무엇 일까요? 컴포넌트는 자바스크립트의 함수와 거의 유사하게, 기능을 역할에 따라서 나눠놓은 행동 또는 기능의 조각 이라고 할 수 있습니다. 

순수 하지 않은 함수 대 순수 함수

우리가 여기서 stateful과 stateless 컴포넌트에 대해서 얘기할때는, 프레임워크에 대해서는 적용 단계 전까지는 전적으로 무시 하도록 하고, 자바스크립트의 함수에 대해서 생각 해보겠습니다. 자, 먼저 순수와 비순수 함수에 대해서 고려 해봅시다. 그리고 그것들을 stateful과 stateless와도 조금 비교 해보도록 하겠습니다. 저는 UI composition을 더 잘 이해하기 위해 컴포넌트 타입을 함수와 비교하는 것을 정말 좋아하니까요.

제 생각에는 이 글을 읽은 후에 여러분은 아래의 것에 대해 이해 할 수 있을 것 입니다:

  • 비순수 함수 = Stateful 컴포넌트
  • 순수 함수 = Stateless 컴포넌트

더 자세한 비순수 함수 대 순수 함수의 내용은 글을 확인 해보시기 바랍니다. 하지만 여기에서는 기본만 다루도록 하겠습니다.

비순수 함수 (stateful)

유저의 몸무게와 키 값을 파싱한 후, bmi(Body Mass Index) 공식으로 계산하는 아래의 코드를 한번 보도록 하겠습니다. 

const weight = parseInt(form.querySelector('input[name=weight]').value, 10);
const height = parseInt(form.querySelector('input[name=height]').value, 10);
const bmi = (weight / (height /100 * height / 100)).toFixed(1);

이 코드는 단순히 동작하는 코드라는 점에서는 좋다고 할 수 있지만, BMI를 다른 곳에서도 사용 할 수 있도록 하는 재사용 가능한 함수가 없고, 테스트 하기도 어려우며, 절차지향의 코드에 매우 의존적이이라고 할 수 있습니다. 자, 이 코드를 순수 함수로 만들어 보도록 하겠습니다. 순수 함수를 통해 외부의 변수에 의존하지 않고 데이터를 받아서 새로운 데이터를 리턴하는 작고 분리된 함수를 만들 것 입니다.  

순수 함수 (stateless)

우리가 순수함수에 대해서 생각 할 때,  외부의 값에 대해 모르더라도 항상 같은 결과를 리턴 할 것임을 예상 할 수 있습니다. 자, 위의 공식을 순수 함수로 리팩토링 해보도록 하겠습니다:

const weight = form.querySelector('input[name=weight]').value;
const height = form.querySelector('input[name=height]').value;
const getBMI = (weight, height) => {
let newWeight = parseInt(weight, 10);
let newHeight = parseInt(height, 10);
return (newWeight / (newHeight /100 * newHeight / 100)).toFixed(1);
};
const bmi = getBMI(weight, height);

getBMI 함수는 다른 어디에서도 사용 될 수 있습니다. 지금 저 함수는 순수 합니다. 그렇다면 "왜" 순수 함수라고 할 수 있는지 아래를 확인 해보십시오.

  • 가상의 데이터로 쉽게 테스트 가능합니다.
  • 재사용 가능합니다.
  • 정의된 입력 값이 있습니다. (함수 arguments)
  • 정의된 결과 값이 있습니다. (새로운 데이터를 리턴하는 statement)

위의 4개는 stateless 컴포넌트와 직접적으로 매핑 되는 컨셉들입니다. 

자, 이제 "비순수" 함수를 stateful 컴포넌트와 동등하게 생각하고, "순수" 함수를 stateless 컴포넌트와 매핑하여 생각 해봅시다.  

Stateful 컴포넌트

Much like an impure JavaScript function, a stateful component is the driver of what happens, and it therefore utilises any stateless components at our disposal. (이 부분은 어떻게 번역해야 할지 잘 모르겠습니다..;;)

stateful 컴포넌트가 갖는 몇 가지 속성:

  • 함수를 통해 상태 값을 변경
  • 데이터를 제공 (ie.e http 레이어에서)
  • 초기 데이터를 서비스 레이어 콜 대신 라우트를 통해 받을 수 있음
  • 현재 상태 값을 알 수 있음
  • stateless 컴포넌트로부터 변경이 필요 할 때 그 정보를 받을 수 있음
  • 외부 종속성(http 같은)이 있는 것과 커뮤니케이션을 할 수 있음
  • 단일의 <div> 같은 것으로 래핑된 stateless 또는 stateful한 child 컴포넌트를 렌더링 할 수 있음
  • Redux 액션을 포함 할 수 있음 (예제: ngrx/store 또는 ng2redux)

위의 목록과 나머지 글은 Dan Abramov의 Presentational and Container components 글에서 영감을 받았습니다.

Stateful Todo 컴포넌트

이 글에서 우리는 이러한(stateful) 컨셉과 stateless 컨셉을 보여주기 위해 간단한 Todo 어플리케이션을 만들 것입니다. 

먼저, 베이스 컴포넌트(<app>)를 렌더링 하는 것으로 시작 해보겠습니다:

import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<todos></todos>
`
})
export class AppComponent { }

여기에서, 우리는 <todos> 컴포넌트를 렌더링 하고있습니다. 이것은 stateful한 컴포넌트가 될 것 입니다. 자, 계속 해보겠습니다! todo 앱 만드는 것은 거의 다 알고 있다고 생각하기 때문에 todo 앱을 어떻게 만드는지에 대해서는 다루지 않을 것입니다.  그래서, 앞으로는 어떻게 stateful과 stateless 패러다임을 Angular2 컴포넌트에 적용하고, 이러한 아이디어에 대해서 관찰 해보도록 하겠습니다.

우리는 계속해서 컴포넌트 컴포지션을 ASCII 형태로 볼 수 있을 것입니다. 지금은 <app> 컴포넌트가 있습니다:

          ┌─────────────────┐          
│ <app> │
└─────────────────┘          

이제는 <todos> 컴포넌트를 보도록 하겠습니다:

import { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';
@Component({
selector: 'todos',
template: `
<div>
<todo-form
(onAdd)="addTodo($event)">
</todo-form>
<todo-list
[todos]="todos"
(onComplete)="completeTodo($event)"
(onDelete)="removeTodo($event)">
</todo-list>
</div>
`
})
export class TodosComponent implements OnInit {
todos: any[];
constructor(private todoService: TodoService) {}
ngOnInit() {
this.todos = this.todoService.getTodos();
}
addTodo({label}) {
this.todos = [{label, id: this.todos.length + 1}, ...this.todos];
}
completeTodo({todo}) {
this.todos = this.todos.map(
item => item.id === todo.id ? Object.assign({}, item, {complete: true}) : item
);
}
removeTodo({todo}) {
this.todos = this.todos.filter(({id}) => id !== todo.id);
}
}

위의 코드에서 볼 수 있듯이 <div>로 랩핑되어있고 2개의 child (stateless) 컴포넌트를 포함한 컨테이너가 있습니다. 이외의 템플릿의 다른 로직은 없습니다. <todo-form> 컴포넌트는 어떤 입력 값도 받지 않지만, onAdd라는 출력을 바인딩 할 것 입니다. 다음으로, <todo-list> 컴포넌트는 [todos] 입력값이 바인딩된 todos 데이터를 받습니다. 그리고, 두개의 결과 값 (onComplete)와 (onDelete)을 stateless 컴포넌트로 위임(delegating)합니다.

나머지 컴포넌트의 클래스는 todo 컴포넌트의 기능들을 구성하는 메쏘드들 입니다. 불변하는 작업(Immutable operation)은 각각의 콜백 내에서 실행되고 있습니다. 그리고 각각의 콜백은 stateless 컴포넌트의에서 실행 할 수 있도록 합니다. 이러한 모든 함수들은 뭔가가 변경되는 것을 알리는 것입니다. 예를들어, "이봐! 여기 새로운 todo 라벨이 있어, stateful 컴포넌트에서 선언해놓은 일을 해. 어떻게 함수들이 child인 stateless 레벨에서 불리는지(called) 눈여겨보도록 합시다.

이것이 말그대로 stateful 컴포넌트 입니다. stateful 컴포넌트가 가질 수 있는 가능한 컨셉들에 대해서 커버 했습니다. 그럼 더 자주 사용되는 stateless 컴포넌트로 넘어가보도록 하겠습니다. 

ASCII (TodoService는 주입된 서비스를 대신 합니다):

          ┌─────────────────┐          
│ <app> │
└────────┬────────┘

┌─────────────────────────────┐
│ <todos> │
│ ┌─────────────────┐ │
│ │ TodoService │ │
└─────┴─────────────────┴─────┘   

Stateless 컴포넌트

순수 자바스크립트 함수와 같이, stateless 컴포넌트는 데이터를 바인딩된 프로퍼티(함수의 argument와 같은)로 받기 때문에 "lexical" 변수에 대해서는 신경쓰지 않습니다. 그리고 event를 통해 변화를 emit(함수의 return block과 같은)합니다.

저것은 무엇을 의미 할까요? 어떻게 함수 스코프 체인이 실행 되는지에 대해 생각해본다면, stateless 컴포넌트는 어플리케이션의 어떤 부분도 아는게 없다는 것을 의미합니다. stateless 컴포넌트는 재사용 될 수 있고, 쉽게 테스트 가능하며 쉽게 이동 될 수 있다는 것을 의미 합니다. 

여기 stateless 컴포넌트가 갖고 있는 몇가지 속성이 있습니다:

  • 데이터를 요청/불르거나 하지 않음
  • 프로퍼티 바인딩을 통해 데이터를 받음
  • 데이터를 이벤트 콜백을 통해 내뿜음(emit)
  • stateless (또는 stateful) 컴포넌트를 렌더링 함
  • 지역(local) UI 상태를 포함 할 수 있음
  • 큰 그림의 작은 조각 임

Stateless TodoForm 컴포넌트

이 컴포넌트를 시작하기 전에, 이것이 특별한 종류의 stateless 컴포넌트라는 것을 이해해야 합니다. 왜나하면, 이것은 사용자의 입력 값을 회수하여 UI 상태 값만 존재 하는 컴포넌트이기 때문 입니다:

import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'todo-form',
template: `
<form (ngSubmit)="submit()">
<input name="label" [(ngModel)]="label">
<button type="submit">Add todo</button>
</form>
`
})
export class TodoFormComponent {
label: string;
@Output() onAdd = new EventEmitter();
submit() {
if (!this.label) return;
this.onAdd.emit({label: this.label});
this.label = '';
};
}

이 컴포넌트는 또한  어떤 데이터도 프로퍼티 바인딩을 통해 받지는 않지만, 완벽히 받아들여질 수 있습니다. 이 컴포넌트의 역할은 새로운 todo 아이템의 label을 캡쳐링 하는 것 입니다. 이것은 stateless 컴포넌트가 내부적으로 UI 상태를 캡쳐링 하는 함수를 가지고 있고, 그것으로 무엇인가 하는 특별한 유즈케이스 입니다.

Stateless TodoList 컴포넌트

<todos> 컴포넌트의 바로 하위의 child인 두번째 stateless 컴포넌트를 보도록 합시다:

import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'todo-list',
template: `
<ul>
<li *ngFor="let todo of todos">
<todo
[item]="todo"
(onChange)="onComplete.emit($event)"
(onRemove)="onDelete.emit($event)">
</todo>
</li>
</ul>
`
})
export class TodoListComponent {
@Input() todos;
@Output() onComplete = new EventEmitter();
@Output() onDelete = new EventEmitter();
}

우리의 "@Input"과 "@Output"이 여기 잘 정의되어 있고, 여기서 볼 수 있듯이 이 컴포넌트 클래스에는 다른 것은 존재하지 않습니다. 우리는 실제로 각 결과값에 EventEmitter 인스턴스를 만들었고, 이것을 stateless 컴포넌트 - 여기서는 컬렉션에 있는 각각의 todo를 렌더링 할 <todo> 컴포넌트- 에 위임하였습니다. 우린 또한 부모 컴포넌트에 바운딩 될 onComplete과 onDelete 메쏘드를 여기에 위임했습니다. <todo> 컴포넌트를 보도록 하겠습니다:

import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'todo',
styles: [`
.complete { text-decoration: line-through; }
`],
template: `
<div>
<span [ngClass]="{ complete: item.complete }"></span>
<button
type="button"
(click)="onChange.emit({ todo: item });">Done</button>
<button
type="button"
(click)="onRemove.emit({ todo: item });">Delete</button>
</div>
`
})
export class TodoComponent {
@Input() item;
@Output() onChange = new EventEmitter();
@Output() onRemove = new EventEmitter();
}

Hopefully you can see a pattern emerging here! Again, we have some inputs and outputs that can send event information up to the parent, then up again (if needed). All of the above Angular 2 components are stateless. They have no knowledge of their surroundings, but are passed data via property bindings and emit changes via event callbacks.

여기서 다뤄지는 패턴이 있다는 것을 볼 수 있습니다! 다시말하면, 우리는 event 정보를 부모 컴포넌트에 보낼 수 있는 몇개의 입력 값과 결과 값이 있다는 것 입니다. 위의 모든 앵귤러2 컴포넌트는 stateless 입니다. 저것들은 앱의 상태에 대해선 알지 못합니다. stateless 컴포넌트는 데이터를 프로퍼티 바인딩을 통해서 받고, 그 변화를 이벤트 콜백을 통해 알린다는(emit) 것 입니다.  

여기 우리가 다뤄왔던 최종 컴포넌트 트리를 ASCII로 확인 할 수 있습니다:

          ┌─────────────────┐          
│ <app> │
└────────┬────────┘

┌─────────────────────────────┐
│ <todos> │
│ ┌─────────────────┐ │
┌┤ │ TodoService │ ├┐
│└─────┴─────────────────┴─────┘│
┌──▼──────────────┐ ┌──────────────▼──┐
│ <todo-form> │ │ <todo-list> │
└─────────────────┘ └──────────────┬──┘
┌──────────────▼──┐
│ <todo> │
└─────────────────┘

최종 코드

Angular 1.x 버전?

전체 앵귤러 1.x 버전의 코드

const todos = {
template: `
<div>
<todo-form
new-todo="$ctrl.newTodo"
on-add="$ctrl.addTodo($event);">
</todo-form>
<todo-list
todos="$ctrl.todos"
on-complete="$ctrl.completeTodo($event);"
on-delete="$ctrl.removeTodo($event);">
</todo-list>
</div>
`,
controller: class TodoController {
constructor(TodoService) {
this.todoService = TodoService;
}
$onInit() {
this.todos = this.todoService.getTodos();
}
addTodo({ label }) {
this.todos = [{ label, id: this.todos.length + 1 }, ...this.todos];
}
completeTodo({ todo }) {
this.todos = this.todos.map(
item => item.id === todo.id ? Object.assign({}, item, { complete: true }) : item
);
}
removeTodo({ todo }) {
this.todos = this.todos.filter(({ id }) => id !== todo.id);
}
}
};
const todoForm = {
bindings: {
onAdd: '&'
},
template: `
<form ng-submit="$ctrl.submit();">
<input ng-model="$ctrl.label">
<button type="submit">Add todo</button>
</form>
`,
controller: class TodoFormController {
constructor() {}
submit() {
if (!this.label) return;
this.onAdd({
$event: { label: this.label }
});
this.label = '';
};
}
};
const todoList = {
bindings: {
todos: '<',
onComplete: '&',
onDelete: '&'
},
template: `
<ul>
<li ng-repeat="todo in $ctrl.todos">
<todo
item="todo"
on-change="$ctrl.onComplete($locals);"
on-remove="$ctrl.onDelete($locals);">
</todo>
</li>
</ul>
`
};
const todo = {
bindings: {
item: '<',
onChange: '&',
onRemove: '&'
},
template: `
<div>
<span ng-class="{ complete: $ctrl.item.complete }"></span>
<button
type="button"
ng-click="$ctrl.onChange({ $event: { todo: $ctrl.item } });">Done</button>
<button
type="button"
ng-click="$ctrl.onRemove({ $event: { todo: $ctrl.item } });">Delete</button>
</div>
`
};
class TodoService {
constructor() {}
getTodos() {
return [{
label: 'Eat pizza',
id: 0,
complete: true
},{
label: 'Do some coding',
id: 1,
complete: true
},{
label: 'Sleep',
id: 2,
complete: false
},{
label: 'Print tickets',
id: 3,
complete: true
}];
}
}
angular
.module('app', [])
.component('todos', todos)
.component('todo', todo)
.component('todoForm', todoForm)
.component('todoList', todoList)
.service('TodoService', TodoService);

추가 참고 

"@Input", "@Output" 과 "EventEmitter"에 더 알고 싶다면 "Input", "Output"과 "EventEmitter" 글을 읽어 보시기 바랍니다.




이 글은 Stateful and stateless components, the missing manual 번역한 글입니다. 전문 번역가가 아니라서 오역이 많을 수 있습니다. 오역에 대해 지적해주시면 바로 수정 하도록 하겠습니다. 이 글에 대한 원문은 아래에서 확인 할 수 있습니다.



리뷰