[React] 6. 상태 관리, Reducer, Immer, Form으로 만드는 방법, 컴포넌트의 재사용

ㅎㅎ·2023년 8월 1일
0

React

목록 보기
8/11

협업에서의 문제를 다뤄봄 꼼꼼하게 정리를 해보길

🔥 마우스 따라 가기

직접 해보기

참고 사이트:
https://www.w3schools.com/jsref/event_onmousemove.asp
https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_onmousemove_dom

  • AppXY.jsx
import React, { useState } from 'react';
import './AppXY.css';

export default function AppXY() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);
  const handleMove = (e) => {
    setX(e.clientX);
    setY(e.clientY);
  };

  return (
    <div
      className='container'
      onMouseMove={handleMove}
    >
      <div
        className='pointer'
        style={{position: "absolute", left: x, top: y}}
      />
    </div>
  );
}
body, html, div {
  height: 100%;
  width: 100%;
}

.container {
  height: 100%;
  width: 100%;
  background-color: orange;
  position: relative;
}

.pointer {
  height: 1rem;
  width: 1rem;
  background-color: red;
  border-radius: 100%;
}

와~~~ 해냄

더 찾아보면서 코드를 줄이는 방법을 알게되었다.

  • AppXY.jsx
import React, { useState } from 'react';
import './AppXY.css';

export default function AppXY() {
  const [xy, setXY] = useState({x: 0, y: 0});
  const handleMove = (e) => {
    setXY({x: e.clientX, y: e.clientY});
  };

  return (
    <div
      className='container'
      onMouseMove={handleMove}
    >
      <div
        className='pointer'
        style={{position: "absolute", left: xy.x, top: xy.y}}
      />
    </div>
  );
}
  • AppXY.css
.container {
  height: 100vh;
  width: 100vh;
  background-color: orange;
  position: relative;
}

.pointer {
  height: 1rem;
  width: 1rem;
  background-color: red;
  border-radius: 100%;
}

vh는 창사이즈에 맞는 퍼센트

참고용 코드 (1) - 기본 구현

리액트보단 브라우저 이벤트에 대한 지식이 필요했던 예제다.

검색 방법:
react pointer event 로 검색하기

  • AppXY.jsx
import React, { useState } from 'react';
import './AppXY.css';

export default function AppXY() {
  const [xy, setXY] = useState({x: 0, y: 0});

  return (
    <div className='container' onPointerMove={(e) => {
      setXY({x: e.clientX, y: e.clientY});
    }}>
      <div
        className='pointer'
        style={{position: "absolute", left: xy.x, top: xy.y}}
      />
    </div>
  );
}

오잉 마우스 무브가 아니라 포인터 무브를 사용하네

참고용 코드 - 개선하기

  • AppXY.jsx
import React, { useState } from 'react';
import './AppXY.css';

export default function AppXY() {
  const [position, setPosition] = useState({x: 0, y: 0});

  return (
    <div className='container' onPointerMove={(e) => {
      setPosition({x: e.clientX, y: e.clientY});
    }}>
      <div
        className='pointer'
        style={{position: "absolute", left: position.x, top: position.y}}
      />
    </div>
  );
}

만약 수평으로만 이동이 가능하다면?

import React, { useState } from 'react';
import './AppXY.css';

export default function AppXY() {
  const [position, setPosition] = useState({x: 0, y: 0});

  return (
    <div className='container' onPointerMove={(e) => {
      // setPosition({x: e.clientX, y: e.clientY});
      // 만약 수평으로만 이동이 가능하다면?
      setPosition(prev => ({x: e.clientX, y: prev.y}));
    }}>
      <div
        className='pointer'
        style={{position: "absolute", left: position.x, top: position.y}}
      />
    </div>
  );
}

setPosition(prev => ({ ...prev, x: e.clientX}));
… 모든 것들을 그대로 돌리고 x만 바꾼다

🔥 중첩객체 상태 관리 (멘토바꾸기)

