상태 객체가 없으면 React 컴포넌트는 그저 보기 좋은 정적 템플릿일 뿐이다.
속성을 변경하여 뷰를 갱신할 수 있다. 하지만 속성은 현재 컴포넌트 내부에서는 수정할 수 없다. 이유는 속성은 해당 컴포넌트 생성 시에 전달받는 값이기 때문이다.
즉, 속성은 현재 컴포넌트에서 변경할 수 없기에 부모 컴포넌트에서 컴포넌트를 새로 생성해서 새로운 값을 전달하는 방법 외에는 컴포넌트의 속성을 변경할 수 없다.
그러면 받아온 정보를 어디에 저장해야 뷰를 출력할 수 있다.
하지만 속성을 변경할 수 없다면?
한 가지 방법이 있긴하다. 서버에서 응답을 받을 때마다 새로운 속성으로 엘리먼트를 렌더링하는 것이다. 하지만 이 경우에는 관련된 로직을 컴포넌트 외부에 작성해야해서 독립적인 컴포넌트가 되지 못 한다.
즉, createElement() or JSX < NAME/>을 사용하여 컴포넌트를 다시 생성하지 않고, 사용자 조작으로 발생한 이벤트를 처리하여 뷰를 갱신해야 한다.
'상태 객체'를 이용하면 이 문제를 해결할 수 있다.
'상태'는 React 컴포넌트에 데이터를 저장하고, 데이터의 변경에 따라 자동으로 뷰를 갱신하도록 하는 핵심개념이다.
React 컴포넌트에서 '상태'란?
React의 '상태'는 컴포넌트의 변경 가능한 데이터 저장소라는 말이다. 독립적이고 기능 중심인 UI와 논리의 블록인 것이다. '변경 가능하다는 것'은 상태 값을 변경할 수 있다는 것이다. 뷰(render())에서 상태를 이용하고, 이 값을 나중에 변경하면 뷰의 표현에 영향을 줄 수 있다.
컴포넌트 안에 속성과 상태가 있고 이 결과가 UI 표현(뷰)라고 생각하면 쉽다. React 측에서도 "컴포넌트는 상태 머신이다" 라고 까지 이야기한다. 속성 상태는 모두 뷰를 갱신하기 위해 사용할 수 있지만, 서로 목적은 다르다.
상태 객체에 접근할 때는 이름을 사용한다. 이름은 this.state 객체의 속성이다.
예를 들면, this.state.autocompleMatches, this.state.inputFieldValue가 있다.
※ 위 문단에서 말한 '객체 속성'이란, 객체 키 or 객체 프로퍼티를 의미한다. 컴포넌트의 속성이 아니다!
※ 일반적으로 '상태 객체'라고 하면 컴포넌트의 this.state 객체에 속한 모든 키-값 쌍을 말한다. '상태'라고 하면 문맥에 따라 this.state 객체를 의미할 수도 있지만 this.state.inputFieldValue처럼 개별 상태 값을 의미할 수도 있다.
상태 데이터는 뷰의 렌더링이 갱신될 때 동적 정보를 출력하기 위해 사용된다.
예를 들어 네이버나 다음 등의 검색에서 자동완성 기능을 생각해보면,
사용자 입력을 받아 서버에 XHR 요청을 보내고, 돌아온 응답에 따라 상태를 변경한다. React는 뷰에 출력된 상태가 변경되면, 변경된 상태를 뷰에 반영해서 뷰를 최신 상태로 유지한다.
'관련된 부분만' 갱신된다는 것이 포인트이다.
DOM에 있는 그외의 다른 부분은 그대로 유지된다. 이는 가상 DOM 덕분이다. 즉, React가 '보정과정'을 통해 변경할 부분을 결정하는 방식인 것이다. 이 덕에 '선언적'으로 작성할 수도 있는 것이다.
React는 상태 객체를 이용해 새로운 UI를 생성해내는 것이다.
이전 글에 설명한 '속성'은 부모 컴포넌트에서 새로운 값을 전달하면, 뷰를 갱신하여 현재 다루고 있는 자식 컴포넌트의 새로운 인스턴스를 생성한다. 해당 자식 컴포넌트의 컨텍스트 내에서 속성을 변경하려고 해도 소용없다.
상태 객체에 접근하기
상태 객체는 컴포넌트의 멤버 변수로 this를 통해 접근할 수 있다. 예를 들면, this.state.name 과 같은 방식이다. 또한, render()에서 this.state를 렌더링할 수 있다. 또한, JSX에서 중괄호({})를 이용해 변수에 접근하고 출력했던 것처럼 {this.state.inputFieldValue}처럼 작성할 수도 있다. 즉, 문법이 속성에 접근하는 방법인 this.props.name과 비슷하다.
속성과 달리 상태 객체는 부모 컴포넌트에서 설정하는 것이 아니다. 상태를 설정하기 위해 render() 메서드 안에 setState를 실행할 수도 없다. 그러면 setState -> render -> setState ~~~로 계속 반복되어 React에서 오류가 발생하기 때문이다.
초기 상태 설정
render()에서 상태 데이터를 사용하려면 먼저 상태를 초기화해야 한다.
초기 상태를 설정하려면, React.Component를 사용하는 ES6 클래스의 생성자에서 this.state를 선언해야 한다. 그리고 반드시 super()에 속성을 전달해서 실행해야 한다. 그렇지 않으면 부모 클래스인 React.Component의 기능을 정상적으로 사용할 수 없다.
아래와 같은 구조를 가진다고 보면 된다.
class Velog extends React.Component{
constructor(props){
super(props)
this.state = {~~~}
}
render(){~~~}
}
초기 상태를 설정하면서 다른 로직도 추가할 수도 있다.
예를 들면 아래와 같다.
class Clock extends React.Component{
constructor(props){
super(props)
this.state = {currentTime: (new Date()).toLocaleString('en')}
}
}
this.state의 값은 반드시 객체여야 한다. constructor()가 낯설텐데, 한번 개인적으로 알아보는 걸 추천한다. 일단은 객체지향 프로그래밍 언어에서 클래스의 인스턴스가 생성될 때 constructor()가 호출된다고 알고 있으면 된다. 그리고 생성자 메서드의 이름은 반드시 constructor로 한다. ES6의 규칙이라고 생각하면 편하다. 그냥 암기!
또한, 부모 클래스가 있는 클래스에서 constructor() 메서드를 생성하면 그 안에서 거의 항상 super()를 호출한다. 안 그러면 부모 클래스의 생성자가 실행되지 않는다. 만약 상속으로 클래스를 구현하는 경우에 constructor() 메서드를 따로 작성하지 않으면, super()를 호출한 것으로 가정한다.
위에 예시를 든 것에서 currentTime은 임의로 지은 이름이다. 여러분이 잊지 않고 필요할 때 사용할 수 있다면, 상태의 이름은 마음대로 정할 수 있다.
상태 객체는 배열이나 다른 객체를 중첩해서 가질 수도 있다.
예를 들면 이러하다.
class Hwibin extends React.Component{
constructor(props){
super(props)
this.state = {
myname = 'hwibin',
sns: [
'facebook',
'instagram',
'velog'
]
}
}
render(){~~~}
}
constructor() 메서드는 앞의 컴포넌트 클래스에서 React 엘리먼트가 생성되는 시점에 한번만 호출된다. 이런식으로 constructor() 메서드 내에서 한 번만 this.stsate로 직접 상태를 선언할 수 있다.
하지만 여기까지만 보면, 값을 입력해서 보여줄 뿐, 시간이 지나버린다. 즉, 1초만이 지나버린다. 따라서 갱신을 해줄 필요도 있다.
상태 갱신하기
클래스 메서드인 this.setState(data, callback)을 사용하면 상태를 변경할 수 있다. 이 메서드가 실행되면, React는 전달하는 data를 현재 상태에 병합하고 render()를 호출한다. 이후에 React가 callback 함수를 실행하게 된다.
setState()에 콜백함수를 사용할 수 있다는 것이 의외로 중요하다. setState()가 '비동기'로 작동하기 때문이다. 새로운 상태에 의존하는 경우, 콜백함수를 사용해야 새로운 상태가 적용된 후에 필요한 작업을 수행할 수 있게 된다.
setState()가 완료되길 기다리지 않고 새로운 상태에 의존하는 작업을 수행하는 것은 비동기 작업을 동기처럼 다루는 것이다.
이 경우에는 갱신될 새로운 상태 값에 의존하는 코드를 작성하면 버그가 생길 수 있다. 상태 객체가 이전 값을 가진 이전의 상태 객체로 남아있기 때문이다.
일반적으로 setState()는 이벤트 핸들러나 데이터 수신 또는 갱신을 처리하는 콜백함수에 호출된다.
※ this.state.name = 'new name', 이런 식의 상태변경은 아무 효과가 없다. 대부분의 경우에 setState()를 거치지 않고 직접 상태 객체를 변경하는 것은 안티패턴이므로 위험하고 피해야 한다.
setState()로 전달하는 상태는 상태 객체의 일부분만 갱신한다는 것을 기억하자! 매번 상태 객체를 완전히 바꾸지 않는다는 것이다.
만약 상태를 모두 갱신하고 싶다면 setState()에 상태에 대한 새로운 값을 명시적으로 전달해야 한다.
아래와 같이 말이다.
constructor(props){
super(props)
this.state={
myName: 'hwibin',
myInsta: '@brian_binn'
}
}
updateValues(){
this.setState({
myName: 'secret',
myInsta: '@secret'
})
}
setState()가 render()를 실행시킨다는 것도 알고있고 기억해야 한다. 코드가 외부 데이터에 의존하는 매우 특이한 경우에는 다시 렌더링하기 위해 this.forceUpdate()를 호출할 수도 있다. 하지만 이 방법은 상태가 아닌 외부 데이터에 의존하여 컴포넌트를 불안정하게 만들고, 외부 요소와 강하게 결합되어 좋지 않아 피해야 한다.
말했듯, this.state를 통해 상태 객체에 접근할 수 있다. 그리고 JSX에서 값을 출력할 때 중괄호({})를 사용한다. 그래서 뷰에서 상태를 노출하려면 render()의 return문에서 {this.state.Name}을 사용한다.
가장 신기하고 중요한 점은, 필요한 최소한의 DOM 요소에만 정확하게 영향을 준다는 점이다.
상태 객체와 속성
'상태 객체'와 '속성'은 모두 클래스의 멤버이고, 각각 this.state와 this.props를 뜻한다. 이것이 거의 유일한 공통점이라고 할 수도 있다. 중요한 차이점 중 하나는, '상태 객체'는 변경 가능하지만, '속성'은 변경이 불가능하다는 것이다.
또 다른 차이점은, '속성'은 부모 컴포넌트에서 전달하지만, '상태'는 부모 컴포넌트가 아닌, 해당 컴포넌트 자체에서 정의한다는 것이다. 이는 또 속성 값을 변경하는 것은 오직 부모 컴포넌트에서만 가능하며 자체적으로는 변경할 수 없다는 것을 뜻한다. 따라서 속성은 뷰 생성 시에 정해지고, 정적인 상태로 유지된다. 하지만 상태는 해당 컴포넌트에서 설정하고 갱신된다.
속성과 상태는 서로 다른 목적으로 사용되지만, 둘 다 컴포넌트 클래스에서 접근이 가능하고, 다른 표현(뷰)으로 여러 컴포넌트를 구성할 수 있게 한다.
하지만 모든 컴포넌트가 상태를 가져야 하는 것은 아니다.
상태비저장 컴포넌트
'상태비저장 컴포넌트'는 상태 객체가 없고, 컴포넌트 메서드 또는 다른 React의 라이프사이클 이벤트 또는 메서드를 갖지 않는다.
상태비저장 컴포넌트의 목적은 오직 뷰를 렌더링 하는 것이다. 속성을 전달받아 처리하는 일 밖에 못한다.
즉, 상태비저장 컴포넌트는 속성을 입력받아 UI 엘리먼트를 출력하는 간단한 함수이다.
이 상태비저장 컴포넌트는 예측가능하다는 이점이 있다. 출력을 결정하는 입력이 하나뿐이기 때문이다. 예측가능성은 곧 이해가 쉽고, 유지보수나 디버깅이 편하다는 것을 의미한다.
실제로 상태를 가지지 않는 것이 React의 가장 좋은 예라고 한다. 상태비저장 컴포넌트는 더 많이 사용할수록, 상태저장 컴포넌트는 더 적게 사용할수록 좋다.
아래와 같은 것이 상태비저장 컴포넌트라 볼 수 있다.
class HelloWorld extends React.Compnent{
render(){
return <h1>hello</h1>
}
}
상태비저장 컴포넌트는 상태를 가질 수 없다. 하지만 propTypes와 defaultProps를 프로퍼티로 가질 수 있고, 이 둘을 컴포넌트 객체에 추가할 수 있다.
또한, 상태비저장 컴포넌트(함수)에서는 엘리먼트 참조(refs)를 사용할 수 없다. refs를 사용하려면 상태비저장 컴포넌트를 일반적인 React 컴포넌트로 감싸야 한다.