JD의 블로그

Learning React - 6장 본문

Web/프론트엔드

Learning React - 6장

GDong 2021. 10. 11. 00:53

리액트 상태 관리

사용자 인터페이스는 콘텐츠 생산자가 콘텐츠를 만들어내기 위해 사용하는 도구다. 콘텐츠 생산자에게 최선의 도구를 만들기 위해서는 데이터를 효율적으로 조작하고 변경하는 방법을 알아야 한다.

 

컴포넌트 트리는 프로퍼티를 통해 데이터가 흘러갈 수 있는 컴포넌트 계층 구조를 뜻한다. 프로퍼티는 절반을 담당하고, 나머지 절반은 상태(State)가 담당하게 된다. 리액트 애플리케이션의 상태는 데이터에 의해 조종되며 변경될 수 있다. 상태를 조리법 애플리케이션에 도입하면 셰프가 새 조리법을 만들 수 있고, 기존 조리법을 변경하거나 오래된 조리법을 제거할 수도 있다.

 

상태와 프로퍼티는 서로 관계가 있다. 리액트 애플리케이션을 사용할 때는 이 관계에 기반해 컴포넌트들을 부드럽게 합성해 서로 엮는다. 컴포넌트의 트리가 상태가 바뀌면 프로퍼티도 바뀐다. 새로운 데이터는 컴포넌트 트리를 타고 흐르고, 콘텐츠에 새로 반영되도록 특정 말단이나 가지가 다시 렌더링된다. 

이번 장에서는 상태가 있는 컴포넌트를 만드는 방법을 배우고, 컴포넌트 트리의 아래 방향으로 상태를 전달하는 방법과 사용자 상호작용을 컴포넌트 트리의 위쪽으로 돌려보내는 방법을 살펴본다.

 

1. 별점 컴포넌트 만들기

5점 만점의 별점 시스템은 다양한 서비스에 적용되어 있다. 

StarRating 컴포넌트를 통해 사용자는 콘텐츠에 별의 개수를 바탕으로 점수를 매길 수 있다. 별은 react-icons에서 별을 얻을 수 있다.

 

npm i react-icons

react-icons는 npm 라이브러리로, 리액트 컴포넌트 형태로 된 수백 가지 SVG 아이콘을 제공한다. 

 

import React from 'react';
import { FaStar } from "react-icons/fa";

export default function StartRating() {
  return [
    <FaStar color="red" />,
    <FaStar color="red" />,
    <FaStar color="red" />,
    <FaStar color="grey" />,
    <FaStar color="grey" />,
  ]
}

이 코드는 react-icons에서 임포트한 SVG 별을 다섯 개 렌더링하는 StarRating 컴포넌트를 만든다. 선택된 별은 빨간색으로 칠해져야 하고, 선택되지 못한 별은 회색으로 표시되야 한다. 

 

선톡된 프로퍼티에 따라 자동으로 별을 만들어내는 컴포넌트를 하나 만들자.

const Star = ({selected = false}) => (
  <FaStar color={selected ? "red": "grey"} />
);

Star 컴포넌트는 별 하나를 렌더링한다. 이때 selected 프로퍼티에 따라 적절한 색으로 별안쪽을 채워넣는다. selected 프로퍼티가 컴포넌트에 전달되지 않으면 별이 선택되지 않았다고 가정하고 기본적으로 회색으로 별 내부를 칠한다.

 

5점 별점 시스템도 좋지만 10점 별점 시슽메이 훨씬 더 자세한 정보를 제공할 수 있다. 개발자가 자신의 앱에 별점 컴포넌트를 추가할 때 원하는 대로 별의 개수를 정할 수 있게 해야 한다. totalStars 프로퍼티를 StarRating 컴포넌트에 추가해서 전체 별 개수를 지정할 수 있게 한다.

 

const createArray = lenght => [...Array(length)];

export default function StarRating({totalStars = 5}) {
  return createArray(totalStars).map((n,i) => <Star key={i}/>);
}

여기서 2장에서 배운 createArray 함수를 사용한다. 생성하려는 배열의 원소 개수를 지정하기만 하면 원하는 개수의 원소가 들어있는 배열을 얻을 수 있다. 이 함수를 totalStars 프로퍼티랑 함께 사용하면 원하는 길이의 배열을 생성할 수 있다. 배열이 생기면 그 배열에 대해 map을 수행하면서 Star 컴포넌트를 렌더링한다. 

 

2. useState 훅

