자바스크립트/러닝 리엑트

리액트 상태 관리

막이86 2023. 11. 13. 11:32
728x90

러닝 리엑트을 요약한 내용입니다.

  • 컴포넌트 트리는 프로퍼티를 통해 데이터가 흘러갈 수 있는 컴포넌트 계층 구조를 뜻한다.
  • 상태와 프로퍼티는 서로 관계가 있다.
    • 컴포넌트 트리의 상태가 바뀌면 프로퍼티도 바뀐다.
    • 새로운 데이터는 컴포넌트 트리를 타고 흐르고, 콘텐츠에 새로 반영되도록 한다.
    • 반영된 데이터를 가지고 다시 랜더링 된다.

6.1 별점 컴포넌트 만들기

  • StartRating 컴포넌트를 통해 사용자는 콘텐츠에 별의 개수를 바탕으로 점수를 매길수 있다.
  • 별에 대한 icon은 react-icons에서 설치해서 사용한다.
  • // 프로젝트 생성 create-react-app star-rating // 별 아이콘을 가져오기 위한 라이브러리 설치 npm install react-icons
  • 별 다섯 개 렌더링하는 StarRating 컴포넌트
    • 3개는 빨간색, 2개는 회색으로 채워진다.
    import React from 'react'
    import { FaStar } from 'react-icons/fa'
    
    export default function StarRating() {
    	return [
    		<FaStar color="red" />,
    		<FaStar color="red" />,
    		<FaStar color="red" />,
    		<FaStar color="grey" />,
    		<FaStar color="grey" />
    	]
    }
    
    // App.js
    import './App.css';
    import StarRating from './components/StartRating';
    
    function App() {
      return (
        <div className="App">
          <StarRating />
        </div>
      );
    }
    
    export default App;
    
  • 선택된 프로퍼티에 따라 자동으로 별을 만들어내는 컴포넌트
    • 프로퍼티 값에 따라 별이 빨강색이 되거나 회색이 되도록 한다.
    import React from 'react'
    import { FaStar } from 'react-icons/fa'
    
    export default function StarRating({ selectd = false }) {
    	return <FaStar color={ selectd ? "red" : "grey"} />
    }
    
  • 5점, 10점 별점 시스템을 만들 수 있도록 별 개수를 지정할 수 있도록 수정
  • import React from 'react' import Star from './Star' const createArray = length => [...Array(length)] export default function StarRating({ totalStars = 5}) { return createArray(totalStars).map((n, i) => <Star key={i} />) }

6.2 useState 훅

  • rating 값은 변경이 될수 있는 값이기 때문에 리액트 상태에서 값을 저장하고 변경을 해야한다.
  • 상태를 리액트 함수 컴포넌트에 넣을 때는 훅스라고 부르는 기능을 사용한다.
    • 훅스에는 컴포넌트 트리와 별도로 재사용 가능한 코드 로직이 들어 있다.
    • 리액트에는 몇 가지 훅을 기본 제공한다.
    • useState 훅은 react 패키지에 들어 있다.
    // useState 를 사용하는 방법
    import React, { useState } from 'react'
    
  • 선택한 별의 개수를 저장하는 selectedStart라는 상태 변수를 만들고, useState 훅을 사용
    • 컴포넌트와 상태를 서로 엮었다(hooked)
    • selectedStars는 초기값으로 3으로 설정
    import React, { useState } from 'react'
    import Star from './Star'
    
    const createArray = length => [...Array(length)]
    
    export default function StarRating({ totalStars = 5 }) {
    	const [selecedStars] = useState(3)
    	return (
    		<>
    			{createArray(totalStars).map((n, i) => <Star key={i} selectd={selecedStars > i} />)}
    			<p>
    				{selecedStars} / {totalStars}
    			</p>
    		</>		
    	)
    }
    
  • 이벤트를 만들어 별을 선택할 수 있도록 수정
    • onSelect라는 프로퍼티를 추가
    • 별을 클릭하면 부모 컴포넌트에게 이벤트 알림
    • 디폴트 값은 f ⇒ f, 아무것도 하지 않는 함수
      • 오류를 발생하지 않도록 하기 위한 코드
    import React from 'react'
    import { FaStar } from 'react-icons/fa'
    
    export default function StarRating({ selectd = false, onSelect = f => f }) {
    	return <FaStar color={ selectd ? "red" : "grey"} onClick={onSelect} />
    }
    
  • StarRating의 상태를 변경해보기
    • useState 훅이 반환하는 배열의 두 번째 원소는 상태를 변경할 때 사용하는 함수
    • 함수 이름에 set을 붙여 setSelectedStars로 정의
    • 값이 변경이 되면 다시 컴포넌트를 랜더링 해준다.
    import React, { useState } from 'react'
    import Star from './Star'
    
    const createArray = length => [...Array(length)]
    
    export default function StarRating({ totalStars = 5 }) {
    	const [selecedStars, setSelectedStars] = useState(3)
    	return (
    		<>
    			{createArray(totalStars).map((n, i) =>
    				<Star key={i} selectd={selecedStars > i} onSelect={ () => setSelectedStars(i + 1) }/>
    			)}
    			<p>
    				{selecedStars} / {totalStars}
    			</p>
    		</>		
    	)
    }
    

