() translation by (you can also view the original English article)
React는 인터랙티브한 UI를 구축하는 대중화된 자바스크립트 프론트 엔드 라이브러리 입니다. React는 상대적으로 빨리 배울 수 있는데, 그 점이 최근들어 주목을 끌게 된 이유 중 하나입니다.
훑어봐야 하는 중요한 개념들이 많겠지만, component는 React에서 가장 중요한 개념임이 명백합니다. React 개발자로서 component를 잘 이해한다면 일이 수월해 질 것입니다.
전제 조건
이 튜토리얼은 React를 막 배우기 시작하고 component를 좀 더 개략적으로 알아야 하는 초보자를 대상으로 합니다. Component의 기본으로 시작해서 component 패턴과 그러한 패턴을 사용할 시점 같이 좀 더 난이도 있는 개념으로 옮겨가겠습니다. 클래스(class) 대 함수형(functional) 컴포넌트, stateful 대 stateless 컴포넌트, 컨테이너(container) 대 프레젠테이셔널(presentational) 컴포넌트와 같은 여러가지 컴포넌트 유형을 다룰 것입니다.
설명을 시작하기 전에 여러분에게 이 튜토리얼에서 사용할 코드를 소개하고 싶습니다. React로 구축된 간단한 카운터입니다. 튜토리얼에서 전반적으로 이 예제를 부분적으로 언급할 것입니다.
시작해 봅시다.
컴포넌트란 무엇인가?
컴포넌트는 UI의 일부분을 묘사하는 자생적이고 독립적인 아주 작은 엔터티입니다. 하나의 애플리케이션 UI는 더 작은 컴포넌트로 쪼개질 수 있습니다. 각각의 쪼개진 컴포넌트에는 해당 코드와 구조, API가 있습니다.
페이스북을 예로 들면, 여러분이 그 웹 애플리케이션을 볼 때 거기에는 수천 개의 기능이 함께 서로 연결되어 있습니다. 흥미로운 점은, 페이스북이 3만 개의 컴포넌트로 구성되며 그 수는 점점 늘고 있다는 사실입니다. 여러분은 그 컴포넌트 아키텍처를 낱개로 떼어 생각할 수 있습니다. 각 개별 컴포넌트는 다른 컴포넌트에 어떻게 영향이 미칠지를 염려하지 않아도 해당 범위 안에서 어떤 것이든지 업데이트 할 수 있습니다.
페이스북의 UI를 예로 들면, 아마 검색 바가 컴포넌트에 관한 좋은 후보가 됩니다. 페이스북의 뉴스피드도 또다른 컴포넌트 (혹은 다수의 하위(sub) 컴포넌트를 관리하는 컴포넌트)일지 모릅니다. 검색 바와 관련 있는 메소드와 AJAX 호출은 그 컴포넌트 안에 들어갑니다.
컴포넌트 역시 재사용 가능합니다. 여러 곳에서 동일한 컴포넌트가 쓰여야 한다면, 쉽게 할 수 있습니다. JSX 구문을 이용해 컴포넌트가 보여지기 바라는 곳마다 명시해 주면 됩니다. 그게 다예요.
1 |
<div> |
2 |
Current count: {this.state.count} |
3 |
<hr /> |
4 |
{/* Component reusability in action. */ } |
5 |
<Button sign = "+" count={this.state.count} |
6 |
updateCount = {this.handleCount.bind(this) }/> |
7 |
<Button sign = "-" count={this.state.count} |
8 |
updateCount = {this.handleCount.bind(this) }/> |
9 |
</div> |
Props와 State
컴포넌트에는 함께 작동할 데이터가 있어야 합니다. 컴포넌트와 데이터를 묶을 수 있는 두 가지 방법이 있습니다. props 나 state를 쓰는 것이죠. props와 state로 컴포넌트가 무엇을 렌더링하고 어떻게 동작할지 정합니다. props부터 시작해 보죠.
props
만약 컴포넌트가 일반적인 자바스크립트 함수라면, props는 함수의 입력(input)이 됩니다. 은유적으로 표현해 본다면, 컴포넌트는 입력(props라고 부르는)을 받아 절차를 거쳐서 JSX 코드를 넘겨 줍니다



