컴포넌트¶
리엑트 네이티브는 컴포넌트들이 중요한 역할을 합니다. 화면을 구성하는 단위라고 생각할 수 있습니다. 리엑트 네이티브 컴포넌트는 리엑트 컴포넌트를 사용합니다.
컴포넌트는 UI의 일부분을 묘사하는 자생적이고 독립적인 아주 작은 엔터티입니다. 하나의 애플리케이션 UI는 더 작은 컴포넌트로 쪼개질 수 있습니다. 각각의 쪼개진 컴포넌트에는 해당 코드와 구조, API가 있습니다. 2
리엑트 컴포넌트에는 두 가지 유형이 있습니다. 클래스 컴포넌트와 함수형 컴포넌트입니다. 그 두 가지 유형의 차이는 명칭에서 뚜렷하게 드러납니다.
컴포넌트 생성¶
컴포넌트를 만드는 방법은 클래스 기반 컴포넌트(React.Component, React.PureComponent)를 상속하거나 함수 기반으로 만드는 방법이 있습니다.
함수형 컴포넌트¶
함수형 컴포넌트는 자바스크립트 함수입니다. 이 컴포넌트는 props로 부르는 입력을 선택적으로 취할 수 있습니다.
화살함수(arrow function)를 사용함으로써 두 가지 키워드인 function
과 return
, 그리고 중괄호를 빼고 적용할 수도 있습니다.
const Hello = ({ name }) => (<div>Hello, {name}!</div>);
함수형 컴포넌트가 클래스 기반의 컴포넌트와 다른점은 무엇일까요? 함수형 컴포넌트는 클래스 기반의 컴포넌트와 달리, state, 라이프 사이클 메소드(componetDidMount, shouldComponentUpdate 등등..)와 ref 콜백을 사용 할 수 없다는데 있습니다(context는 사용 할 수 있습니다). 언뜻 보면, 함수형 컴포넌트가 state도 없고, 라이프 사이클도 신경쓰지 않기 때문에, 클래스 기반의 컴포넌트 보다 퍼포먼스가 뛰어날 것이라고 예상 하지만 실제로는 그렇지 않습니다. 이유는 함수형 컴포넌트도 결국엔 클래스 기반 컴포넌트로 래핑(wrapping)되기 때문입니다. 1
클래스 기반 컴포넌트 생성¶
클래스 컴포넌트는 더 많은 피처(features)를 제공합니다. 더 많은 피처를 사용해 더 많은 작업이 생기지만요. 함수형 컴포넌트가 아닌 클래스 컴포넌트를 선택하는 주된 이유는 state를 넣을 수 있다는 것입니다. 2
클래스 기반 컴포넌트(React.Component, React.PureComponent)에 대해 알아봅니다. 두 개는 shouldComponentUpdate 라이프 사이클 메소드를 다루는 방식을 제외하곤 동일합니다. 즉, PureComponent는 shouldComponentUpdate 라이프 사이클 메소드가 적용된 버전의 React.Component 클래스라고 보면 됩니다. 1
React.Component를 상속(extends)해서 컴포넌트를 만들 때, shouldComponentUpdate 메소드를 별도로 선언하지 않았다면, 컴포넌트는 props, state 값이 변경되면 항상 리렌더링(re-render) 하도록 되어 있습니다.
하지만, React.PureComponent를 상속해서 컴포넌트를 만들면, shouldComponentUpdate 메소드를 선언하지 않았다고 하더라도, PureComponent 내부에서 props와 state를 shallow level 안에서 비교 하여, 변경된 값이 있을 시에만 리렌더링 하도록 되어 있습니다. 이를 제외하곤 React.Component와 React.PureComponent의 다른 점은 없습니다.
클래스 컴포넌트도 state없이 존재할 수 있습니다. 입력을 받고 JSX로 넘겨 주는 클래스 컴포넌트의 예가 여기에 있습니다.
class Hello extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
Hello {props}
</div>
)
}
}
Props를 입력으로 받는 생성자(constructor) 메소드를 정의합니다. 그 생성자 안에, 부모 클래스로부터 상속 받은 무엇이건 전달하는 super()를 호출합니다. 여러분이 지나칠 수 있는 몇 가지 사항이 있습니다.
우선, 컴포넌트를 정의하는 동안 생성자는 선택입니다. 위의 경우에 컴포넌트에는 state가 없고 생성자는 유용한 동작을 하는 것처럼 보이지 않습니다. render() 안에 있는 this.props는 생성자가 정의 되거나 그렇지 않거나 상관없이 동작할 것입니다. 그런데 공식 문서에 적혀 있는 것이 있습니다.
Note
클래스 component는 props와 함께 기본 생성자를 호출해야 합니다.
하나의 모범 사례로서 모든 클래스 컴포넌트에 관해 생성자를 사용할 것을 권장드립니다.
두 번째로 여러분이 생성자를 사용한다면, super()를 호출해야 합니다. 이는 선택 사항이 아니기 때문에 호출하지 않으면 “Missing super() call in constructor”라는 구문 에러가 날 것입니다.
마지막으로 super() 대 super(props)의 사용에 관한 것입니다. 생성자 안에서 this.props를 호출하려고 할 때 super(props)를 사용해야 합니다. 그런 경우가 아니라면 super()만 사용해도 충분합니다. 2
Stateful 컴포넌트 대 Stateless 컴포넌트¶
컴포넌트를 분류하는 또다른 일반적인 방식입니다. 분류의 기준은 간단합니다. state가 있는 컴포넌트와 state가 없는 컴포넌트이지요.
Stateful 컴포넌트¶
Stateful 컴포넌트는 늘 클래스 컴포넌트입니다. 앞서 얘기했듯이 stateful 컴포넌트에는 생성자에서 초기화되는 state가 있습니다.
// Here is an excerpt from the counter example
constructor(props) {
super(props);
this.state = { count: 0 };
}
여기서 state
객체를 만들고, count를 0으로 함으로써 state
를 초기화했습니다. 클래스 영역에서 보다 쉽게 호출하도록 제안된 대체 구문(syntax)가 있습니다. ECMAScript 명세에 들어가 있지는 않지만, Babel transpiler를 사용한다면 이 구문이 기발하게 작동할 것입니다.
class App extends Component {
/*
// 생성자는 더이상 필요하지 않습니다.
constructor() {
super();
this.state = {
count: 1
}
}
*/
state = { count: 1 };
handleCount(value) {
this.setState((prevState) => ({count: prevState.count+value}));
}
render() {
// 생략
}
}
이 새로운 구문을 이용해 생성자를 사용하지 않을 수 있습니다.
이제 render()를 포함한 클래스 메서드 안에 들어 있는 state에 접근 가능합니다. 현재 count 값을 보여주려고 render() 안에 클래스 메서드를 사용하려 한다면 다음과 같이 중괄호 안에 state를 위치시켜야 합니다.
render() {
return (
Current count: {this.state.count}
)
}
여기에서 this 키워드는 현재 컴포넌트의 인스턴스를 참조합니다.
state
를 초기화하는 것만으로 충분치 않겠죠. 상호작용하는 애플리케이션을 만들려면 state
를 업데이트 할 수 있어야 합니다. 이것만으로 될 거라고 생각한다면, 아니예요. 되지 않아요.
//Wrong way
handleCount(value) {
this.state.count = this.state.count +value;
}
React 컴포넌트에는 state를 업데이트하기 위해 setState
라는 메서드가 있습니다. setState
는 count의 새로운 state를 포함한 새로운 객체를 받습니다.
// This works
handleCount(value) {
this.setState({count: this.state.count+ value});
}
setState()는 객체를 하나의 입력으로서 받아들이고, 우리는 count의 이전 값에 1만큼 증가시킵니다. 예상했던 대로 동작합니다. 그런데 한 가지 문제가 있습니다. state의 이전 값을 읽어 새 값을 작성하는 setState 호출이 여러 번 있을 때는 아마도 경합 조건(race condition)으로 끝나버릴 것입니다. 무슨 의미인가 하면, 최종 결과에서 예상했던 값이 안 나올 거라는 뜻입니다.
여기 명확히 이해할 수 있는 예제가 있습니다. 위의 Codesandbox 코드 조각에서 적용해 보세요.
// What is the expected output? Try it in the code sandbox.
handleCount(value) {
this.setState({count: this.state.count+100});
this.setState({count: this.state.count+value});
this.setState({count: this.state.count-100});
}
100씩 셈이 더해지는 setState가 필요하고, 그 후에는 1씩 업데이트되며, 그러고 나서 이전에 더해진 100을 뺍니다. setState가 실제 순서대로 state 전환을 실행한다면, 예상된 동작을 볼 것입니다. 하지만 setState는 비동기식이라서 다수의 setState 호출은 더 나은 UI 경험과 실행을 위해 한꺼번에 배치될 것입니다. 따라서 위의 코드는 우리의 예상과 다른 동작으로 동작하게 됩니다.
결과적으로 객체를 직접 전달하는 것 대신에 특정한 업데이트 함수로 전달하게 됩니다.
(prevState, props) => stateChange
prevState는 이전 state를 레퍼런스하며 최신 상태로 값을 유지해줍니다. props는 컴포넌트의 props이며, 여기에서 state를 업데이트하는 데 props가 필요하진 않습니다. 고로 신경쓰지 않아도 됩니다. 그러니 우리는 이를 state 업데이트 용으로 사용해 경합 조건을 피할 수 있습니다.
// The right way
handleCount(value) {
this.setState((prevState) => {
count: prevState.count +1
});
}
setState()는 컴포넌트를 다시 렌더링하며, stateful component가 동작하게 됩니다.
Stateless 컴포넌트¶
stateless 컴포넌트를 만드는 데 함수형이나 클래스를 사용하면 됩니다. 그러나 컴포넌트에서 라이프사이클을 적용해야만 stateless 함수형 컴포넌트의 가치를 갖게 됩니다. 여기에서 stateless 함수형 컴포넌트를 사용하고자 결정한다면 이득이 많이 있습니다. 작성하고 이해하며 테스트하기가 용이합니다. 그리고 this 키워드를 모두 적지 않아도 됩니다. 그렇지만 React v16이 나온 현재로서는 클래스 컴포넌트를 사용하는 것보다 stateless 함수형 컴포넌트를 사용하는 게 성능상 이득이 있지는 않습니다.
안좋은 점은 여러분이 라이프사이클을 활용하지 못한다는 것입니다. 라이프사이클 메서드인 ShouldComponentUpdate()는 종종 성능을 최적화하고 렌더링 시킬 것을 수동으로 제어하는 데 쓰입니다. 아직까지는 함수형 컴포넌트에서 그 메서드를 적용하지 못합니다. 참조자(refs)도 지원되지 않습니다. 2
컨테이너 컴포넌트 대 프레젠테이셔널 컴포넌트¶
컴포넌트를 작성하면서 매우 유용한 패턴입니다. 이러한 접근방식은 행동(behavior) 로직을 표현(presentational) 로직과 분리하는데 이롭습니다.
프레젠테이셔널 컴포넌트¶
프레젠테이셔널 컴포넌트는 뷰(view) 혹은 어떻게 보이게 할지와 관련이 있습니다. 이러한 컴포넌트는 그에 상응하는 컨테이너 쌍으로부터 props를 받아 렌더링합니다. UI를 서술하는 것과 연관된 코드는 모두 여기로 가야합니다.
프레젠테이셔널 컴포넌트는 재사용 가능하며 동적인 레이어로부터 분리되어 있어야 합니다. 프레젠테이셔널 컴포넌트는 오로지 props를 통해 데이터와 콜백을 받습니다. 그리고 버튼이 눌리는 식의 이벤트 에러가 발생했을 때, 이벤트 핸들링 메서드를 부르기 위해 props를 통해 컨테이너 컴포넌트로 콜백을 수행합니다.
state를 요구하지 않는 한 프레젠테이셔널 컴포넌트를 작성하는 데 함수형 컴포넌트를 우선적으로 사용해야 합니다. 만일 프레젠테이셔널 컴포넌트에 state가 필요하다면 실 데이터가 아닌 UI state와 연관시켜야 합니다. 프레젠테이셔널 컴포넌트는 Redux store와 상호작용을 하거나 API 호출을 하지 않습니다.
컨테이너 컴포넌트¶
컨테이너 컴포넌트는 동작과 관련한 부분을 다룹니다. 컨테이너 컴포넌트는 프레젠테이셔널 컴포넌트에게 props를 이용해 어떻게 렌더링되어야 하는지 얘기해 줍니다. 제한된 DOM 마크업과 스타일을 포함하지 않습니다. 여러분이 Redux를 이용한다면 컨테이너 컴포넌트에는 저장소로 동작을 전달하는 코드를 넣을 것입니다. 그렇지 않으면 API 호출을 넣고 컴포넌트의 state로 결과 값을 저장하는 위치가 되겠지요.
일반적인 구조에서는 데이터를 props로서 프레젠테이셔널 컴포넌트로 하향 전달하는 컨테이너 컴포넌트가 상위에 있습니다. 규모가 작은 프로젝트에 적합한 구조입니다. 프로젝트의 규모가 점점 커지면 props를 받아서 자식 컴포넌트로 전달하는 중간에 위치한 컴포넌트들이 많아지게 되어 지저분하고 유지 관리가 힘들어집니다. 그런 상황이 벌어질 때는 리프(leaf) 컴포넌트에 고유한 컨테이터 컴포넌트를 만드는 게 더 좋습니다. 그러면 중간에 있는 컴포넌트들에 대한 부담감이 덜어질 것입니다.
정리¶
stateless 함수형 컴포넌트는 보다 명쾌하며, 프레젠테이셔널 컴포넌트를 제작하는 데 일반적으로 좋은 선택이라고 할 수 있습니다. 함수형일 뿐이므로 작성하고 이해하는 데 어려움을 겪지 않을 것입니다. 더구나 테스트 하기에도 아주 쉽습니다.
stateless 함수형 컴포넌트는 ShouldComponentUpdate() 메서드가 없다는 이유로 최적화와 성능에서 우위라는 것은 아니라는 점을 명심해야합니다. React 이후 버전에서 바뀔 지도 모릅니다. 거기에서는 함수형 컴포넌트가 더 나은 성능을 위해 최적화될 지도 모릅니다. 성능이 중요하지 않다면 뷰/프레젠테이션 용으로 함수형 컴포넌트를, 컨테이너 용으로 stateful 클래스 컴포넌트를 고려하세요.
메소드들¶
componetDidMount¶
React 컴포넌트에는 특정 시점에 수행할 로직을 실행할 수 있는 라이프사이클 메서드가 있습니다. componentDidMount() 메서드는 컴포넌트가 접속되고 state가 수정될 때 작동됩니다. 데이터를 불러올 완벽한 지점이지요. 3
fetchQuotes = () => {
this.setState({...this.state, isFetching: true})
fetch(QUOTE_SERVICE_URL)
.then(response => response.json())
.then(result => this.setState({quotes: result,
isFetching: false}))
.catch(e => console.log(e));
}
}
componentDidMount() {
this.fetchQuotes()
}
참고 사이트
- 1(1,2)
PureComponent와 Component 비교: https://www.vobour.com/%EB%A6%AC%EC%95%A1%ED%8A%B8-react-%EC%9D%B4%ED%95%B4-%EA%B8%B0%EC%B4%88-component-vs-purecomp
- 2(1,2,3,4)
컴포넌트란 무엇인가? https://code.tutsplus.com/ko/tutorials/stateful-vs-stateless-functional-components-in-react–cms-29541
- 3
데이터 불러오기 https://code.tutsplus.com/ko/tutorials/fetching-data-in-your-react-application–cms-30670