<aside> 💡 훅이 걸린 컴포넌트를 렌더와 연동 시킨다. selectedStars의 값이 변경될 때마다 랜더링이 다시 이루어진다.

</aside>

구시대적 리액트 상태 관리 - 시간 나면 추가 해볼 것!!!

  • v16.8.0 이전에 사용 방법

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

  • 추후 컨테이너의 스타일을 변경 할 수 있기 때문에 스타일을 수정할 수 있도록 추가
    • 모든 리액트 엘리먼트는 스타일 프로퍼티를 제공한다.
    • 대부분의 컴포넌트도 스타일 프로퍼티를 제공한다.
    export default function App() {
    	return <StarRating style={{ backgroundColor: "lightblue" }} />
    }
    
  • 모든 스타일을 모아서 SartRating 컨테이너에 전달하자
    • 프레그먼트를 div 엘리먼트로 업그레이드하고 div에게 스타일을 전달
    import React, { useState } from 'react'
    import Star from './Star'
    
    const createArray = length => [...Array(length)]
    
    export default function StarRating({ style = {}, totalStars = 5 }) {
    	const [selecedStars, setSelectedStars] = useState(3)
    	return (
    		<div style={{ padding: "5px", ...style}}>
    			{createArray(totalStars).map((n, i) =>
    				<Star key={i} selectd={selecedStars > i} onSelect={ () => setSelectedStars(i + 1) }/>
    			)}
    			<p>
    				{selecedStars} / {totalStars}
    			</p>
    		</div>		
    	)
    }
    