직접 해보기

setState는 중첩된 객체의 업데이트를 지원하지 않는다. 그래서 그 중첩된 리스트자체를 수정해서 리스트를 넣어주는 것.

Object.assign({}, person.mentor);

열거 가능한 자체 속성을 복사해 대상 객체에 붙여넣는다.

  • AppMentor.jsx
import React, { useState } from 'react';

export default function AppMentor(props) {
  const [person, setPerson] = useState({
    name: '엘리',
    title: '개발자',
    mentor: { // 중첩된 객체
      name: '밥',
      title: '시니어개발자',
    },
  });
  return (
    <div>
      <h1>
        {person.name}{person.title}
      </h1>
      <p>
        {person.name}의 멘토는 {person.mentor.name} ({person.mentor.title})
      </p>
      <button
        onClick={() => {
          const name = prompt(`what's your mentor's name?`);
          let tempList = Object.assign({}, person.mentor);
          tempList.name = name;
          setPerson(prev => ({...prev, mentor: tempList}));
        }}
      >
        멘토 이름 바꾸기
      </button>
      <button
        onClick={() => {
          const title = prompt(`what's your mentor's title?`);
          let tempList = Object.assign({}, person.title);
          tempList.title = title;
          setPerson(prev => ({...prev, mentor: tempList}));
        }}
      >
        멘토 타이틀 바꾸기
      </button>
    </div>
  );
}

참고용 코드

setPerson(person => ({...person, mentor: {...person.mentor, name}}));
아하!! 그냥 이렇게 넣어도 되는거구나!!

  • AppMentor.jsx
import React, { useState } from 'react';

export default function AppMentor(props) {
  const [person, setPerson] = useState({
    name: '엘리',
    title: '개발자',
    mentor: { // 중첩된 객체
      name: '밥',
      title: '시니어개발자',
    },
  });
  return (
    <div>
      <h1>
        {person.name}{person.title}
      </h1>
      <p>
        {person.name}의 멘토는 {person.mentor.name} ({person.mentor.title})
      </p>
      <button
        onClick={() => {
          const name = prompt(`what's your mentor's name?`);
          setPerson(person => ({...person, mentor: {...person.mentor, name}}));
        }}
      >
        멘토 이름 바꾸기
      </button>
      <button
        onClick={() => {
          const title = prompt(`what's your mentor's title?`);
          setPerson(person => ({...person, mentor: {...person.mentor, title}}));
        }}
      >
        멘토 타이틀 바꾸기
      </button>
    </div>
  );
}

🔥 배열 상태 관리

배열의 인덱스는 키값으로 쓰길 추천하지 않음

직접 해보기

  • AppMentor.jsx
import React, { useState } from 'react';

export default function AppMentor() {
  const [person, setPerson] = useState({
    name: '엘리',
    title: '개발자',
    mentors: [
      {
        name: '밥',
        title: '시니어개발자',
        index: '0'
      },
      {
        name: '제임스',
        title: '시니어개발자',
        index: '1'
      },
    ],
  });
  return (
    <div>
      <h1>
        {person.name}{person.title}
      </h1>
      <p>{person.name}의 멘토는:</p>
      <ul>
        {person.mentors.map((mentor) => (
          <li key={mentor.index}>
            {mentor.name} ({mentor.title})
          </li>
        ))}
      </ul>
      <button
        onClick={() => {
          let prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
          while(true) {
            if(person.mentors.find(mentor => mentor.name === prev) === undefined) { // 없는 이름일 때
              alert('존재하지 않는 이름입니다. 다시 입력해주세요.');
            } else { break; }
            prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
          }
          
          const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
					// 따로 mentors 부분만 복사해와서 고친 다음에 setPerson으로 넣음
          let tempPerson = Object.assign(person.mentors);
          tempPerson.map((mentor, index) => { // 돌면서 해당 부분의 이름을 바꿔줌
            if(mentor.name === prev) {
              tempPerson[index].name = current;
            }
          });
          setPerson({...person, mentors: tempPerson});
        }}
      >
        멘토의 이름을 바꾸기
      </button>
    </div>
  );
}

