리액트 상태관리

김동현·2022년 12월 20일
0

리액트

목록 보기
4/7

컴포넌트 트리 안의 상태


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

const colorData = [
  {
    id: 1,
    name: "red",
    rgb: "ff0000",
  },
  {
    id: 2,
    name: "blue",
    rgb: "0000ff",
  },
  {
    id: 3,
    name: "green",
    rgb: "008000",
  },
];

function App() {
  const [colors] = useState(colorData); // colors 라는 상태를 생성
  return <ColorList colors={colors} />; // 상태를 ColorList 컴포넌트로 전달
}

function ColorList({ colors = [] }) { // 전달받은 colors 상태 ( 에러방지용 기본값 세팅)
  if (!colors.length) return <div>no color</div>;
  return (
    <div>
      {colors.map((color) => ( // 상태를 Color 컴포넌트로 전달
        <Color key={color.id} {...color} /> // spread 문법을 이용해 color 객체의 모든 프로퍼티를 전달
      ))}
    </div>
  );
}

function Color({ name, rgb }) { // 전달받은 props 중에 name과 rgb만 선택적으로 받음
  return (
    <div
      style={{
        border: "1px solid black",
        margin: "20px",
        padding: "20px",
        color: `#${rgb}`,
      }}
    >
      <p>color name : {name}</p>
      <p>color rgb : {rgb}</p>
    </div>
  );
}

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

삭제 상호작용을 부모 컴포넌트로 보내기

const colorData = [
  {
    id: 1,
    name: "red",
    rgb: "ff0000",
  },
  {
    id: 2,
    name: "blue",
    rgb: "0000ff",
  },
  {
    id: 3,
    name: "green",
    rgb: "008000",
  },
];

function App() {
  const [colors, setColors] = useState(colorData); // 상태를 조작하는 함수 추가
  const removeColor = (id) => { // id 인자를 받아서 삭제하는 상태 제어 함수
    const newColors = colors.filter((color) => color.id !== id);
    setColors(newColors);
  };
  return <ColorList colors={colors} removeColor={removeColor} />; // 제어 함수도 같이 내려보낸다
}

function ColorList({ colors = [], removeColor = (f) => f }) {
  if (!colors.length) return <div>no color</div>;
  return (
    <div>
      {colors.map((color) => (
        <Color key={color.id} {...color} onRemove={removeColor} /> // 한 번 더 내려보낸다.
      ))}
    </div>
  );
}

function Color({ id, name, rgb, onRemove = (f) => f }) { // id 값도 같이 전달받는다.
  return (
    <div
      style={{
        border: "1px solid black",
        margin: "20px",
        padding: "20px",
        color: `#${rgb}`,
      }}
    >
      <p>color name : {name}</p>
      <p>color rgb : {rgb}</p>
      <button onClick={() => onRemove(id)}>삭제</button> // 해당 id로 함수를 호출한다.
    </div>
  );
}

폼 ( form ) 만들기

폼 엘리먼트에서 사용되는 value 값을 리액트에서는 어떻게 가져올까?
2가지가 있다.
하나는 참조를 사용하는 것이고 다른 하나는 상태를 이용하는 것이다.

참조 ( ref ) 사용하기

function App(){
  const txtTitle = useRef();
  const sendValue = value => console.log(value); // 값을 서버에 보내는 동작을 console.log로 대체한다.
  const submit = e => {
    e.preventDefault();
    const title = txtTitle.current.value; // txtTitle.current 는 DOM 노드를 참조한다.
    sendValue(title);
    txtTitle.current.value = ""; // 제출하고나서 빈 칸이 나오도록 한다.
  }  
  return (
    <form onSubmit={submit}>
      <input type="text" ref={txtTitle} />
      <button>제출</button> {/*버튼 태그는 기본 동작으로 submit을 수행한다.*/}
    </form>
  );
}

하지만 DOM 노드를 참조하는 방법은 함수형 프로그래밍에 어긋난다.
부수효과를 발생시키기 때문이다.

제어가 되는 컴포넌트

제어가 되는 컴포넌트라는 말이 뜬금없이 왜 나오는 말인가 싶다.
위의 참조 형태를 사용한건 리액트 컴포넌트에서 제어하는게 아니라 DOM 노드를 직접 수정한 것이다.
즉, 제어가 되지 않는 컴포넌트인 것이다.
반면, 참조를 사용하지않고 리액트 컴포넌트만으로 폼 엘리먼트의 값을 핸들링한다면 제어가 되는 컴포넌트라고 부른다.