6.4 컴포넌트 트리 안의 상태

  • 모든 컴포넌트에 상태를 넣는 것은 좋은 생각이 아니다.
    • 컴포넌트 트리에서 어느 부분에 상태가 존재하는지를 제대로 알기 어려움
  • 애플리케이션의 상태나 어떤 특성의 상태를 한곳에서 관리하는 것이 이해하기 더 쉬움
  • 상태를 컴포넌트 트리에 저장, 자식 컴포넌트에 전달 하는 방법에 대해서 알아보기
  • 색의 목록을 관리하는 애플리케이션의 데이터
  • { "colros": [ { "id": "1", "title": "해질녘 바다", "color": "#00c4e2", "rating": 5 }, { "id": "2", "title": "잔디", "color": "#26ac56", "rating": 3 }, { "id": "3", "title": "밝은 빨강", "color": "#ff0000", "rating": 0 } ] }

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

  • App 컴포넌트가 트리의 최상위이기 때문에 useState를 사용해서 색관련 데이터를 저장 및 관리
  • colorData를 colors의 초기 상태로 사용
  • import './App.css'; import React, { useState } from 'react' import colorData from './color-data.json' import ColorList from './components/ColorList'; export default function App() { const [colors] = useState(colorData) return <ColorList colors={colors} /> }
  • ColorList는 App 컴포넌트에세 색을 전달 받는다.
    • colors가 비어 있으면 컴포넌트는 사용자에게 메세지를 표시한다.
    • 세부 정보를 Color로 정보를 내려보낸다.
    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>
        )
    }
    
  • 세부 정보를 표시하는 Color 컴포넌트
    • Color 컴포넌트는 title, color, rating을 프로퍼티로 받는다.
      • {...color} 스프레드 연산자로 전달 받는다.
    import React from 'react'
    import StarRating from './StartRating'
    
    export default function Color({ title, color, rating }) {
        return (
            <section>
                <h1>{title}</h1>
                <div style={{ height: 50, backgroundColor: color }} />
                <StarRating selectedStarts={rating} />
            </section>
        )
    }
    
  • StarRating 컴포넌트는 rating 값을 선택된 별의 개수로 표시
    • StarRating 컴포넌트를 순수 컴포넌트로 변경
    • 순수 컴포넌트는 상태가 없기 때문에 항상 같은 프로퍼티에 대해서 같은 결과를 리턴 또는 랜더링
    import React from 'react'
    import Star from './Star'
    
    const createArray = length => [...Array(length)]
    
    export default function StarRating({ style = {}, totalStars = 5, selectedStarts = 0 }) {
    	return (
    		<div style={{ padding: "5px", ...style}}>
    			{createArray(totalStars).map((n, i) =>
    				<Star key={i} selectd={selectedStarts > i} />
    			)}
    			<p>
    				{selectedStarts} / {totalStars}
    			</p>
    		</div>		
    	)
    }
    

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

  • 리스트에서 색을 제거하거나 리스트에 있는 색의 평점을 변경하는 방법 알아보자
  • title 옆에 색을 제거하는 삭제 버튼을 추가해보기
    • 사용자가 쓰레기통 아이콘을 클릭하면 onRemove가 호출되고 삭제될 id를 전달
      • Color 컴포넌트는 상태가 없기 때문에 이벤트에 신경쓰지 않아도 됨
      • Color 컴포넌트를 순수 컴포넌트로 유지할 수 있다.
    import React from 'react'
    import { FaTrash } from 'react-icons/fa'
    import StarRating from './StartRating'
    
    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 selectedStarts={rating} />
            </section>
        )
    }
    
  • ColorList도 이벤트를 상위 컴포넌트에게 전달
  • import React from 'react' import Color from './Color' export default function ColorList({ colors = [], onRemoveColor = f => f }) { if (!colors.length) return <div>표시할 색이 없습니다.</div> return ( <div> { colors.map(color => <Color key={color.id} {...color} onRemove={onRemoveColor} />) } </div> ) }
  • App 컴포넌트에서 id를 사용해 상태에서 색을 제거
    • setColors 변수를 추가 한다. (useState가 반환하는 배열의 두 번째 인자는 상태를 변경할때 사용하는 함수)
    • ColorList 가 onRemoveColor 이벤트를 발생시키면 색의 id를 제외한 나머지 값을 다시 저장
    • colors 상태를 바꾸면 App 컴포넌트가 새로운 색 목록에 맞춰 다시 랜더링 된다.
    import './App.css';
    import React, { useState } from 'react'
    import colorData from './color-data.json'
    import ColorList from './components/ColorList';
    
    export default function App() {
      const [colors, setColors] = useState(colorData)
      return (
        <ColorList 
          colors={colors}
          onRemoveColor={id => {
            const newColors = colors.filter(color => color.id !== id)
            setColors(newColors)
          }}
        />
      )
    }
    