헤헤 해결

참고용 코드

  • AppMentor.jsx
import React, { useState } from 'react';

export default function AppMentor() {
  const [person, setPerson] = useState({
    name: '엘리',
    title: '개발자',
    mentors: [
      {
        name: '밥',
        title: '시니어개발자',
        index: '0'
      },
      {
        name: '제임스',
        title: '시니어개발자',
        index: '1'
      },
    ],
  });
  return (
    <div>
      <h1>
        {person.name}{person.title}
      </h1>
      <p>{person.name}의 멘토는:</p>
      <ul>
        {person.mentors.map((mentor) => (
          <li key={mentor.index}>
            {mentor.name} ({mentor.title})
          </li>
        ))}
      </ul>
      <button
        onClick={() => {
          const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
          const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
          setPerson(person => ({
            ...person, 
            mentors: person.mentors.map((mentor) => {
              if(mentor.name === prev) {
                return {...mentor, name: current };
              }
              return mentor;
            })
          }));
        }}
      >
        멘토의 이름을 바꾸기
      </button>
    </div>
  );
}

읭 근데 이건 없는 경우를 해결해주지 않네

왜이렇게 빙글빙글 돌아가면서 해야하는가?

리액트에서 가지고 있는 상태는 불변성을 유지해야한다. 한 번 만들어지면 변경하면 안되게 만들어야한다. 변경해야한다면 새로운 값 새로운 배열 새로운 객체로 만들어주어야한다.

ㅇㅎㅇㅎ

person.mentors[0].name = current; 이렇게 하면 안되는 이유는?
ㄴ 이렇게 해도 업데이트 되지 않기 때문 setPerson으로 해야함
똑같은 객체에서 (참조값이 같은 상태에서) 값을 아무리 변경해봤자 같은 걸로 인지해서 바꿔주지 않음

🔥 멘토 추가/삭제하기

직접 해보기

삭제 기능 해결

<button
        onClick={() => {
          const deleteName = prompt(`삭제할 멘토의 이름을 입력해주세요.`);
          setPerson(person => ({
            ...person, 
            mentors: person.mentors.filter((mentor) => mentor.name != deleteName)
          }));
        }}
      >멘토 삭제하기</button>

추가는…… 모르게써

<button
        onClick={() => {
          const newName = prompt(`추가할 멘토의 이름을 입력해주세요.`);
          const newTitle = prompt(`추가할 멘토의 역할을 입력해주세요.`);
          const tempList = {
            name: newName,
            title: newTitle
          };
          console.log(tempList);
          setPerson(person => ({
            ...person, mentors: Object.assign(person.mentors.map(mentor => mentor), tempList)
          }));
        }}
      >멘토 추가하기</button>

안 됨 ㅠㅠ

참고용 코드

삭제하기

<button
        onClick={() => {
          const name = prompt(`누구를 삭제하고 싶은가요?`);
          setPerson(person => ({
            ...person, 
            mentors: person.mentors.filter((m) => m.name !== name)
          }));
        }}
      >멘토 삭제하기</button>

왕 삭제하기는 똑같애

추가하기

<button
        onClick={() => {
          const name = prompt(`멘토의 이름은?`);
          const title = prompt(`멘토의 직함은?`);
          setPerson(person => ({
            ...person, 
            mentors: [...person.mentors, {name, title}],
          }));
        }}
      >멘토 추가하기</button>

멘토는 배열 스프레드 연산자를 배열에 사용하면 요소가 하나씩 풀어짐
ㄴ 그렇구나… 이거 때문에 그렇게 고생했는데 그냥 … 이면 모든 것이 해결 되는 것이었음

코드 정리

import React, { useState } from 'react';

