Front End/React

React State & Props

Props

컴포넌트의 속성(property)을 의미한다. 사람으로 비유하자면 성별이나 이름처럼 변하지 않는 값이며, 외부로부터 전달받은 값으로 웹 어플리케이션에서 해당 컴포넌트가 가진 속성에 해당된다.

React 컴포넌트는 JavaScript의 함수와 클래스로, props를 함수의 전달인자처럼 전달받아 이를 기반으로 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환한다. 그래서 컴포넌트가 최초 렌더링 될 때 화면에 출력하고자 하는 데이터를 담은 초기값으로 사용 가능하다.

props는 객체 형태로 어떤 타입의 값도 넣어 전달할 수 있으며 외부로부터 전달받는 변하지 않는 값이기 때문에 쉽게 변경할 수 없는 읽기 전용(read-only) 객체이다.

⚠️
만약 읽기 전용 객체가 아니라면, props를 전달밭은 하위 컴포넌트 내에서 props를 직접 수정 시 상위 컴포넌트의 값에 영향을 미쳐 의도치 않은 side effect를 발생시킬 수 있다. 이는 React의 단방향, 하향식 데이터 흐름 원칙에 위배된다. (React is all about one-way data flow down the component hierarchy)

props 사용 방법

예를 들기 위해 상위 컴포넌트 Parent와 하위 컴포넌트 Child컴포넌트를 선언하고 Parent컴포넌트 안에 Child컴포넌트를 작성한다.

function Parent () {
	return (
		<div className="parent">
			<h1>I'm the parent</h1>
			<Child />
		</div>
	);
};

function Child() {
  return (
    <div className="child"></div>
  );
};

1. 하위 컴포넌트에 전달하고자 하는 값과 속성을 정의한다

React에서 속성 및 값을 할당하는 방법은 HTML의 속성을 정의하는 방식과 유사하다. 단, 전달하고자 하는 값을 중괄호를 이용해 감싸주면 된다. 위의 예시를 토대로 text라는 속성을 선언하고 이 속성에 문자열 값을 할당해 Child컴포넌트에 전달한다.

<Child text={"I'm the eldest child"} />

2. props를 이용해 정의된 값과 속성을 전달한다

하위 컴포넌트에서 속성을 전달받는 방법은 간단하다. 함수에 인자를 전달하듯 함수 선언 부분에 매개변수를 추가해주면 된다. 이 매개변수는 필요한 모든 데이터를 가지고 있다. Parent컴포넌트에서 전달한 문자열을 Child에서 받기 위해 매개변수를 추가한다.

function Child(props) { // props 매개변수 추가
  return (
    <div className="child"></div>
  );
};

3. 전달받은 props를 렌더링한다

전달받은 props는 객체이기 때문에, 객체에서 값을 가져오는 방식(dot notation)으로 작성하면 된다. 이 때 객체의 키는 상위 객체에서 선언한 속성(attribute)이다. Parent컴포넌트에서 text속성을 선언했기 때문에 해당 속성은 Child에서 props.text로 접근이 가능하다.

다른 방법으로는 상위 컴포넌트에서 하위 컴포넌트의 여는 태그와 닫는 태그 사이에 값을 넣는 것이다. 이 경우, prop.children을 이용하면 해당 value에 접근할 수 있다.

function Parent() {
  return (
    <div className="parent">
        <h1>I'm the parent</h1>
        <Child text={"I'm the eldest child"}>Hello, World!</Child>
    </div>
  );
};