색의 평점을 수정하는 코드

  • onRemoveColor와 비슷한 방식으로 onRate 이벤트를 적용
  • StarRating에 onRate 핸들러에서 평점 점수를 입력
  • import React from 'react' import Star from './Star' const createArray = length => [...Array(length)] export default function StarRating({ totalStars = 5, selectedStarts = 0, onRate = f => f }) { return ( <> {createArray(totalStars).map((n, i) => <Star key={i} selectd={selectedStarts > i} onSelect={() => onRate(i + 1)}/> )} <p> {selectedStarts} / {totalStars} </p> </> ) }
  • Color 컴포넌트는 상위 컴포넌트에서 프로퍼티로 받은 onRate를 StarRating으로 전달
    • onRate를 상위 컴포넌트로 전달 할때 id값도 같이 넘겨줌
    import React from 'react'
    import { FaTrash } from 'react-icons/fa'
    import StarRating from './StartRating'
    
    export default function Color({ id, title, color, rating, onRemove = f => f, onRate = f => f }) {
        return (
            <section>
                <h1>{title}</h1>
                <button onClick={() => onRemove(id)}>
                    <FaTrash />
                </button>
                <div style={{ height: 50, backgroundColor: color }} />
                <StarRating selectedStarts={rating} onRate={onRate} />
            </section>
        )
    }
    
  • ColorList 컴포넌트는 Color 컴포넌트에서 발생한 onRate 이벤트를 상위 컴포넌트로 전달
  • import React from 'react' import Color from './Color' export default function ColorList({ colors = [], onRemoveColor = f => f, onRateColor = f => f }) { if (!colors.length) return <div>표시할 색이 없습니다.</div> return ( <div> { colors.map(color => <Color key={color.id} {...color} onRemove={onRemoveColor} onRate={onRateColor} />) } </div> ) }
  • App 컴포넌트에서 평점을 수정하는 코드를 작성
  • import './App.css'; import React, { useState } from 'react' import colorData from './color-data.json' import ColorList from './components/ColorList'; export default function App() { const [colors, setColors] = useState(colorData) return ( <ColorList colors={colors} onRemoveColor={id => { const newColors = colors.filter(color => color.id !== id) setColors(newColors) }} onRateColor={(id, rating) => { const newColors = colors.map(color => color.id === id ? {...color, rating} : color) setColors(newColors) }} /> ) }
  • 프로퍼티를 통해 데이터를 컴포넌트 트리로 내려보는 것과 비슷하게, 사용자 상호작용은 함수 프로퍼티를 통해 데이터와 함께 트리 위로 전달된다.

6.5 폼 만들기

  • HTML 폼 엘리먼트 모두는 리액트 엘리먼트로도 제공된다.
  • 색을 추가하는 폼
  • <form> <input type="text" placeholder="color title..." required /> <input type="color" required /> <button>ADD</button> </form>

6.5.1 참조 사용하기

  • 리액트에서 폼 컴포넌트를 만들때 몇가지 패턴중 참조라는 리액트 기능을 사용해 직접 DOM에 접근하는 방법을 알아보기
  • AddColorForm 컴포넌트 생성
    • useRef 훅을 사용하여 참조를 만든다
    • ref 프로퍼티를 사용하면 참조의 값을 직접 JSX에서 설정 가능
    • ref 속성을 추가하면 DOM 엘리먼트를 직접 참조 객체에 대한 current 필드를 생성됨
    • ADD 버튼을 누르면 submit 함수가 호출 됨
      • e.preventDefault를 사용해 이벤트 막기
    • onNewColor 함수 프로퍼티를 통해 상위 컴포넌트로 값 전달
    import React, {useRef} from 'react'
    
    export default function AddColorForm({ onNewColor = f => f}) {
        const txtTitle = useRef()
        const hexColor = useRef()
    
        const submit = e => {
            e.preventDefault()
            const title = txtTitle.current.value
            const color = hexColor.current.value
            onNewColor(title, color)
            txtTitle.current.value = ''
            hexColor.current.value = ''
        }
    
        return (
            <form onSubmit={submit}>
                <input ref={txtTitle} type="text" placeholder="color  title..." required />
                <input ref={hexColor} type="color" required />
                <button>ADD</button>
            </form>
        )
    }
    