이제 StarRating 컴포넌트를 클릭할 수 있게 만들어야 한다. 사용자는 컴포넌트를 클릭해서 rating을 바꿀 수 있어야한다. rating은 변경될 수 있는 값이기 때문에, 리액트 상태에 이 값을 저장하고 변경해야 한다. 상태를 리액트 함수 컴포넌트에 넣을 때는 훅스(Hooks)라고 부르는 리액트 기능을 사용한다. 훅스에는 컴포넌트 트리와 별도로 재사용 가능한 코드 로직이 들어 있다. 훅스를 사용하면 우리가 만든 컴포넌트에 기능을 끼워 넣을 수 있다. 리액트는 몇 가지 훅을 기본적으로 제공하므로 즉시 훅을 사용할 수 있다. 

 

가장 먼저 다룰 훅은 상태를 리액트 컴포넌트에 추가하고 싶을 때 사용할 수 있는 useState 훅이다. 이 훅은 실제로는 react 패키지에 들어있어서 임포트하기만 하면 된다.

import React, {useState} from "react";
import {FaStar} from 'react-icons/fa';

const createArray = length => [...Array(length)];

const Star = ({selected = false}) => (
   <FaStar color={selected ? "red":"grey"} />
);


export default function StarRating({ totalStars = 5}) {
  const [selectedStars] = useState(3);
  return (
    <>
      {createArray(totalStars).map((n,i) => (
        <Star key={i} selected={selectedStars > i} />
      ))}
      <p>
        {selectedStars} / {totalStars}
      </p>
    </>
  )
}

useState 훅은 배열을 반환하는 호출 가능한 함수다. 이 배려의 첫 번째 값이 우리가 사용하려는 상태 변수다. 여기서는 이 변수가 selectedStars이고, 이 변수의 값은 StarRating에서 빨간색으로 칠해야 하는 별의 개수다. useState는 배열을 반환한다. useState 함수에 전달하는 값은 상태 변수의 디폴트 값이다. 

 

사용자로부터 다른 점수를 얻기 위해서는 사용자가 아무 별이나 클릭할 수 있게 해야한다. 이 말은 onClick 핸들러를 FaStar 컴포넌트에 추가해서 별을 클릭할 수 있게 만들어야 한다는 뜻이다.

 

const Star = ({selected = false, onSelect = f => f}) => (
   <FaStar color={selected ? "red":"grey"} onClick={onSelect} />
);

Star를 변경해서 onSelect 라는 프로퍼티를 추가한다. 이 프로퍼티는 함수다. 사용자가 FaStar 컴포넌트를 클릭하면 이 함수를 호출할 것이다. 이 함수는 부모 컴포넌트에게 별이 클릭됐음을 통지한다. 이 함수의 디폴트 값은 f => f 인데 이 함수는 인자로 받은 값을 그대로 돌려주는 일 외에 아무 일도 안하는 가짜 함수일 뿐이다. 하지만 onSelect 프로퍼티의 값은 반드시 함수여야 하므로 onSelect에 함수를 지정하지 않고 FaStar 컴포넌트를 클릭하면 오류를 발생한다. 

 

이제 Star 컴포넌트를 클릭할 수 있으므로 이 성질을 이용해 StarRating의 상태를 바꿔본다.

export default function StarRating({ totalStars = 5}) {
  const [selectedStars, setSelectedStars] = useState(0);
  return (
    <>
      {createArray(totalStars).map((n,i) => (
       <Star
        key={i}
        selected ={selectedStars > i}
        onSelect={()=> setSelectedStars(i+1) }
      />
      ))}
      <p>
        {selectedStars} / {totalStars}
      </p>
    </>
  )
}

StarRating 컴포넌트의 상태를 바꾸려면 selectedStars의 값을 바꾸는 함수가 필요하다. useState 훅이 반환하는 배열의 두 번째 원소는 상태 값을 변경할 때 쓸 수 있는 함수다. 

 

훅스에서 가장 중요한 내용은 훅이 걸린 컴포넌트를 렌더러와 연동시킨다는 점이다.

setSelectedStars 함수를 사용해 selectedStars의 값을 바꿀 때마다 StarRating 함수 컴포넌트가 훅에 의해 다시 호출되면서 렌더링이 다시 이뤄진다. 이때 새로운 렌더링은 selectedStars의 새 값을 활용해 이뤄진다. 훅스가 중요하고 유용한 기능이 될 수 있는 이유가 바로 이것이다. 훅이 걸린 데이터가 변경되면 데이터에 대한 훅이 걸린 컴포넌트에 새 값을 전달하면서 컴포넌트를 다시 렌더링해준다. 

 