export default function AppMentor() {
  const [person, setPerson] = useState(initialPerson);
  const handleUpdate = () => {
    const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
    const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
    setPerson(person => ({
      ...person, 
      mentors: person.mentors.map((mentor) => {
        if(mentor.name === prev) {
          return {...mentor, name: current };
        }
        return mentor;
      })
    }))
  };
  const handleAdd = () => {
    const name = prompt(`멘토의 이름은?`);
    const title = prompt(`멘토의 직함은?`);
    setPerson(person => ({
      ...person, 
      mentors: [...person.mentors, {name, title}],
    }));
  };
  const handleDelete = () => {
    const name = prompt(`누구를 삭제하고 싶은가요?`);
    setPerson(person => ({
      ...person, 
      mentors: person.mentors.filter((m) => m.name !== name)
    }));
  };

  return (
    <div>
      <h1>
        {person.name}{person.title}
      </h1>
      <p>{person.name}의 멘토는:</p>
      <ul>
        {person.mentors.map((mentor, index) => (
          <li key={index}>
            {mentor.name} ({mentor.title})
          </li>
        ))}
      </ul>
      <button onClick={handleUpdate}>멘토의 이름을 바꾸기</button>
      <button onClick={handleAdd}>멘토 추가하기</button>
      <button onClick={handleDelete}>멘토 삭제하기</button>
    </div>
  );
}

const initialPerson = {
  name: '엘리',
  title: '개발자',
  mentors: [
    {
      name: '밥',
      title: '시니어개발자',
    },
    {
      name: '제임스',
      title: '시니어개발자',
    },
  ],
};

정적인 데이터는 아래에 함수는 위에

상태관리 라이브러리에 대해

이러한 상태들은 컴포넌트 안에서만 사용할 수 있음
최신 버전에서는 contextAPi를 사용하면 공통적으로 상태관리를 할 수 있다.

Reducer 사용해보기

peson의 action에 따라서 하는 함수를 만드는 것

const [person, dispatch] = useReducer(personReducer, initialPerson);

초기값은 이니셜펄슨

dispatch를 이용해서 원하는 명령을 이용할 수 있음
person은 객체를 받고 action은 넘겨준걸 받아옴

  • person-reducer.js
export default function personReducer(person, action) {
  switch(action.type) {
    case 'updated': {
      const {prev, current} = action;
      return {
        ...person, 
        mentors: person.mentors.map((mentor) => {
          if(mentor.name === prev) {
            return {...mentor, name: current };
          }
          return mentor;
        }),
      };
    }
    case 'added': {
      const {name, title} = action;
      return {
        ...person, 
        mentors: [...person.mentors, {name, title}], 
      };
    }
    case 'deleted': {
      return {
        ...person, 
        mentors: person.mentors.filter((mentor) => mentor.name !== action.name),
      };
    }
    default: {
      throw Error(`알 수 없는 액션 타입니다: ${action.type}`);
    }
  }
}
  • AppMentor.jsx
import React, { useReducer } from 'react';
import personReducer from './reducer/person-reducer';

export default function AppMentor() {
  // const [person, setPerson] = useState(initialPerson);
  const [person, dispatch] = useReducer(personReducer, initialPerson);

  const handleUpdate = () => {
    const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
    const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
    dispatch({type: 'updated', prev, current});
  };
  const handleAdd = () => {
    const name = prompt(`멘토의 이름은?`);
    const title = prompt(`멘토의 직함은?`);
    dispatch({type: 'added', name, title});
  };
  const handleDelete = () => {
    const name = prompt(`누구를 삭제하고 싶은가요?`);
    dispatch({type: 'deleted', name});
  };

  return (
    <div>
      <h1>
        {person.name}{person.title}
      </h1>
      <p>{person.name}의 멘토는:</p>
      <ul>
        {person.mentors.map((mentor, index) => (
          <li key={index}>
            {mentor.name} ({mentor.title})
          </li>
        ))}
      </ul>
      <button onClick={handleUpdate}>멘토의 이름을 바꾸기</button>
      <button onClick={handleAdd}>멘토 추가하기</button>
      <button onClick={handleDelete}>멘토 삭제하기</button>
    </div>
  );
}