function App(){
  const [title, setTitle] = useState(); // input value 값을 담을 상태 변수이다.
  const sendValue = value => console.log(value); // 값을 서버에 보내는 동작을 console.log로 대체한다.
  const submit = e => {
    e.preventDefault();
    sendValue(title);
    setTitle(""); // 제출하고나서 빈 칸이 나오도록 한다.
  }
  return (
    <form onSubmit={submit}>
      <input type="text" value={title} onChange={e=>setTitle(e.target.value)}/>
      <button>제출</button>
    </form>
  );
}

제어가 되는 컴포넌트 방법을 이용한다면 리액트의 input 가상 엘리먼트에서 사용되는 onChange 이벤트때문에 렌더링이 심하게 발생하여 성능저하가 될 것같은 느낌이 든다.
리액트 만드는 사람들이 어련히 알아서 잘 동작하도록 설계했다고 한다.
리액트 공식문서에서도 폼 엘리먼트를 핸들링 하고싶다면 제어가 되는 컴포넌트를 이용하는 것을 권장하고 있다.

커스텀 Hook 만들기

제어가 되는 컴포넌트 방법을 이용하면 각각의 컴포넌트들은 같은 동작을 하는 부분들이 생긴다.
그 부분들을 모아서 하나의 Hook으로 만들어보자.

// 초기값을 인자로 전달받고 [{value, 이벤트리스너}, 초기화함수] 를 반환한다.
const useInput = initialValue => { 
  const [value, setValue] = useState(initialValue); /
  return [{value, onChange:e=>setValue(e.target.value)}, ()=>setValue(initialValue)];
}

function App(){
  const [titleProps, resetTitle] = useInput();
  const sendValue = value => console.log(value); // 값을 서버에 보내는 동작을 console.log로 대체한다.
  const submit = e => {
    e.preventDefault();
    sendValue(titleProps.value);
    resetTitle();
  }
  return (
    <form onSubmit={submit}>
      <input type="text" {...titleProps} />
      <button>제출</button>
    </form>
  );
}

리액트 Context

부모, 자식, 손자 컴포넌트가 다음과 같이 있다고 가정하자.

  • 부모
    • 자식
      • 손자1
      • 손자2

상태 변수는 부모 컴포넌트에 있는데 상호작용은 손자 컴포넌트에서 한다면 부모 컴포넌트에서 손자 컴포넌트로 상태 변수를 전달해야 한다.
이 과정에서 자식 컴포넌트는 상태 변수를 사용하지도 않는데 상태 변수를 부모에게 받고 자식에게 넘겨주는 징검다리 역할을 할 수 밖에 없다.

뭔가 빙 돌아가는 느낌쓰다.
다이렉트로 꽂아버리기는 방법이 있다.
Context 를 사용하면 된다.

Context에 값 넣고 빼기

Context는 Provider와 Consumer 두 가지로 나뉜다.
Provider는 값을 넣을 때 사용하고 Consumer는 값을 가져올 때 사용한다.
단, Consumer는 레거시 리액트에서만 직접 사용되었고 현재 대세인 함수형 컴포넌트에서는 useContext() 를 호출하여 간접 사용된다.
useContext() 내부적으로는 Consumer를 사용한다.

const ColorsContext = createContext(); // ColorsContext 라는 콘텍스트 컴포넌트를 만든다.

function Parent() {
  const [colors, setColors] = useState(["red", "green", "blue"]);
  return (
    <ColorsContext.Provider value={colors}> 
      {/* 상태 변수 colors를 ColorsContext에 넣는다. 
    	ColorsContext 컴포넌트의 하위 컴포넌트에서만 colors값을 사용할 수 있다.*/}
      <Child />
    </ColorsContext.Provider>
  );
}

function Child() {  // 부모로부터 전달 받지 않고 자식에게 전달 하지도 않는 클린한 컴포넌트다.
  return <GrandChild />;
}

function GrandChild() {
  const colors = useContext(ColorsContext); // ColorsContext 콘텍스트에서 값을 가져온다.
  console.log(colors);
  return (
    <>
      {colors.map((color, i) => (
        <p key={i}>{color}</p>
      ))}
    </>
  );
}

