리액트를 처음 접하면 당황스러운 일이, 바로 state 와 props 이다. 리액트는 선언적 특징을 가지고 있기 때문에 직접 DOM 요소를 조작하지 않고 state 의 변화를 통해서 UI 를 리-렌더링한다. 그렇기 때문에 이전에 명령을 통해서 조작하던 방식과는 사고의 흐름이 다르다고 느껴졌다. 하지만, state와 props 은 알면 알수록 재밌고 흥미로운 요소이다. 이에 대해서 정리해보려고 한다.
state 는 컴포넌트가 가지고 있는 상태값을 의미한다. 더 정확히는 UI에 대한 정보를 담고 있는 객체라고 볼 수 있다. state 는 불변값이 아니며 언제든지 사용하고 변경될 수 있다.
class Main extends Component {
state = {
feeds: [],
};
render() {
return (
<main className="main>
<Nav />
<section className="main__feeds">
{this.state.feeds.map(feed => (
<Feed key={feed.id} feed={feed} comments={feed.comments} />
))}
</section>
<Aside />
</main>
);
}
}
export default Main;
위의 예시는 메인 페이지의 컴포넌트에서 state 를 가지고 있는 모습이다. 메인 페이지는 state에 feeds 라는 배열을 가지고 있다. 이 배열 안에는 또 각 피드의 정보를 담고 있는 객체들이 들어갈 예정이다. 지금은 생성하면서 초기화만 진행했기 때문에 feeds 에 빈 배열이 들어와 있다.
// mock data 받아오기
componentDidMount = () => {
fetch('http://localhost:3000/data/CommentDataCJ.json', {
method: 'GET',
})
.then(result => result.json())
.then(feeds => {
this.setState({
feeds,
});
});
};
위의 코드는 componentDidMount() 를 통해서 mock data 를 받아오고 이를 이용해서 setState 를 진행했다. 여기서 setState 를 사용해서 업데이트를 한 점을 보자. state 는 UI에 대한 정보를 가지고 있고 이를 렌더링 하기 위해서 존재한다고 볼 수 있다. 만약, state 의 값을 직접 조작하는 경우에는 state의 변화는 있겠지만 render() 가 호출되지 않아서 리-렌더링이 생겨나지 않는다. 그렇기 때문에 setState() 를 호출하고 이 때, render() 가 호출되기 때문에 정상적으로 UI를 그릴 수 있게 된다.
state 를 사용하면서 겪었던 어려움은 setState() 가 비동기적으로 작동하는 점이었다. state 가 잘 업데이트 되는지 확인하기 위해서 console.log(this.state) 를 사용하는 과정에서 한 박자 느린 반응을 보게 된다거나 전혀 예상하지 못한 흐름으로 동작한다거나 하는 오류들을 겪었다. 특히, ID 와 PASSWORD 를 입력하는 상황에서 한 쪽이 사라지는 경우를 겪기도 했다.
this.setState({
counter: this.state.counter + this.props.increment,
});
만약 위와 같은 코드를 사용하게 된다면 카운터가 제대로 업데이트 되지 않는 상황을 겪을 수도 있다. 되도록 this.state 와 this.props 을 사용할 때 비동기적으로 업데이트 될 수 있음을 명심하고 사용하는 것이 좋다.
state 가 컴포넌트 본인이 가지고 있는 상태값이라면 props 은 부모로부터 받은 속성 객체라고 볼 수 있다. 여기에는 배열, 객체 등이 들어갈 수 있고 메소드도 가능하다. state 는 본인이 가지고 있는 값이기 때문에 얼마든지 변경이 가능하지만 props 는 읽기 전용이기 때문에 자식 컴포넌트에서 수정할 수가 없다.
class Comment extends Component {
handleDelete = e => {
this.props.onDelete(this.props.reply);
};
handleLike = e => {
this.props.onLike(this.props.reply);
};
render() {
const { userName, comment, isUser, isLike } = this.props.reply;
return (
<li className="comment-cheoljin">
<span>{userName}</span>
<span>{comment}</span>
<button
type="button"
className="comment__heart"
onClick={this.handleLike}
>
<i className={isLike ? 'fas fa-heart' : 'far fa-heart'} />
</button>
<button
type="button"
className="comment__delete"
onClick={this.handleDelete}
>
<i
className={isUser ? 'far fa-trash-alt' : 'far fa-trash-alt none'}
/>
</button>
</li>
);
}
}
export default Comment;
위의 예시를 보자. props 를 구조분해할당을 통해서 userName, comment, isUser, isLike 로 나눴다. 그 후에 각 필요한 부분에 {} 을 통해 사용했다. 또한, handleDelete 등의 함수를 보면 부모 컴포넌트로부터 전달 받은 props 객체의 메소드인 onDelete를 호출하는 함수 임을 알 수 있다.
이처럼 props 는 부모 컴포넌트와 자식 컴포넌트를 연결해주는 다리의 역할을 한다.
더 설명해보자면 자식 컴포넌트인 Comment 에 있는 <button> 에서 onClick 이벤트가 발생하게 되면 handleDelete 함수가 호출되고 handleDelete 함수는 props로 전달받은 onDelete 함수를 호출한다. 이때 인자로 props 인 reply 를 전달할 수 있다.
위와 같은 방식으로 자식 요소에서 발생한 이벤트를 부모 컴포넌트로 전달해서 state 를 업데이트 할 수 있다.
오늘은 state 와 props 에 대해서 간단하게 정리했다. 리액트를 처음 접하게 되는 경우에는 어떻게 이용해야 좋을지 막막한 기분을 느끼는 경우가 많은데 사용하는 방법에 대해서 잘 이해한다면 바닐라 자바스크립트만을 사용하는 경우보다 훨씬 편함을 알 수 있다.
앞으로 평생 함께해야 할 state와 props. 막히는 부분이 있는 경우에는 꼭 리액트 공식 문서에서 답을 찾으며 계속 공부해 볼 예정이다.