사용자가 Star를 클릭할 때마다 StarRating 컴포넌트가 다시 렌더링된다. 사용자가 Star를 클릭하면 해당 Star의 onSelect 프로퍼티가 호출된다. onSelect 프로퍼티는 setSelectedStars 함수를 호출해서 방금 선택한 별의 개수를 전송한다. map 함수의 i 변수를 사용해 개수를 쉽게 계산할 수 있다. map이 첫 번째 Star를 렌더링할 때 i의 값은 0이다. 따라서 i에 1을 더해서 올바른 별의 개수를 얻을 수 있다. setSelectedStars가 호출되면 새로운 selectedStars 값을 사용해 StarRating 컴포넌트가 호출된다.

 

3. 재사용성을 높이기 위한 리팩터링

사용자에게 평점을 받을 필요가 있는 여러 애플리케이션에서 이 컴포넌트를 활용할 수 있다. 하지만 이 컴포넌트를 npm에 올려 누구나 다른 사용자에게 평점을 받기 위해 이 컴포넌트를 활용하게 만들고 싶다면 몇 가지를 더 생각해봐야한다.

 

1. style 프로퍼티를 생각해봐야 한다. 이 프로퍼티를 사용하면 CSS 스타일을 엘리먼트에 추가할 수 있다. 나중에 개발자가 전체 컨테이너의 스타일을 변경해야 할 수도 있다. 이런 경우 아래와 같이 스타일을 추가할 수 있다.

export default function App() {
	return <StarRating style={{ backgroundColor: "lightblue" }} />;
}

모든 리액트 엘리먼트는 style 프로퍼티를 제공한다. 대부분의 컴포넌트도 스타일 프로퍼티를 제공한다. 따라서 전체 컴포넌트의 스타일을 변경하는게 합리적이다.

 

해야 할 일은 모든 스타일을 모아서 StarRating 컨테이너에 전달하는 것뿐이다. 현재는 리액트 프래그먼트를 사용하기 때문에 StarRating에는 단일 컨테이너가 없다. 따라서 이 방식이 작동하려면 프래그먼트를 div 엘리먼트로 업그레이드하고, 이 div에 스타일을 전달해야한다.

 

export default function StarRating({style={}, totalStars= 5}) {
	const [selectedStars, setSelectedStars] = useState(0);
    return (
    	<div style={{ padding: "5px", ...style}}>
        	{createArray(totalStars).map((n,i) => (
            	<Star
                	key={i}
                    selected={selectedStars >i}
                    onSelect={()=> setSelectedStars(i+i)}
                />
            ))}
            <p>
            	{selectedStars} / {totalStars}
            </p>
        </div>
    );
}

이 코드에서 프래그먼트를 div엘리먼트로 바꾸고 이 div 엘리먼트에 대해 스타일을 적용했다. 디폴트로 div에 5px의 padding을 적용하고, 스프레드 연산자를 통해 style 객체의 나머지 프로퍼티를 div 스타일로 가져왔다.

 

 

4. 컴포넌트 트리 안의 상태

모든 컴포넌트에 상태를 넣는 것은 좋은 생각이 아니다. 상태 데이터가 너무 많은 컴포넌트에 분산되면 버그를 추적하거나 애플리케이션의 기능을 변경하기 어려워진다. 어려워지는 이유는 컴포넌트 트리에서 어느 부분에 상태가 존재하는지 파악하기 힘들기 때문이다. 애플리케이션의 상태를 한곳에서 관리할 수 있으면 상태를 이해하기 더 쉬워진다. 상태를 한곳에서 관리하는 방법이 몇 가지 있는데 그 중 처음 살펴볼 것은 상태를 컴포넌트 트리에 저장하고, 자식 컴포넌트들에게 프롭으로 전달하는 방법이다. 

 

색의 목록을 관리하는 작은 애플리케이션을 생각해보자. 이 앱은 사용자가 목록에 있는 색에 대해 별점과 제목을 부여할 수 있게 한다. 

먼저 다음과 같은 데이터 집합이 있다.

 

{
    colors: [
        {
            "id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
            "title": "해질녘 바다",
            "color": "#00c4e2",
            "rating": 5
        },
        {
            "id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
            "title": "잔디",
            "color": "#26ac56",
            "rating": 3
        },
        {
            "id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
            "title": "밝은 빨강",
            "color": "#ff0000",
            "rating": 0
        }
    ]
}

color-data.json 파일에는 3가지 색으로 이뤄진 배열이 들어있다. 각 색에는 id, title, color, rating이 있다. 

먼저 이 데이터를 브라우저에 표시할 리액트 컴포넌트로 구서된 UI를 만들고, 그 후 사용자가 새로운 리스트에 색을 추가하거나 삭제하고, 색에 평점을 매길 수 있게 만들면 된다.

 

4.1 상태를 컴포넌트 트리의 아래로 내려보내기