function Child(props) {
  return (
    <div className="child">
        <p>{props.text}</p> {// "I'm the eldest child"}
        <p>{props.children}</p> {// "Hello, World!"}
    </div>
  );
};

State

컴포넌트를 사용하는 중 컴포넌트 내부에서 변할 수 있는 값을 뜻한다. props가 외부에서 전달받는 속성이고 변할 수 없는 것과 달리 State는 컴포넌트 안에서 변화하는 상태이다. 사람으로 비유하자면 나이, 직업 등 상황에 따라 변할수 있는 값인 것이다.

실제 어플리케이션을 예시로 쇼핑몰 장바구니를 들어보자. 사용자는 구매할 물건과 당장은 구매하지 않을 문건을 체크박스에 체크하여 구분짓는다. 이를 장바구니 내의 상태로 구분하면 check 된 상태check 되지 않은 상태이다.

로켓배송은 못참지

실제 쇼핑몰 화면을 보자. 체크 여부에 따라 구매할 물건의 개수나 구매 금액이 변경되고 이에 따라 사용자의 화면 또한 달라진다. 이처럼 컴포넌트 내에서 변할 수 있는 값, 즉 상태는 React state로 다루어야 한다.

State hook, useState

useState 사용법

React에서는 state를 다루는 방법 중 하나로 useState라는 특별한 함수를 제공한다. 체크박스를 예로 작성한다.

import React, { useState } from "react";
import "./styles.css";

function CheckboxExample() {
  const [isChecked, setIsChecked] = useState(false);

  const handleChecked = (event) => {
    setIsChecked(event.target.checked);
  };
  return (
    <div className="App">
      <input type="checkbox" checked={isChecked} onChange={handleChecked} />
      <span>{isChecked ? "Checked!!" : "Unchecked"}</span>
    </div>
  );
}

export default CheckboxExample;
  1. useState를 React로부터 import 키워드로 불러온다. import { useState } from "react";
  1. 이후 useState를 컴포넌트 안에서 호출한다. useState를 호출하는 것은 ‘state’라는 변수를 선언하는 것과 같지만, 일반적인 변수가 함수가 끝날 때 사라지는 것과 달리 state 변수는 React에 의해 함수가 끝나도 사라지지 않는다.
    function CheckboxExample() {
    // 새로운 state 변수를 구조 분해 할당 방식으로 선언
      const [isChecked, setIsChecked] = useState(false);
      isChecked; // false
    	setIsChecked; // 변수를 갱신할 수 있는 함수
    }

    useState를 호출하면 배열을 반환하는데, 0번째 요소에는 현재 state 변수가 들어가고 1번째 요소는 이 변수를 갱신할 수 있는 함수가 들어간다. useState의 인자로 넘겨주는 값은 state의 초기값이다. const [state 저장 변수, state 갱신 함수] = useState(상태 초기 값);

  1. state 변수에 저장된 값을 JSX 엘리먼트 안에서 직접 불러서 사용할 수 있다. 위의 예시에서는 isChecked가 불리언 값을 가지기 때문에 이에 따라 다른 결과가 보이도록 삼항연산자를 사용한다.
    <span>{isChecked ? "Checked!!" : "Unchecked"}</span>

state 갱신하기

  1. state를 갱신하려면 state 변수를 갱신할 수 있는 함수를 사용한다. 예시에는 setIsChecked변수에 할당해줬기 때문에 이를 호출한다.
  1. 예시의 경우 input[type=checkbox] 엘리먼트의 값 변경에 따라 isChecked가 변경되어야 하기 때문에, 엘리먼트의 값이 변경될 때 발생하는 onChange이벤트의 이벤트 핸들러로 handleChecked가 연결되어 있다. handleCheckedsetIsChecked를 호출하며, 호출된 결과에 따라 isChecked변수가 갱신된다. React는 새로운 isChecked변수를 CheckboxExample컴포넌트에 넘겨 해당 컴포넌트를 다시 렌더링한다.
    function CheckboxExample() {
      const [isChecked, setIsChecked] = useState(false);
    
      const handleChecked = (event) => {
        setIsChecked(event.target.checked);
      };
    
      return (
        <div className="App">
          <input type="checkbox" checked={isChecked} onChange={handleChecked} />
          <span>{isChecked ? "Checked!!" : "Unchecked"}</span>
        </div>
      );
    }

주의점

  • React 컴포넌트는 state가 변경되면 새롭게 호출되어 리렌더링된다.
  • React state는 상태 변경 함수 호출로 변경되어야 한다. 강제로 재할당하여 변경을 시도하면 리렌더링이 되지 않거나 state가 제대로 변경되지 않는다.

이벤트 처리

React의 이벤트 처리(이벤트 핸들링) 방식은 DOM의 이벤트 처리 방식과 유사하지만, 몇 가지 문법 차이가 있다.

  • React에서 이벤트는 소문자 대신 카멜 케이스(camelCase)를 사용한다.
  • JSX를 사용해 문자열이 아닌 함수로 이벤트 핸들러를 전달한다.
<button onclick="handleEvent()">Event</button> // HTML의 이벤트 처리 문법
<button onClick={handleEvent}>Event</button> // React의 이벤트 처리 문법

자주 사용하는 이벤트 처리

onChange

onChange<input>, <textarea>, <select>와 같은 폼(Form) 엘리먼트의 텍스트가 바뀔 때마다 발생하는 이벤트이다.

폼(Form) 엘리먼트는 사용자의 입력값을 제어하는 데 사용된다. React에서는 이러한 변경될 수 있는 입력 값을 일반적으로 컴포넌트의 state로 관리하고 업데이트 한다. 폼 엘리먼트에서 onChange이벤트가 발생하면 e.target.value를 통해 이벤트 객체에 담신 input값을 읽어올 수 있다.

function NameForm() {
  const [name, setName] = useState("");

  const handleChange = (e) => {
    setName(e.target.value);
  }

  return (
    <div>
      <input type="text" value={name} onChange={handleChange}></input>
      <h1>{name}</h1>
    </div>
  )
};

예시의 컴포넌트의 return문 안에 있는 input태그에 valueonChange를 넣어주면 입력창에 텍스트가 변경될 때 handleChange 함수가 작동하며 이벤트 객체에 담긴 input 값을 setName 함수를 통해 새로운 state로 갱신한다.

onClick

onClick이벤트는 말 그대로 사용자가 클릭이라는 행동을 할 때 발생하는 이벤트이며 <button>이나 <a>태그를 통한 링크 이동과 같이 주로 사용자의 행동에 따라 어플리케이션이 반응해야 할 때 자주 사용된다.

주의해야 할 점은 onClick이벤트에 함수를 전달할 때는 함수를 호출하는 것이 아니라 함수 자체를 전달하여야 하며, 해당 함수가 컴포넌트 내의 state에 접근할 수 있게 화살표 함수 () =>를 사용하여야 한다. 만약 함수를 호출하게 되면 컴포넌트가 렌더링 될때 함수가 호출되어 실행되게 되고, 리턴 값이 없는 함수는 undefined를 리턴하기 때문에 onClick에 적용되면 클릭했을 때 아무런 결과도 일어나지 않는다.

function NameForm() {
  const [name, setName] = useState("");

  const handleChange = (e) => {
    setName(e.target.value);
  }

	const handleClick = (e) => {
		alert(name);
	}

  return (
    <div>
      <input type="text" value={name} onChange={handleChange}></input>
      <button onClick={() => alert(name)}>Button</button> {// 함수를 정의해주거나}
      <button onClick={handleClick}>Button</button> {// 함수 자체를 전달하거나}
      <h1>{name}</h1>
    </div>
  );
};

Controlled Component

React에서는 상태에 해당하는 데이터를 state로 따로 관리하는 것을 권장하고, React가 state를 통제할 수 있는 컴포넌트를 Controlled Component라고 한다. 어떻게 하면 React가 state를 통제할 수 있을까? 위의 input을 예시로 들자면, input에 값을 입력할 시 state도 그때그때 바뀌면 된다. 그리고 이 변경된 state와 inputvalue 또한 같게 작성되어야 한다.

function App() {
  const [username, setUsername] = useState("");
  const [msg, setMsg] = useState("");

  return (
    <div className="App">
      <div>{username}</div>
      <input
        type="text"
        value={username}  // state와 value가 같을 수 있게 state를 넣어준다
        onChange={(event) => setUsername(event.target.value)}
        placeholder="여기는 인풋입니다."
        className="tweetForm__input--username"
      ></input>
      <div>{msg}</div>
      <textarea
        placeholder="여기는 텍스트 영역입니다."
        className="tweetForm__input--message"
        onChange={(event) => setMsg(event.target.value)}
				value={msg} // state와 value가 같을 수 있게 state를 넣어준다
      ></textarea>
    </div>
  );
}

React의 데이터 흐름

React의 개발 방식의 가장 큰 특징은 페이지 단위가 아닌, 컴포넌트 단위로 시작한다는 점이다. 그리고 이런 컴포넌트들은 기능에 따라 상위/하위의 관계를 가지고 있고 상위 컴포넌트에서 하위 컴포넌트로 속성(props)을 전달해주는 형태를 띈다. 이는 데이터가 하향식(top-down), 즉 단방향 데이터 흐름(one-way data flow)을 가진다는 의미를 뜻한다.

개발은 상향식(bottom-up)으로

그림의 위와 같이 프로토타입을 전달받았다면, 이 곳에서 만들어야 할 컴포넌트는 아래와 같다. 앞서 말했듯 React는 컴포넌트 단위로 개발을 하기 때문에 컴포넌트를 먼저 만들고 다시 페이지를 조립해 나간다. 즉, 상향식(bottom-up)으로 어플을 만들게 된다.

상향식 개발의 가장 큰 장점은 테스트가 쉽고 확장성이 좋다는 것이다. 그래서 우리는 프로토타입이나 앱의 디자인을 전달받게 되면 이를 컴포넌트 계층 구조로 나누는 것이 가장 먼저 해야 할 일이다.

이 때, 컴포넌트를 구분짓는 기준은 하나의 컴포넌트가 한 가지 일만 할 수 있도록 하는 것이다. 이를 단일 책임 원칙이라 한다. 이를 트리 구조로 나타내면 오른쪽 그림과 같다.

데이터의 흐름은 하향식(top-bottom)으로

다음은 데이터를 어디에 둘 지 결정한다. 앞서 말했듯 컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 인자나 속성처럼 전달받을 수 있다.

데이터를 전달하는 주체는 부모 컴포넌트가 되고 이말인 즉 데이터 흐름이 하향식(top-down)임을 의미한다. 이 원칙은 단방향 데이터 흐름(one-way data flow)이라는 키워드가 React를 대표하는 설명 중 하나일 정도로 매우 중요하다. 또한 컴포넌트는 props를 통해 전달받은 데이터가 어디에서 왔는지 확인할 수 없다.

데이터 정의, 속성(props)인가 상태(state)인가?

어플리케이션에서 필요한 데이터가 무엇이 있을지 생각해보면 다음과 같은 데이터가 있을 것이다.

  • 전체 트윗 목록
  • 사용자가 작성 중인 새로운 트윗 내용

모든 데이터를 상태로 둘 필요는 없으며, 상태가 많아질수록 어플리케이션은 복잡해지기 때문에 상태는 최소화라는 것이 가장 좋다. 어떤 데이터를 상태로 두어야 할까? 다음 세 가지 질문으로 판단할 수 있다. 해당이 된다면 상태가 아닌 것이다.

  • 부모로부터 props를 통해 전달되는가?
  • 시간이 지나도 변하지 않는가?
  • 컴포넌트 안의 다른 state나 props를 가지고 계산이 가능한가?

상태(state)를 위치시키는 방법

상태가 특정 컴포넌트에서만 유의미하다면 해당 컴포넌트에만 두면 되지만, 만약 하나의 상태를 기반으로 두 컴포넌트가 영향을 받는다면 이 때는 공용 소유 컴포넌트를 찾아 그 곳에 상태를 위치해야 한다. 즉, 두 개의 자식 컴포넌트가 하나의 상태에 접근하고자 할 때는 두 자식의 공통 부모 컴포넌트에 상태를 위치해야 한다.

상태를 계승중입니다 아버지

예시를 토대로 : “전체 트윗 목록” 데이터 위치시키기

그렇다면 “전체 트윗 목록” 데이터는 어디에 위치해야 할까? 전체 트윗은 개별 트윗을 출력하기 위해 Tweets 컴포넌트가 필요로 한다. 그리고 하위 컴포넌트인 SingleTweet은 모두 “전체 트윗 목록”에 의존하여 실제 각 데이터를 출력한다. SingleTweet의 부모 컴포넌트가 Tweets이기 때문에 이 데이터는 Tweets에 위치하는 것이 맞다.

이처럼 React에서 데이터를 다룰 때는 컴포넌트들간의 상호 관계와 데이터의 역할, 데이터의 흐름을 고려하여 위치를 설정해야 한다.


Uploaded by N2T