리액트는 컴포넌트 기반의 계층 구조로 이루어져 있고 그 안에서 데이터를 다룰 수 있다고 했었다. 상태(state)와 속성(props)는 이 데이터의 일종이다. 상태란 해당 컴포넌트 내부에서 관리되는 데이터로 컴포넌트의 렌더링 결과나 동작에 영향을 주는 값이다. 주로 컴포넌트가 생성되면서 초기화되고 사용자의 상호작용이나 비동기적 이벤트에 의해 값이 변경될 수도 있다. 한편 속성은 부모 컴포넌트로부터 전달되는 읽기 전용 데이터이다. 따라서 전달된 값에 의존하여 동작하거나 표시할 데이터를 받기만 하고 해당 컴포넌트 내부에서 속성을 변경할 수는 없다.
속성은 부모 컴포넌트로부터 전달되는 읽기 전용 데이터이다. 그래서 보통 컴포넌트에 전달되는 인자 형태로 사용된다.
위의 예시에서 간단한 속성의 사용법을 살펴볼 수 있다. Child 컴포넌트는 계층 구조 상 Parent 컴포넌트의 하위 컴포넌트로 들어가있다. 이때 Child 컴포넌트가 어떤 인자를 전달받는 것을 확인할 수 있는데 이것은 Parent 컴포넌트에서 Child 컴포넌트를 호출하면서 속성으로 작성했던 데이터이다. 예시에서는 text 속성을 전달하고 있으며 실제로 Child 컴포넌트에서는 props.text로 접근해서 해당 데이터를 출력하고 있는 것을 볼 수 있다. 이처럼 리액트에서 속성은 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 데이터이다. 즉 속성 데이터의 흐름은 하향식으로 구성되어 있다는 소리이다. 이런 디자인 패턴을 단뱡향 데이터 흐름이라고 부르며 이 키워드는 리액트를 대표하는 특징 중에 하나이다. 어떤 상태 값을 필요로 하는 컴포넌트가 단 1개라면 그 컴포넌트에서 상태를 관리하면 된다. 하지만 만약 여러 컴포넌트가 해당 상태 값을 필요로 한다면 공통 부모 컴포넌트까지 올라가서 해당 컴포넌트에서 데이터를 관리하면서 속성의 형태로 값을 필요로 하는 컴포넌트까지 내려주어야 하는 것이 기본적인 리액트의 설계 방식인 것이다.
리액트에서 상태를 다루기 위해서는 주로 useState라는 함수를 사용한다. 이런 식으로 함수형 컴포넌트에서 상태 관리와 리액트의 기능을 사용할 수 있도록 도와주는 함수들을 훅(Hook)이라고 부른다. useState는 리액트에서 기본적으로 제공하는 훅이고 그 외에도 useEffect, useContext 등의 훅을 제공하며 일반적으로 앞에 use가 붙은 네이밍을 사용하고 있다. 리액트 컴포넌트 안에서 useState 함수를 호출하는 것은 일종의 변수를 선언하는 것과 같은데 일반적인 변수는 함수가 끝날 때 사라지게 되지만 상태는 함수가 끝나도 사라지지 않는다.
const [상태 저장 변수, 상태 갱신 함수] = useState(상태 초기 값);
useState 함수는 위와 같이 사용하게 된다. 상태 저장 변수에 상태 값이 저장되고 이제 이 상태 저장 변수로 상태 값을 호출하는 것이 가능하다. 특이한 점은 상태 갱신 함수 또한 작성해야한다는 것인데 상태 값을 변경하고 싶다면 오로지 이 상태 갱신 함수를 통해서 해야만 한다. 만약 다른 방법으로 상태 값을 변경하려고 시도한다면 제대로 변경되지 않는다. 또한 상태가 변경된다면 해당 컴포넌트와 그 밑에 하위 컴포넌트들은 전부 리렌더링 된다.
위의 예시는 useState 훅을 이용해서 체크박스를 구현하는 간단한 예시이다. 체크 박스는 2개가 있으며 위의 체크박스는 상태 갱신 함수를 통해 상태를 변경하지만 밑의 체크박슨는 직접 상태 저장 변수를 호출해서 값을 할당하는 방식으로 상태를 변경하려고 한다. 위의 체크박스는 정상적으로 상태가 변경되지만 밑의 체크박스는 클릭해도 변화가 없는 것을 확인할 수 있다. 또한 CheckboxExample 컴포넌트가 호출되는 것을 확인할 수 있게 가장 상단에 콘솔에 특정 문구를 출력하는 코드를 작성했다. 첫 번째 체크박스를 클릭할 때마다 콘솔에 해당 문구가 출력되는 것으로 보아 상태가 정상적으로 변경되면 상태를 가진 컴포넌트도 리렌더링 되고 있다는 것을 알 수 있다. 한편 두 번째 체크박스는 상태를 변경시키지 못하므로 리렌더링도 되지 않는다.
상태나 속성은 컴포넌트의 렌더링 결과나 동작에 영향을 미칠 수 있다는 점에서 이벤트와 깊은 관계가 있다. 리액트에서 이벤트 처리 방식은 HTML 문서에서 사용하는 것과 거의 유사한데 살짝 차이가 있다.
<button onClick={handleEvent}>Event</button>
위의 예시처럼 이벤트 핸들러가 소문자가 아닌 카멜 케이스를 사용하고 있으며 JSX 문법에 따라 중괄호에 담겨서 전달되고 있는 것을 볼 수 있다. 그렇다면 자주 사용하는 이벤트 핸들러는 어떤 것들이 있을까?
input이나 textarea와 같은 태그는 입력 폼을 삽입한다고 했었다. 이런 폼 엘린먼트는 주로 사용자의 입력값을 제어하는데 사용한다. 리액트에선 사용자의 입력에 따라 바뀔 수 있는 입력 값은 당연히 컴포넌트의 상태로 관리할 것이다. onChange 이벤트 핸들러는 입력 폼에 값이 바뀔 때마다 이벤트 발생되어서 이벤트 객체를 매개변수로 전달 받게 된다. 따라서 개발자는 이벤트 객체에서 값을 추출해서 사용할 수 있고 그 값을 상태 갱신 함수에서 사용함으로써 상태값을 입력 폼의 값과 똑같이 유지하는 것도 가능하다.
onChange 이벤트 핸들러는 변경을 감지한다면 onClick 이벤트 핸들러는 마우스 클릭을 감지한다.
위의 예시는 버튼을 하나 구현하고 onClick 이벤트 핸들러를 연결해서 버튼이 클릭될 때 alert 함수를 실행시켜 경고 상자 안에 상태값을 띄우게 구현되어 있다.
이런 식으로 리액트에서 변경될 수 있는 값을 상태로 관리하고 그 상태를 이벤트 핸들러를 통해 통제할 수 있는 컴포넌트를 흔히 Controlled Component(제어 컴포넌트)라고 부른다. 제어 컴포넌트의 경우에는 입력 값과 상태가 항상 동기화되기 때문에 입력 값이 바뀔때마다 상태 값도 바뀌고 그렇기 때문에 매번 컴포넌트가 리렌더링 된다. 따라서 실시간으로 입력 값을 반영해서 렌더링 되는 값을 수정해야 하거나 동작 로직이 변경되야 하는 컴포넌트를 이벤트 핸들러를 연결해서 제어 컴포넌트로 구현해야 한다. 하지만 특정 시점에만 값이 필요한 경우는 오히려 이런 기능을 구현하지 않아서 불필요한 리렌더링을 줄이는 것도 성능 향상에 도움이 될 수 있다.