DOM 노드의 value 값을 직접 ""로 설정함으로써 DOM 노드의 속성을 변경했다. 이런 코드는 명령형 코드이다. AddColorForm은 DOM을 통해 폼 값을 저장하기 때문에 제어되지 않는 컴포넌트가 된다. 하지만 제어가 되는 컴포넌트가 더 좋은 접근 방법이다.

6.5.2 제어가 되는 컴포넌트

  • 제어가 되는 컴포넌트에서는 폼 값을 DOM이 아니라 리액트로 관리 한다.
  • 제어가 되는 컴포넌트를 사용하면 튼튼한 폼 검증 기능 등의 추가가 훨씬 더 쉬워진다.
  • 제어가 되는 AddColorForm 만들기
    • 참조를 사용하는 대신 title, color 값을 리액트 상태를 통해 저장
    • setTItle, setColor 함수를 정의
    • form의 input 엘리먼트 안에 있는 value 속성을 설정해서 값을 표시 할 수 있음
    • input 엘리먼트에 문자를 입력할 때마다 상태 변수를 변경해 값을 변경
    • 폼을 제출할 때 onNewColor 함수 프로퍼티를 호출하면서 title, color의 상태값 전달
      • 폼 전달 이후 setTitle, setColor를 통해 값을 초기화 할수 있음
    import React, {useRef} from 'react'
    
    export default function AddColorForm({ onNewColor = f => f}) {
        const [title, setTitle] = useRef('')
        const [color, setColor] = useRef('#000000')
    
        const submit = e => {
            e.preventDefault()
            onNewColor(title, color)
            setTitle('')
            setColor('#000000')
        }
    
        return (
            <form onSubmit={submit}>
                <input value={title} onChange={event => setTitle(event.target.value)} type="text" placeholder="color  title..." required />
                <input value={color} onChange={event => setColor(event.target.value)} type="color" required />
                <button>ADD</button>
            </form>
        )
    }
    

리액트가 폼의 상태를 모두 제어하기 때문에 이런 컴포넌트를 제어가 되는 컴포넌트라고 부른다. 값을 바꿀 때마다 다시 랜터링 된다. 하지만 리액트는 이러한 부하를 처리할 수 있도록 설계됐으니 괜찮다. 다시 랜더린 된다는 사실만 기억하고 부하가 가는 처리는 가급적 하지 않는 것이 좋다. 리액트 컴포넌트를 최적화를 한다면 이런 부분에 대한 지식이 필요함

6.5.3 커스텀 훅 만들기

  • input 엘리먼트가 많이 들어 있는 큰 폼을 만든다면 다음 코드를 복사해 붙여 넣을 수 있다.
    • 이런 경우 함수에서 추상화 할 수 있는 중복이 발생했다고 볼수 있다.
    <input value={title} onChange={event => setTitle(event.target.value)} />
    
  • 중복이 되는 코드를 추상화한 useInput 훅
    • 커스텀 훅
    • useSstate 훅을 이용해 상태 value를 만듬
    • value와 상태의 value를 변경하는 onChange 함수 프로퍼티가 배열의 첫 번째 원소
    • value의 값을 초기값으로 재설정하는 두번째 원소
    import { useState } from 'react'
    
    export default function({initValue}) {
        const [value, setValue] = useState(initValue)
        return [
            { value, onChange: e => setValue(e.target.value) },
            () => setValue(initValue)
        ]
    }
    
  • useInput 훅을 사용해 AddColorForm 안에서 사용
    • useState훅은 useInput 훅 안에 캡슐화된다.
    • 커스텀 훅이 반환하는 배열의 첫 번째 원소를 구조 분해해서 title, color 값을 얻는다.
    • 커스텀 훅으로부터 프로퍼티를 스프레드 연산으로 넣는다.
    • useInput 함수가 반환하는 커스텀 재설정 함수를 사용해 값을 재설정
    import React, {useRef} from 'react'
    import useInput from './useInput'
    
    export default function AddColorForm({ onNewColor = f => f}) {
        const [titleProps, resetTitle] = useInput('')
        const [colorProps, resetColor] = useInput('#000000')
    
        const submit = e => {
            e.preventDefault()
            onNewColor(titleProps.value, colorProps.value)
            resetTitle()
            resetColor()
        }
    
        return (
            <form onSubmit={submit}>
                <input {...titleProps} type="text" placeholder="color  title..." required />
                <input {...colorProps}  type="color" required />
                <button>ADD</button>
            </form>
        )
    }
    