이터레이션에서는 상태를 색 관리자 앱의 루트인 App 컴포넌트에 저장한다. 그리고 색을 자식 컴포넌트로 내려보내서 렌더링한다. App 컴포넌트는 우리 앱에서 상태를 저장할 유일한 컴포넌트다. useState 훅을 사용해 App에 색 리스트를 추가한다.

 

import React, { useState } from "react";
import colorData from "./color-data.json";
import ColorList from "./ColorList.js";

export default function App() {
	const [colors] = useState(colorData);
    return <ColorList colors={colors} />;
}

App 컴포넌트는 트리의 최상위에 있다. useState를 이 컴포넌트에 추가하면 색의 상태 관리를 App과 연결한다. colorData는 앞에서 본 색의 배열이다. App 컴포넌트는 colorData를 colors의 초기 상태로 사용한다. App으로부터 colors가 ColorList라는 컴포넌트에게 전달한다. 

 

import React from "react";
import Color from "./color";

export default function ColorList({ colors =[]}) {
	if(!colors.length) return <div>표시할 색이 없습니다. </div>;
    return (
    	<div>
        	{
            	colors.map(color => <Color key={color.id} {...color} />)
            }
        </div>
    );
}

ColorList는 props로 App 컴포넌트에게서 색을 전달받는다. colors에 색 배열이 있으면 이 배열에 대해 map을 수행하면서 세부 정보를 Color로 만들면서 트리의 아래 방향으로 각 색 정보를 내려보낸다.

export default function Color({ title, color, rating}) }
	return (
    	<section>
        	<h1>{title}</h1>
            <div style={{ height: 50, backgroundColor: color }} />
            <StarRating selectedStars={rating} />
        </section>
    );
}

Color 컴포넌트는 받은 프로퍼티를 통해 title과 color 그리고 rating을 표시한다. 

 

export default function StarRating({ totalStars = 5, selectedStars = 0}) {
	return (
    	<>
        	{createArray(totalStars).map((n,i) => (
            	<Star
                	key={i}
                    selected={selectedStars > i}
                />
            ))}
            <p>
            	{selectedStars} / {totalStars}
            </p>
        </>
    );
}

StarRating 컴포넌트를 순수 컴포넌트로 변경했다. 순수 컴포넌트는 상태가 없기 때문에 항상 같은 프롭에 대해 같은 사용자 인터페이스를 렌더링해주는 컴포넌트를 뜻한다. 색 별점 상태가 컴포넌트 트리의 루트에 있는 colors 배열에 저장되기 때문에 이 컴포넌트를 순수 컴포넌트로 만들 수 있었다. 

 

4.2 상호작용을 컴포넌트 트리 위쪽으로 전달하기

지금까지는 리액트 컴포넌트를 합성해 데이터를 부모 컴포넌트에서 자식 컴포넌트로 내려보내는 방식으로 컴포넌트 트리 아래쪽으로 내려보냄으로써 colors 배열을 렌더링했다. 리스트에서 색을 제거하거나 리스트에 있는 색의 평점을 변경하면 자식 컴포넌트에 대해 벌어진 상호작용을 수집해서 트리의 위로 올려보내 상태가 저장된 루트 컴포넌트에 도착하게 해야한다.

 

예를 들어 색 이름 옆에 상태로부터 해당 색을 제거하는 삭제 버튼을 붙이고 싶다면 Color 컴포넌트에 이런 버튼을 추가할 수 있다.

import {FaTrash} from "react-icons/fa";

export default function Color({id, title, color, rating, onRemove = f => f}) {
	return (
    	<section>
        	<h1>{title}</h1>
            <button onClick={() => onRemove(id)}>
            	<FaTrash />
            </button>
            <div style={{ height: 50, backgroundColor: color}} />
            <StarRating selectedStars={rating} />
        </section>
    );
}

여기서 먼저 쓰레기통 아이콘을 react-icon에서 임포트했다. 그리고 이 FaTrash 아이콘을 버튼으로 둘러싸고 onClick 핸들러를 추가하면 onRemove 함수 프로퍼티를 호출할 수 있다. 이 함수 프로퍼티는 프로퍼티 리스트에 id 프로퍼티와 함께 추가된다. 사용자가 쓰레기통 아이콘으로 표시된 삭제 버튼을 클릭하면 removeColor를 호출하면서 이 함수에게 삭제할 색의 id를 전달할 것이다. 그래서 Color 컴포넌트의 프로퍼티로 id를 추가해야 한다.

 

 

'Web > 프론트엔드' 카테고리의 다른 글

Learning React - 5장  (0) 2021.10.08
Learning React - 4장  (0) 2021.10.06
Learning React - 3장  (0) 2021.10.06
Learning React - 2장  (0) 2021.10.04