props에 있는 데이터는 컴포넌트에 접근할 수 있지만, props는 변하지 않으며 상의하달식이어야 한다는 게 React의 철학입니다. 무슨 얘기인가 하면, 부모 컴포넌트는 어떤 데이터이든지 props로서 자식 컴포넌트에 전달할 수 있습니다. 그러나 자식 컴포넌트는 전달 받은 props를 수정하지 못합니다. 그러니 제가 아래에 했던 것처럼 여러분이 props를 편집 하려고 하면, “Cannot assign to read-only” TypeError를 보게 될 것입니다.
1 |
const Button = (props) => { |
2 |
// props are read only
|
3 |
props.count =21; |
4 |
.
|
5 |
.
|
6 |
}
|
State
그와 달리 state는 선언된 곳에 있는 컴포넌트가 소유한 일종의 오브젝트입니다. 그 적용 범위는 현재 컴포넌트에 한정됩니다. 필요할 때마다 컴포넌트는 그 안에 있는 state를 초기화하고 업데이트할 수 있습니다. 부모 컴포넌트의 state는 보통 자식 컴포넌트의 props가 되는 것으로 끝이 납니다. state가 현재의 범위 밖으로 전달될 때 우리는 그것을 prop처럼 참조합니다.
컴포넌트의 기본 지식을 알았으니 이제는 컴포넌트의 기본 유형을 보죠.
클래스 컴포넌트 대 함수형 컴포넌트
React 컴포넌트에는 두 가지 유형이 있습니다. 클래스 컴포넌트이거나 함수형 컴포넌트입니다. 그 두 가지 유형의 차이는 명칭에서 뚜렷하게 드러납니다.
함수형 컴포넌트
함수형 컴포넌트는 그저 자바스크립트 함수입니다. 제가 앞에서 얘기했듯이, 이 컴포넌트는 props로 부르는 입력을 선택적으로 취합니다.
일부 개발자들은 컴포넌트를 정의하는 데 새로운 ES6 arrow 함수를 선호합니다. Arrow 함수는 함수 표시를 하는 데 좀 더 군더더기 없이 깔끔하며 정확한 구문법(syntax)을 제공합니다. Arrow 함수를 사용함으로써 두 가지 키워드인 function
과 return
, 그리고 중괄호를 빼고 적용할 수 있습니다.
1 |
const Hello = ({ name }) => (<div>Hello, {name}!</div>); |
클래스 컴포넌트
클래스 컴포넌트는 더 많은 피처(features)를 제공합니다. 더 많은 피처를 사용해 더 많은 작업이 생기지만요. 함수형 컴포넌트가 아닌 클래스 컴포넌트를 선택하는 주된 이유는 state
를 넣을 수 있다는 것입니다.