6.5.4 색을 상태에 추가하기

  • 새로운 색이 추가되면 onNewColor 프로퍼티가 호출
    • colors의 스프레드 연산을 통해 새로운 배열을 만들고 값을 추가 한다.
    • uuid 패키지에 있는 v4함수를 사용해 색의 새로운 id 를 생성
    • setColors를 호출해서 색 배열을 상태에 저장
import './App.css';
import React, { useState } from 'react'
import colorData from './color-data.json'
import ColorList from './components/ColorList';
import AddColorForm from './components/AddColorForm'
import {v4} from 'uuid'

export default function App() {
  const [colors, setColors] = useState(colorData)
  return (
    <>
      <AddColorForm 
        onNewColor={(title, color) => {
          const newColors = [
            ...colors,
            {
              id: v4(),
              rating: 0,
              title,
              color
            }
          ]
          setColors(newColors)
        }}
      />
      <ColorList 
        colors={colors}
        onRemoveColor={id => {
          const newColors = colors.filter(color => color.id !== id)
          setColors(newColors)
        }}
        onRateColor={(id, rating) => {
          const newColors = colors.map(color => color.id === id ? {...color, rating} : color)
          setColors(newColors)
        }}
      />
    </>
  )
}

6.6 리액트 콘텍스트

  • 트리 루트의 한 위치에서 상태를 저장하는 패턴은 리액트의 초기 버전이 성공할 수 있는 이유
  • 리액트가 진화하고 컴포넌트 트리가 커짐에 따라, 상태를 한 군데 유지하는 원직을 따르는 것이 점점 어려워짐
    • 트리 구조가 복잠해짐에 따라 하위 컴포넌트로 전달해야하는 코드들이 많아짐
    • 코드의 크기 증가 밎 UI 규모 확장이 어려움
    • 이런 구조로 인해 유지보수가 어려워짐
  • 리액트 콘텍스트는 데이터를 위한 제트기와 같다
  • 콘텍스트 프로바이더는 컴포넌트 트리 전체나 일부를 감싸는 이랙트 컴포넌트
  • 콘텍스트 프로바이더는 데이터 허브이기도 하다
  • 목적지를 콘텍스트 소비자라고 한다

콘텍스트를 사용하면 데이터를 한 위치에 저장할 수 있지만, 데이터를 사용하지 않는 여러 컴포넌트를 거쳐서 최종 컴포넌트에 전달할 필요가 없어진다.