보통 부모 컴포넌트와 자식 컴포넌트, 손자 컴포넌트는 각각 다른 jsx파일로 만드는 경우가 많다.
그럴땐 ColorContext 컴포넌트를 부모 컴포넌트에서 export하고 손자 컴포넌트에서 import 해줘서 사용해야 한다.

상태가 있는 Context Provider

앞서 ColorsContext.Provider 의 value 프로퍼티에 colors 값만을 넣었다.
하지만 하위 컴포넌트에서 상태 변수인 colors 값을 수정하려면 setColors 함수 값도 넣으면 된다.

<ColorsContext.Provider value={{colors, setColors}}>

하위 컴포넌트에서 사용할때는 다음과 같이 사용할 수 있다.

const {colors, setColors} = useContext(ColorsContext);

Context 컴포넌트는 이렇게 값을 넣고 빼는 것은 할 수 있지만 Parent 컴포넌트처럼 상태변수를 만들거나 조작하지는 못한다.
하지만 Context Provider를 렌더링하는 추상 컴포넌트를 만들면 된다.
이를 커스텀 프로바이더라고 한다.

const ColorsContext = createContext(); 

function ColorsProvider({children}){
  const [colors, setColors] = useState(["red", "green", "blue"]); 
  return (
    <ColorsContext.Provider value={{colors, setColors}}>
      {children}
    </ColorsContext.Provider>
  );
}

function Parent() {
  return (
    <ColorsProvider>
      <Child />
    </ColorsProvider>
  );
}

ColorsContext() 함수 컴포넌트의 매개변수로 children 프로퍼티를 전달받았다.
이전에 배웠던 리액트 엘리먼트의 생김새를 다시 상기해보자.

{
  $$typeof:Symbol(React.element),
  "type": "h1",
  "Key": null,
  "ref": null,
  "props": {id:"recipe-0", children: "연어 스테이크"},
  "_owner": null,
  "_store": {}
}

여기서 주목할 부분은 props 부분이다.
함수 컴포넌트의 호출 ( JSX를 사용 ) 시 매개변수로 props 프로퍼티가 전달된다.
위의 ColorsProvider 컴포넌트는 start tag와 end tag 사이에 어트리뷰트가 없으므로 props가 다음과 같이 구성될 것이다.

props: {children: React.createElement(Child, null, null)}

함수 컴포넌트의 매개변수로 props가 전달되니 코드상으로 아래와 같이 작성될 수 있다.

function ColorsProvider(props){ // props가 전달받는다.
  const [colors, setColors] = useState(["red", "green", "blue"]); 
  return (
    <ColorsContext.Provider value={{colors,setColors}}>
      {props.children}	{/* props 프로퍼티 안에 children 컴포넌트가 들어있다.*/}
    </ColorsContext.Provider>
  );
}

자바스크립트의 구조 분해 할당을 이용해서 props.children{childrend}로 바꿨을 뿐이다.

Context 컴포넌트에 setColors 함수를 전달하는 건 보안상에 좋지가 않다.
colors 변수가 택도 없는 값으로 변경될 수도 있기 때문이다.
따라서 ColorsProvider 내부에서 addColors, removeColors, updateColors 와 같이 정해진 동작만을 수행하도록 하는 함수를 작성하고 그 함수들만을 Context 컴포넌트에 전달하는 것이 좋다.

Context와 커스텀 Hook

Context Provider와 Context Consumer를 정의하는 파일은 서로 다른 파일일 것이다.
그러므로 export와 import를 활용해서 Context 컴포넌트를 전달할 것이다.
즉, Consumer 컴포넌트에게 Context를 노출해야한다는 것이다.
커스텀 훅을 사용해 Consumer 컴포넌트에게 Context가 노출되지 않도록 만들자.

const ColorsContext = createContext(); // ColorsContext 라는 콘텍스트 컴포넌트를 만든다.
export const useColors = () => useContext(ColorsContext); // 동일한 파일에 useColors() 훅을 만들고 export한다.

Consumer 컴포넌트는 useColors Hook만 import 해서 사용하면 된다.

const colors = useColors();

[ 참고 ] : 러닝 리액트 2판

profile
프론트에_가까운_풀스택_개발자

0개의 댓글