const initialPerson = {
  name: '엘리',
  title: '개발자',
  mentors: [
    {
      name: '밥',
      title: '시니어개발자',
    },
    {
      name: '제임스',
      title: '시니어개발자',
    },
  ],
};

Immer 사용해보기

아무리 리듀서를 사용해도 중첩된 객체가 많으면 많을 수록 …pesron으로 빙글빙글 돌면서 해야하는데
이걸 좀더 직관적으로하게 해주는게 immer

불변성 상태의 트리를 아주 손쉽게 변경할 수 있게 해주는 라이브러리

use-immer를 설치

usestate를 내부적으로 사용하고 있음

  • AppMentorsImmer.jsx
import React, { useState } from 'react';
import { useImmer } from 'use-immer';

export default function AppMentorsImmer() {
  const [person, updatePerson] = useImmer(initialPerson);

  const handleUpdate = () => {
    const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
    const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
    updatePerson((person) => {
      const mentor = person.mentors.find((m) => m.name === prev); // 이름이 맞는지 확인
      mentor.name = current;
    });
  };

  const handleAdd = () => {
    const name = prompt(`멘토의 이름은?`);
    const title = prompt(`멘토의 직함은?`);
    updatePerson((person) => {
      person.mentors.push({ name, title });
    });
  };

  const handleDelete = () => {
    const name = prompt(`누구를 삭제하고 싶은가요?`);
    updatePerson((person) => {
      const index = person.mentors.findIndex((m) => m.name === name);
      person.mentors.splice(index, 1); // 해당 인덱스의 아이템을 삭제
    });
  };
  return (
    <div>
      <h1>
        {person.name}{person.title}
      </h1>
      <p>{person.name}의 멘토는:</p>
      <ul>
        {person.mentors.map((mentor, index) => (
          <li key={index}>
            {mentor.name} ({mentor.title})
          </li>
        ))}
      </ul>
      <button onClick={handleUpdate}>멘토의 이름을 바꾸기</button>
      <button onClick={handleAdd}>멘토 추가하기</button>
      <button onClick={handleDelete}>멘토 삭제하기</button>
    </div>
  );
}

const initialPerson = {
  name: '엘리',
  title: '개발자',
  mentors: [
    {
      name: '밥',
      title: '시니어개발자',
    },
    {
      name: '제임스',
      title: '시니어개발자',
    },
  ],
};

Form을 만드는 법

  • AppForm.jsx (1)
import React, { useState } from "react";

export default function AppForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const handleSubmit = (e) => {
    e.preventDefault(); // 이게 없으면 submit 버튼 누르면 페이지가 리프레쉬 됨
  };
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">이름:</label>
      <input
        type="text"
        id="name"
        name={name}
        onChange={(e) => {
          setName(e.target.value);
        }}
      />
      <label htmlFor="email">이메일:</label>
      <input
        type="email"
        id="email"
        name={email}
        onChange={(e) => {
          setEmail(e.target.value);
        }}
      />
      <button>Submit</button>
    </form>
  );
}

이렇게 보다 하나로, 폼으로 관리하는 방법

  • AppForm.jsx (2)
import React, { useState } from "react";

export default function AppForm() {
  const [form, setForm] = useState({});
  const handleSubmit = (e) => {
    e.preventDefault(); // 이게 없으면 submit 버튼 누르면 페이지가 리프레쉬 됨
    console.log(form);
  };
  const handleChange = (e) => {
    // 이벤트에서 발생하는 타겟을 받아오기
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
  };
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">이름:</label>
      <input type="text" id="name" name={form.name} onChange={handleChange} />
      <label htmlFor="email">이메일:</label>
      <input
        type="email"
        id="email"
        name={form.email}
        onChange={handleChange}
      />
      <button>Submit</button>
    </form>
  );
}