6.6.1 콘텍스트에 색 넣기

  • 리액트에서 콘텍스트를 사용하려면 먼저 콘텍스트 프로바이더에게 데이터를 넣고, 프로바이더를 컴포넌트 트리에 추가 해야함
  • 리액트에는 새로운 콘텍스트 객체를 만들 때 쓰는 createContext라는 함수가 있다.
  • 만들어진 콘텍스트 객체에는 콘텍스트 Provider와 콘텍스트 Consumer라는 2가지 컴포넌트가 있다.
  • color-data.json 파일에서 찾은 디폴트 색 정보를 콘텍스트에 넣자 (index.js 파일)
    • createContext를 사용해 새로운 리액트 콘텍스트를 만들고 ColorContex로 정의
    • ColorContext.Provider와 ColorContext.Consumer라는 2가지 컴포넌트가 있다.
    • 색의 상태를 넣기 위해서는 Provider의 value를 설정하면 콘텍스트에 데이터 추가 가능
    import React, { createContext } from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    import colorData from './color-data.json';
    
    export const ColorContext = createContext()
    
    ReactDOM.render(
      
        
      ,
      document.getElementById('root')
    );
    
    // If you want to start measuring performance in your app, pass a function
    // to log results (for example: reportWebVitals(console.log))
    // or send to an analytics endpoint. Learn more: <https://bit.ly/CRA-vitals>
    reportWebVitals();
    
  • App 컴포넌트는 더이상 상태를 보관하고 하위 컴포넌트에 전달할 필요가 없다.
    • 더이상 데이터에 신경쓸 필요 없다.
    import './App.css';
    import React from 'react'
    import ColorList from './components/ColorList';
    import AddColorForm from './components/AddColorForm'
    
    export default function App() {
      return (
        <>
          <AddColorForm />
          <ColorList />
        </>
      )
    }
    

6.6.2 useContext를 통해 색 얻기

  • useContext 훅을 사용해 콘텍스트에서 값을 얻을 수 있다.
    • 콘텍스트에서 colors를 가져오기 때문에 colors=[] 프로퍼티를 제거
    • 프로바이더를 추가했던 index.js 파일로부터 ColorContext 인스턴스를 import 한다.
    import React, {useContext} from 'react'
    import Color from './Color'
    import { ColorContext } from '../' 
    
    export default function ColorList() {
        const { colors } = useContext(ColorContext)
        if (!colors.length) return <div>표시할 색이 없습니다.</div>
        return (
            <div>
                {
                    colors.map(color => <Color key={color.id} {...color} />)
                }
            </div>
        )
    }
    

6.6.3 상태가 있는 콘텍스트 프로바이더

  • 콘텍스트 프로바이더는 객체를 콘텍스트에 넣을 수 있다. 하지만 프로바이더 자체로는 콘텍스상에 들어 있는 값을 변경할 수 없다.
  • 부모 컴포넌트의 도움을 받아야 값을 변경할 수 있다.
  • 상태가 있는 컴포넌트의 상태가 변경되면 컴포넌트가 새로운 콘텍스트 데이터를 가지고 콘텍스트 프로바이더를 다시 랜더링한다.
  • 콘텍스트 프로바이더를 랜더링하느 상태가 있는 컴포넌트는 우리가 만드는 커스텀 프로바이더 이다.
  • 프로바이더와 함께 App을 감싸야 할 때 이 컴포넌트를 사용해야 한다는 뜻이다.
  • ColorProvider를 만들자
    • ColorProvider는 ColorContext.Provider를 랜더링하는 컴포넌트 이다.
    • useState를 사용해 colors라는 상태 변수를 만든다.
    • 초기 데이터는 color-data.json에서 가져온다.
    • ColorProvider는 상태에서 얻은 colors를 ColorContext.Provider의 value 프로퍼티를 통해 콘텍스트에 설정
    • setColors를 콘텍스트에 추가하는 것이 가장 좋은 생각은 아닐 수 있다.
      • 콘텍스트에 setColors를 추가하면 다른 개발자가 나중에 이 함수를 사용하면서 실수를 할 여지가 있다.
      • 정해진 함수만을 노출하는 것이 좋다
      • setColors 함수를 소비자에게 노출하지 않도록 수정
    import React, { createContext, useState } from 'react'
    import colorData from '../color-data.json'
    import { v4 } from 'uuid'
    
    const ColorContext = createContext()
    
    export default function ColorProvider({ children }) {
        const [colors, setColors] = useState(colorData)
        const addColor = (title, color) => {
            setColors([
                ...colors,
                {
                    id: v4(),
                    rating: 0,
                    title,
                    color
                }
            ])
        }
    
        const rateColor = (id, rating) => {
            setColors(colors.map(color => (color.id === id ? {...color, rating} : color )))
        }
    
        const removeColor = id => setColors(colors.filter(color => color.id !== id));
    
        return (
            <ColorContext.Provider value={{ colors, addColor, removeColor, rateColor }}>
                {children}
            </ColorContext.Provider>
        )
    }
    

