리액트에서 컴포넌트는 하나의 기능 단위로 구분된 코드 모음, 즉 데이터 + 화면이 같이 존재한다. 따라서 컴포넌트 내부 로직은 데이터가 변경되면 화면이 바뀌는, 데이터 중심으로 동작한다. 컴포넌트에서 외부로부터 받아오는 데이터는 props, 컴포넌트 안에서 바뀌는 데이터는 state로 구분할 수 있다.
Props란 무엇인가?
props란 컴포넌트의 프로퍼티(properties)를 의미한다.
자바스크립트에서 함수를 생각하면 조금 이해가 쉬운데, 함수를 전달 인자와 함께 호출하면, 호출된 함수 내부에서 그 전달 인자를 매개변수로 받아와 접근할 수 있듯이, 상위 컴포넌트에서 호출된 하위 컴포넌트에 속성 값을 전달하면, 그 속성 값 props라는 객체를 하위 컴포넌트에서 접근할 수 있다.
실제 함수를 호출할 때 처럼 괄호 안에 데이터를 전달하는 것은 아니고, HTML에서 어트리뷰트를 설정할 때처럼 작성하면 된다.
// 상위 컴포넌트에서 Child 하위 컴포넌트에 속성 전달.
const Parent = () => {
const data = [
{
name: 'hyejung',
age: 26,
},
{
name: 'sandeul',
age: 3,
},
];
return <Child username = {data[0].name} age = {data[0].age} />
}
// 하위 컴포넌트 Child 가 속성들을 props로 받아온다.
const Child = props => {
console.log(props); // age: 26, name: "hyejung"
return (
<div>
name : {props.name} <br />
age : {props.age}
</div>
);
};
이렇게 상위 컴포넌트로부터 전달되는 데이터(객체)를 props라고 하며, 함수와 마찬가지로 props 이외에 다른 이름으로 가져올 수도 있다. 상부에서 하부로 전달되는 단방향 흐름을 가지고 있다.
전달받은 props 객체는 변경할 수 없는 읽기 전용임을 기억하자!
우리가 코드를 짤 때는 의도치 않은 동작, 부수 효과를 최대한 지양해야 한다. 그 이유는 결과를 예측할 수 없는 코드는 오류 발생 확률을 높이기 때문에 안정성과 생산성이 저하된다. 이를 예방하는 방법 중 하나가 순수 함수를 사용하는 것인데, 순수 함수란 동일한 입력값에 대해 동일한 결과를 반환하는 함수를 말한다. (변경을 하지 않기 때문에 결과를 예측하기 수월할 것이다...!) 따라서 리액트의 컴포넌트에서도 전달받은 props를 변경하지 않는 규칙에 따라야한다.
Props를 왜 사용할까?
굳이 컴포넌트 외부에서 props를 전달받아와 사용해야 하는 이유는 무엇일까?
이것도 우리가 함수를 사용하는 이유와 비교하면서 생각해보면 이해가 쉽다. 함수를 사용하는 중요한 이유 중 하나는 "재사용"이다. 만들어진 함수에 각각 다른 값을 전달하여 다른 결과를 도출시킴으로써 함수를 재사용하고, 같은 코드를 작성하는 번거로움을 줄일 수 있다.
컴포넌트도 이와 동일하다. 유튜브의 알고리즘 추천 화면을 생각해보자.
각각의 동영상 썸네일은 모두 같은 포맷을 가졌다. 이미지, 영상 이름, 채널 이름 등... 이 수많은 썸네일 컴포넌트에서 다른 점은 데이터뿐인데, 이 데이터가 외부에서 전달되지 않는다면 컴포넌트 내부에서 이 데이터를 정의해야 한다. 데이터만 다른 수십 개의 같은 코드들이 생겨날 것이다 ㅜㅜ.
우리가 이런 코드의 중복을 방지하기 위해서 함수를 사용하는 것처럼, 컴포넌트도 동일하게 생각하면 될 것 같다. 서로 다른 데이터를 전달해 컴포넌트를 동적으로 생성하는 것이다. 그리고 이 때는 배열의 map 메서드를 활용해서 각각 다른 props를 전달받는 컴포넌트를 생성할 수 있다. (위 예제처럼 인덱스로 접근할 수도 있겠지만, 그렇다면 별로 의미가 없을 것이다. )
const Parent = () => {
const data = [
{
name: 'hyejung',
age: 26,
},
{
name: 'sandeul',
age: 3,
},
];
// map으로 데이터를 각 props 를 전달받는 컴포넌트로 맵핑하여 리턴
return data.map((user, idx) => {
return <Child key={idx} name={user.name} age={user.age} />;
});
};
State란 무엇인가?
외부에서 전달되는 변경되지 않는 값이 props 라면, 컴포넌트 내부에서 변경되는 데이터는 state 라고 할 수 있다.
리액트가 데이터 중심으로 변경, 즉 데이터가 변경되면 화면이 변경되기 때문에 state는 이 변경 상태를 감지해서 화면에 표시해주는 역할을 한다. 리액트를 활용해 프론트엔드 개발을 편하게 할 수 있는 이유 중 하나가, 개발자가 데이터만 바꿔주면 리액트가 변경을 감지해 화면에 렌더링 하는 역할을 해주기 때문이다.
이렇게 변경을 감지해주기 위해서는 화면에서 변하는 데이터들을 state에 등록하고, 특별한 state 업데이트 함수를 사용해서 변경해주어야 한다. 이때 사용하는것이 useState라는 훅 Hook 이다. 훅은 리액트에서 제공하는 함수라고 생각하면 된다.
참고로 훅은 리액트 16.8 버전에서 새로 추가되었다. 기존에는 클래스 컴포넌트에서만 사용할 수 있었던 기능들을 함수 컴포넌트에서도 사용할 수 있게 해 준다. 많은 개발자들이 훅을 사용하지만 개발을 하다 보면 클래스로 짜인 예전 코드들을 볼 수도 있기 때문에, 클래스 컴포넌트도 알아두면 좋다.
위 예제에서 state가 될 부분은 두 부분이 있다.
1. 사용자의 입력을 감지하는 input 부분
2. [입력] 버튼을 눌렀을 때, 입력 내용을 표시하는 결과 부분.
useState를 사용하기 위해서 먼저 리액트에서 이를 불러와준다.
import React, { useState } from 'react';
useState를 불러오면 이를 이용해 각각의 state와 그 state를 업데이트하는 별도의 함수를 정의해줄 수 있다. 이는 구조분해할당 문법을 사용하는데, useState의 첫 번째 요소는 state이고, 두 번째 요소는 업데이트 함수라고 생각하면 된다. 이를 컴포넌트 내부에서 정의해준다.
// 컴포넌트 InputEg (input 예제...)
const InputEg = () => {
// 변경되는 두 state 를 등록한다.
const [value, setValue] = useState('초기값');
const [result, setResult] = useState('초기값');
}
useState()의 괄호 안에 전달하는 값은 state의 초기값이다. 원래는 '' 아무것도 없는 것이 맞는데, 나는 예시를 위해 '초기값'이라는 문자열을 전달해보았다.
state를 생성했으면, 이제 각 state를 컴포넌트의 필요한 부분에 위치시켜준다.
const InputEg = () => {
const [value, setValue] = useState('초기값');
const [result, setResult] = useState('초기값');
return (
<div>
<form>
<input type="text" value={value}/>
<button>입력</button>
</form>
<div>입력한 내용 : {result}</div>
</div>
);
};
먼저 input의 value 속성을 state인 value로 정의하고, result state가 화면 하단에 나올 수 있게 위치시켰다. 이렇게만 작성하면 초기값인 '초기값' 문자열이 화면에 뜰 것이다. 또한 아직 변경을 감지하는 이벤트 핸들러가 없기 때문에, onChange 핸들러를 만들라는 에러 메시지도 함께 뜰 것이다.
따라서 아래와 같이 이벤트 핸들러를 만들어 각 이벤트에 맞는 함수를 전달해보자. 최종적으로 작성되는 코드는 아래와 같다.
const InputEg = () => {
// 변경되는 두 state 를 등록한다.
const [value, setValue] = useState('');
const [result, setResult] = useState('');
const onChangeInput = e => {
// input 변화가 일어나면 value를 input의 value로 변경한다.
setValue(e.target.value);
};
const onSubmitForm = e => {
e.preventDefault();
setResult(value);
};
return (
<div>
<form onSubmit={onSubmitForm}>
<input type="text" value={value} onChange={onChangeInput} />
<button>입력</button>
</form>
<div>입력한 내용 : {result}</div>
</div>
);
};
const [state, setState] = useState(' ') , 꼭 setState를 사용해야함?
위 코드에서 변경되는 값으로 state를 업데이트하기 위해서 setState 함수를 사용했다.
state를 변경하는데 왜 state 값을 재할당하지 않고 setState 함수를 통해서 업데이트해야만 할까? 앞서 설명한 바와 같이 리액트 useState 훅은 변경을 감지해서 새로 렌더링을 해주는 기능을 한다. setState 함수를 통해 변경하지 않으면 변경을 감지할 수 없기 때문에 반드시 함수를 사용해야 한다!
추가적으로 setState 함수는 비동기적으로 동작한다. 다시 말해서 setState를 호출한 직후에 바로 새로운 값이 state에 반영된다고 생각하면 안 된다. **
또 변경되는 모든 부분이 state는 아님을 기억하자. state가 많아질수록 애플리케이션이 복잡해지기 때문에 state가 되어야 하는 부분을 구별하는 것도 중요하다. 아래 세 가지 기준을 가지고 판단해보자!
1. 데이터가 props로 전달되는가?
2. 시간이 지나도 변하지 않는 고정적인 데이터인가?
3. 컴포넌트 내부의 다른 state나 props를 통해서 계산되어서 도출될 수 있는 값인가?
이 조건들에 하나라도 부합한다면 state가 아니다.
SUMMARY
- Props는 외부를 통해 전달받는 읽기 전용 데이터, state는 컴포넌트 내부에서 변경되는 값이다.
- state를 사용할 때는 (함수 컴포넌트에서) useState 훅을 사용하며, state를 갱신할 때는 훅에서 제공하는 별도의 함수를 사용한다.
- 리액트는 페이지가 아닌 컴포넌트 중심, 데이터 중심으로 동작한다. 데이터의 흐름과 위치, 컴포넌트 간의 연결을 고려하여 개발하도록 하자!
참고자료
'Stacks > React.js' 카테고리의 다른 글
[React] Hooks의 동작 원리 파악하기 (useEffect 를 중심으로) (0) | 2023.04.12 |
---|---|
React Hook Form (0) | 2022.09.27 |
[React] UseEffect로 fetch API 요청하기 (0) | 2022.08.08 |
[React] SPA 는 무엇인가? / React Router 활용하기 (0) | 2022.08.01 |
[React] 리액트는 무엇이고, 왜 리액트를 사용해야 하는가? (0) | 2022.07.20 |