폼에있는 인풋데이터는 사용자가 바로 수정가능하고 바로 확인가능하기 때문에 언컨트롤 컴포넌트 - 리액트의 추구 원칙과 어긋난다 항상 상태로부터 발생되어야한다

그렇기에 항상 이 상태를 이용해서 변경 될 때마다 업데이트 해줘야한다 폼은 객체를 이용해서 사용할 수 있다

  • AppForm.jsx (3)
import React, { useState } from "react";

export default function AppForm() {
  const [form, setForm] = useState({ name: "", email: "" });
  const handleSubmit = (e) => {
    e.preventDefault(); // 이게 없으면 submit 버튼 누르면 페이지가 리프레쉬 됨
    console.log(form);
  };
  const handleChange = (e) => {
    // 이벤트에서 발생하는 타겟을 받아오기
    const { name, value } = e.target; // e.target.name 과 e.target.value로 바인딩 됨
    setForm({ ...form, [name]: value });
  };
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">이름:</label>
      <input
        type="text"
        id="name"
        name="name"
        value={form.name}
        onChange={handleChange}
      />
      <label htmlFor="email">이메일:</label>
      <input
        type="email"
        id="email"
        name="email"
        value={form.email}
        onChange={handleChange}
      />
      <button>Submit</button>
    </form>
  );
}

컴포넌트의 재사용1 - (넵바)

  • AppWrap.jsx (1)
import React from "react";

export default function AppWrap() {
  return (
    <div>
      <Navbar />
    </div>
  );
}

function Navbar() {
  return (
    <header style={{ backgroundColor: "yellow" }}>
      <Avatar
        image="https://images.unsplash.com/photo-1574158622682-e40e69881006?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=580&q=80"
        name="han"
        size={200}
      />
    </header>
  );
}

function Avatar({ image, name, size }) {
  return (
    <div>
      <img
        src={image}
        alt={`${name}`}
        width={size}
        height={size}
        style={{ borderRadius: "50%" }}
      />
    </div>
  );
}

여기서 냅바는 재사용성이 좀 떨어진다 안에 글씨를 넣고 싶기도 빼고 싶기도 한데….

이럴 때 유용한게 웹 컴포넌트

  • AppWrap.jsx (2)
import React from "react";

export default function AppWrap() {
  return (
    <div>
      <Navbar>
        <Avatar
          image="https://images.unsplash.com/photo-1574158622682-e40e69881006?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=580&q=80"
          name="han"
          size={200}
        />
      </Navbar>
    </div>
  );
}

function Navbar({ children }) {
  return <header style={{ backgroundColor: "yellow" }}>{children}</header>;
}

function Avatar({ image, name, size }) {
  return (
    <div>
      <img
        src={image}
        alt={`${name}`}
        width={size}
        height={size}
        style={{ borderRadius: "50%" }}
      />
    </div>
  );
}

냅바 안의 내용이 children으로 전달이 됨

이렇게 했을 때의 장점 냅바를 내가 원하는 컨텐츠를 넣어ㅓㅅ 만들 수 있음

컴포넌트의 재사용2 - (카드)

  • AppCard.jsx
import React from "react";

export default function AppCard() {
  return (
    <>
      <Card>
        <p>Card1</p>
      </Card>

      <Card>
        <h1>Card2</h1>
        <p>설명</p>
      </Card>

      <Card>
        <article></article>
      </Card>
    </>
  );
}

function Card({ children }) {
  return (
    <div
      style={{
        backgroundColor: "black",
        borderRadius: "20px",
        color: "white",
        minHeight: "200px",
        maxWidth: "200px",
        margin: "1rem",
        padding: "1rem",
        textAlign: "center",
      }}
    >
      {children}
    </div>
  );
}
profile
Backend

0개의 댓글