6.6.4 콘텍스트와 커스텀 훅

  • ColorContext 인트턴스를 노출하는 대신, 콘텍스트에서 색을 반환해주는 useColors 훅을 만들 수 있다.
    • 필요한 기능을 한개의 자바스크립트 모듈로 감쌌다.
    import React, { createContext, useContext, useState } from 'react'
    import colorData from '../color-data.json'
    import { v4 } from 'uuid'
    
    const ColorContext = createContext()
    export const useColors = () => useContext(ColorContext)
    
    export function ColorProvider({ children }) {
        const [colors, setColors] = useState(colorData)
        const addColor = (title, color) => {
            setColors([
                ...colors,
                {
                    id: v4(),
                    rating: 0,
                    title,
                    color
                }
            ])
        }
    
        const rateColor = (id, rating) => {
            setColors(colors.map(color => (color.id === id ? {...color, rating} : color )))
        }
    
        const removeColor = id => setColors(colors.filter(color => color.id !== id));
    
        return (
            <ColorContext.Provider value={{ colors, addColor, removeColor, rateColor }}>
                {children}
            </ColorContext.Provider>
        )
    }
    
  • index.js 파일에서도 ColorProvider로 감싸야 한다.
  • import React, { createContext } from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { ColorProvider } from './components/ColorProvider' ReactDOM.render( , document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: <https://bit.ly/CRA-vitals> reportWebVitals();
  • ColorList 컴포넌트 화면에서 색을 표시하기 위해 colors 배열에 접근 해야한다.
  • import React, {useContext} from 'react' import Color from './Color' import { useColors } from './ColorProvider' export default function ColorList() { const { colors } = useColors() if (!colors.length) return <div>표시할 색이 없습니다.</div> return ( <div> { colors.map(color => <Color key={color.id} {...color} />) } </div> ) }
  • Color 컴포넌트는 훅을 사용해 색을 제거하고 색의 평점을 매기는 함수를 직접 얻을 수 있다.
    • Color 컴포넌트는 더이상 함수를 상위 컴포넌트로 이벤트를 전달하지 않아도 된다.
    import React from 'react'
    import { FaTrash } from 'react-icons/fa'
    import StarRating from './StartRating'
    import { useColors } from './ColorProvider'
    
    export default function Color({ id, title, color, rating }) {
        const { rateColor, removeColor } = useColors()
        return (
            <section>
                <h1>{title}</h1>
                <button onClick={() => removeColor(id)}>
                    <FaTrash />
                </button>
                <div style={{ height: 50, backgroundColor: color }} />
                <StarRating selectedStarts={rating} onRate={rating => rateColor(id, rating)} />
            </section>
        )
    }
    
  • AddColorForm 컴포넌트는 addColor 함수를 통해 색을 직접 추가할 수 있다.
  • import React from 'react' import useInput from './useInput' import { useColors } from './ColorProvider' export default function AddColorForm() { const [titleProps, resetTitle] = useInput('') const [colorProps, resetColor] = useInput('#000000') const { addColor } = useColors() const submit = e => { e.preventDefault() addColor(titleProps.value, colorProps.value) resetTitle() resetColor() } return ( <form onSubmit={submit}> <input {...titleProps} type="text" placeholder="color title..." required /> <input {...colorProps} type="color" required /> <button>ADD</button> </form> ) }

훅은 관심사를 분리 해주는 놀라운 도구이다. UI와 훅스는 따로 개발될 수 있고, 분리해 테스트될 수 있으며, 따로 배치 될 수도 있다.

728x90