클래스 컴포넌트를 작성하는 데는 두 가지 방식이 있습니다. React.createClass()
를 사용하는 게 일반적인 방식입니다. ES6에서는 React.Component
를 확장하는 class를 작성할 수 있는 syntax sugar을 도입했습니다. 그렇더라도 그런 방식들은 똑같이 작동하게 되어 있습니다.
클래스 컴포넌트도 state없이 존재할 수 있습니다. 입력을 받고 JSX로 넘겨 주는 클래스 컴포넌트의 예가 여기에 있습니다.
1 |
class Hello extends React.Component { |
2 |
constructor(props) { |
3 |
super(props); |
4 |
}
|
5 |
|
6 |
render() { |
7 |
return( |
8 |
<div> |
9 |
Hello {props} |
10 |
</div> |
11 |
)
|
12 |
}
|
13 |
}
|
Props를 입력으로 받는 생성자(constructor) 메소드를 정의합니다. 그 생성자 안에, 부모 클래스로부터 상속 받은 무엇이건 전달하는 super()를 호출합니다. 여러분이 지나칠 수 있는 몇 가지 사항이 있습니다.
우선, 컴포넌트를 정의하는 동안 생성자는 선택입니다. 위의 경우에 컴포넌트에는 state가 없고 생성자는 유용한 동작을 하는 것처럼 보이지 않습니다. render()
안에 있는 this.props
는 생성자가 정의 되거나 그렇지 않거나 상관없이 동작할 것입니다. 그런데 공식 문서에 적혀 있는 것이 있습니다.
클래스 component는props
와 함께 기본 생성자를 호출해야 합니다.
하나의 모범 사례로서 모든 클래스 컴포넌트에 관해 생성자를 사용할 것을 권장드립니다.
두 번째로 여러분이 생성자를 사용한다면, super()
를 호출해야 합니다. 이는 선택 사항이 아니기 때문에 호출하지 않으면 “Missing super() call in constructor”라는 구문 에러가 날 것입니다.
마지막으로 super()
대 super(props)
의 사용에 관한 것입니다. 생성자 안에서 this.props
를 호출하려고 할 때 super(props)
를 사용해야 합니다. 그런 경우가 아니라면 super()
만 사용해도 충분합니다.
Stateful 컴포넌트 대 Stateless 컴포넌트
컴포넌트를 분류하는 또다른 일반적인 방식입니다. 분류의 기준은 간단합니다. state가 있는 컴포넌트와 state가 없는 컴포넌트이지요.
Stateful 컴포넌트
Stateful 컴포넌트는 늘 클래스 컴포넌트입니다. 앞서 얘기했듯이 stateful 컴포넌트에는 생성자에서 초기화되는 state가 있습니다.
1 |
// Here is an excerpt from the counter example
|
2 |
constructor(props) { |
3 |
super(props); |
4 |
this.state = { count: 0 }; |
5 |
}
|
우리는 state 오브젝트를 만들고, count를 0으로 함으로써 state를 초기화했었습니다. 클래스 영역에서 보다 쉽게 호출하도록 제안된 대체 구문(syntax)가 있습니다. ECMAScript 명세에 들어가 있지는 않지만, 여러분이 Babel transpiler를 사용한다면 이 구문이 기발하게 작동할 것입니다.
1 |
class App extends Component { |
2 |
|
3 |
/*
|
4 |
// Not required anymore
|
5 |
constructor() {
|
6 |
super();
|
7 |
this.state = {
|
8 |
count: 1
|
9 |
}
|
10 |
}
|
11 |
*/
|
12 |
|
13 |
state = { count: 1 }; |
14 |
|
15 |
handleCount(value) { |
16 |
this.setState((prevState) => ({count: prevState.count+value})); |
17 |
}
|
18 |
|
19 |
render() { |
20 |
// omitted for brevity
|
21 |
}
|
22 |
|
23 |
}
|
이 새로운 구문을 이용해 생성자를 사용하지 않을 수 있습니다.
이제 render()
를 포함한 클래스 메서드 안에 들어 있는 state에 접근 가능합니다. 현재 count 값을 보여주려고 render()
안에 클래스 메서드를 사용하려 한다면 다음과 같이 중괄호 안에 state를 위치시켜야 합니다.
1 |
render() { |
2 |
return ( |
3 |
Current count: {this.state.count} |
4 |
)
|
5 |
}
|
여기에서 this
키워드는 현재 컴포넌트의 인스턴스를 참조합니다.
State를 초기화하는 것만으로 충분치 않겠죠. 상호작용하는 애플리케이션을 만들려면 state를 업데이트 할 수 있어야 합니다. 이것만으로 될 거라고 생각한다면, 아니예요. 되지 않아요.
1 |
//Wrong way
|
2 |
|
3 |
handleCount(value) { |
4 |
this.state.count = this.state.count +value; |
5 |
}
|
React 컴포넌트에는 state를 업데이트하기 위해 setState라는 메서드가 있습니다. setState는 count
의 새로운 state를 포함한 오브젝트를 받습니다.
1 |
// This works
|
2 |
|
3 |
handleCount(value) { |
4 |
this.setState({count: this.state.count+ value}); |
5 |
}
|
setState()
는 오브젝트를 하나의 입력으로서 받아들이고, 우리는 count의 이전 값에 1만큼 증가시킵니다. 예상했던 대로 동작합니다. 그런데 한 가지 문제가 있습니다. state의 이전 값을 읽어 새 값을 작성하는 setState 호출이 여러 번 있을 때는 아마도 경합 조건(race condition)으로 끝나버릴 것입니다. 무슨 의미인가 하면, 최종 결과에서 예상했던 값이 안 나올 거라는 뜻입니다.
여기 명확히 이해할 수 있는 예제가 있습니다. 위의 Codesandbox 스니펫에서 적용해 보세요.
1 |
// What is the expected output? Try it in the code sandbox.
|
2 |
handleCount(value) { |
3 |
this.setState({count: this.state.count+100}); |
4 |
this.setState({count: this.state.count+value}); |
5 |
this.setState({count: this.state.count-100}); |
6 |
}
|
100씩 셈이 더해지는 setState가 필요하고, 그 후에는 1씩 업데이트되며, 그러고 나서 이전에 더해진 100을 뺍니다. setState가 실제 순서대로 state 전환을 실행한다면, 예상된 동작을 볼 것입니다. 하지만 setState는 비동기식이라서 다수의 setState 호출은 더 나은 UI 경험과 실행을 위해 한꺼번에 배치될 것입니다. 고로 위의 코드는 우리의 예상과 다른 동작으로 동작하게 됩니다.
결과적으로 오브젝트를 직접 전달하는 것 대신에 특정한 업데이트 함수로 전달하게 됩니다.
1 |
(prevState, props) => stateChange |
2 |
prevState는 이전 state를 레퍼런스하며 최신 상태로 값을 유지해줍니다. props는 컴포넌트의 props이며, 여기에서 state를 업데이트하는 데 props가 필요하진 않습니다. 고로 신경쓰지 않아도 됩니다. 그러니 우리는 이를 state 업데이트 용으로 사용해 경합 조건을 피할 수 있습니다.
1 |
// The right way
|
2 |
|
3 |
handleCount(value) { |
4 |
|
5 |
this.setState((prevState) => { |
6 |
count: prevState.count +1 |
7 |
});
|
8 |
}
|
setState()
는 컴포넌트를 다시 렌더링하며, stateful component가 동작하게 됩니다.
Stateless 컴포넌트
stateless 컴포넌트를 만드는 데 함수형이나 클래스를 사용하면 됩니다. 그러나 컴포넌트에서 라이프사이클을 적용해야만 stateless 함수형 컴포넌트의 가치를 갖게 됩니다. 여기에서 stateless 함수형 컴포넌트를 사용하고자 결정한다면 이득이 많이 있습니다. 작성하고 이해하며 테스트하기가 용이합니다. 그리고 this
키워드를 모두 적지 않아도 됩니다. 그렇지만 React v16이 나온 현재로서는 클래스 컴포넌트를 사용하는 것보다 stateless 함수형 컴포넌트를 사용하는 게 성능상 이득이 있지는 않습니다.
안좋은 점은 여러분이 라이프사이클을 활용하지 못한다는 것입니다. 라이프사이클 메서드인 ShouldComponentUpdate()
는 종종 성능을 최적화하고 렌더링 시킬 것을 수동으로 제어하는 데 쓰입니다. 아직까지는 함수형 컴포넌트에서 그 메서드를 적용하지 못합니다. 참조자(refs)도 지원되지 않습니다.
컨테이너 컴포넌트 대 프레젠테이셔널 컴포넌트
컴포넌트를 작성하면서 매우 유용한 패턴입니다. 이러한 접근방식은 행동(behavior) 로직을 표현(presentational) 로직과 분리하는데 이롭습니다.
프레젠테이셔널 컴포넌트
프레젠테이셔널 컴포넌트는 뷰(view) 혹은 어떻게 보이게 할지와 관련이 있습니다. 이러한 컴포넌트는 그에 상응하는 컨테이너 쌍으로부터 props를 받아 렌더링합니다. UI를 서술하는 것과 연관된 코드는 모두 여기로 가야합니다.
프레젠테이셔널 컴포넌트는 재사용 가능하며 동적인 레이어로부터 분리되어 있어야 합니다. 프레젠테이셔널 컴포넌트는 오로지 props를 통해 데이터와 콜백을 받습니다. 그리고 버튼이 눌리는 식의 이벤트 에러가 발생했을 때, 이벤트 핸들링 메서드를 부르기 위해 props를 통해 컨테이너 컴포넌트로 콜백을 수행합니다.
state를 요구하지 않는 한 프레젠테이셔널 컴포넌트를 작성하는 데 함수형 컴포넌트를 우선적으로 사용해야 합니다. 만일 프레젠테이셔널 컴포넌트에 state가 필요하다면 실 데이터가 아닌 UI state와 연관시켜야 합니다. 프레젠테이셔널 컴포넌트는 Redux store와 상호작용을 하거나 API 호출을 하지 않습니다.
컨테이너 컴포넌트
컨테이너 컴포넌트는 동작과 관련한 부분을 다룹니다. 컨테이너 컴포넌트는 프레젠테이셔널 컴포넌트에게 props를 이용해 어떻게 렌더링되어야 하는지 얘기해 줍니다. 제한된 DOM 마크업과 스타일을 포함하지 않습니다. 여러분이 Redux를 이용한다면 컨테이너 컴포넌트에는 저장소로 동작을 전달하는 코드를 넣을 것입니다. 그렇지 않으면 API 호출을 넣고 컴포넌트의 state로 결과 값을 저장하는 위치가 되겠지요.
일반적인 구조에서는 데이터를 props로서 프레젠테이셔널 컴포넌트로 하향 전달하는 컨테이너 컴포넌트가 상위에 있습니다. 규모가 작은 프로젝트에 적합한 구조입니다. 프로젝트의 규모가 점점 커지면 props를 받아서 자식 컴포넌트로 전달하는 중간에 위치한 컴포넌트들이 많아지게 되어 지저분하고 유지 관리가 힘들어집니다. 그런 상황이 벌어질 때는 리프(leaf) 컴포넌트에 고유한 컨테이터 컴포넌트를 만드는 게 더 좋습니다. 그러면 중간에 있는 컴포넌트들에 대한 부담감이 덜어질 것입니다.
그렇다면 PureComponent란 무엇인가?
여러분은 React 사이클에서 pure component란 용어와 그 다음에는 React.PureComponent
가 있다는 얘기를 흔히 듣게 될 것입니다. React가 생소하다면 약간 혼란스러울 수 있습니다. 동일한 props와 state라는 전제 하에 동일한 결과 값이 확실히 반환된다면 컴포넌트를 순수하다고(pure) 말합니다. pure component의 좋은 예가 함수형 컴포넌트이지요. 왜냐하면 입력이 있으면 무엇이 렌더링 될지 뻔하기 때문입니다.
1 |
const HelloWorld = ({name}) => ( |
2 |
<div>{`Hi ${name}`}</div> |
3 |
);
|
클래스 컴포넌트도 props와 state가 변하지 않는 한 순수(pure)할 수 있습니다. 만약 props와 state의 불변하는 'deep' 세트가 들어간 컴포넌트가 있다면, React API에는 PureComponent
라고 부르는 무언가가 있습니다. React.PureComponent
는 React.Component
와 유사하지만, ShouldComponentUpdate()
메서드를 약간 다른 식으로 실행합니다. ShouldComponentUpdate()
는 무언가를 렌더링하기 전에 호출되어 동작합니다. 기본 동작은 true를 반환하는 것인데 그렇게 해서 state나 props로의 변화가 컴포넌트에 그려지게 됩니다.
1 |
shouldComponentUpdate(nextProps, nextState) { |
2 |
return true; |
3 |
}
|
하지만 PureComponent로는 object를 피상적으로 비교하게 됩니다. 피상적인 비교(shallow comparison)란 모든 object의 key/value 쌍을 반복적으로 비교하는 대신 object의 현재 콘텐츠를 비교하는 것을 의미합니다. 그렇기에 object 참조자만 비교하게 됩니다. 만약 state/props가 새롭게 변형된다면 의도한대로 결과를 얻지 못할 수 있습니다.
React.PureComponent
는 성능을 최적화하는 데 활용됩니다. 여러분이 성능상의 이슈에 맞닥뜨리지 않는 한 이 컴포넌트를 사용해야 하는지 고려해 볼 이유는 없습니다.
최종 정리하기
Stateless 함수형 컴포너트는 보다 명쾌하며, 프레젠테이셔널 컴포넌트를 제작하는 데 일반적ㅇ로 좋은 선택이 됩니다. 함수형일 뿐이므로 작성하고 이해하는 데 어려움을 겪지 않을 것입니다. 더구나 테스트 하기에도 아주 쉽습니다.
stateless 함수형 컴포넌트는 ShouldComponentUpdate()
메서드가 없다는 이유로 최적화와 성능에서 우위라는 것은 아니라는 점을 알고 계세요. React 이후 버전에서 바뀔 지 모릅니다. 거기에서는 함수형 컴포넌트가 더 나은 성능을 위해 최적화될 지도 모릅니다. 여러분에게 성능이 중요하지 않다면 뷰/프레젠테이션 용으로 함수형 컴포넌트를, 컨테이너 용으로 stateful 클래스 컴포넌트를 고수하세요.
이 튜토리얼이 여러분에게 컴포넌트 기반의 구조에 관한 난이도 높은 개요와 React에서의 다양한 컴포넌트 패턴을 알려주길 바랍니다. 이에 대해 여러분의 생각은 어떤가요? 댓글로 의견을 공유해 주세요.
수십 년이 지나면서 React의 인기가 상승하고 있습니다. 실제로 Envato Market에 구매, 검토, 실행 등이 가능한 제품들이 많이 있습니다. React와 관련한 리소스를 더 많이 찾고 있다면 망설이지 말고 그 제품들을 확인해 보세요.