22/12/30_리액트 프로젝트(2)

강해경·2022년 12월 30일

Today I Learned

목록 보기
29/36

로그인

회원가입을 통해 저장된 데이터에 맞게 이메일과 패스워드를 입력하면 로그인이 되도록 구현했습니다. 로그인 성공시 응답데이터에 담겨오는 이메일, 사용자이름, 프로필사진은 리코일을 통해 전역데이터에 저장하고 토큰은 만료시간을 설정하여 쿠키에 저장하도록 구현했습니다.

function LogIn() {
  const navigate = useNavigate();
  const [inputs, setInputs] = useState({
    email: '',
    password: '',
  });

  const setIsLoggedIn = useSetRecoilState(loginState);
  const [userInfo, setUserInfo] = useRecoilState(userInfoState);
  const { email, password } = inputs;
  const onChange = (event) => {
    console.log(event.target);
    const { value, name } = event.target;
    setInputs({
      ...inputs,
      [name]: value.trim(),
    });
  };
  const onSubmit = async (event) => {
    event.preventDefault();
    console.log(email);
    let json;
    try {
      const res = await fetch(`${authUrl}/login`, {
        method: 'POST',
        headers: HEADERS_USER,
        body: JSON.stringify({ email, password }),
      });
      json = await res.json();
      const {
        user: { displayName, profileImg },
        accessToken,
      } = json;
      setIsLoggedIn(true);
      setUserInfo({
        email,
        displayName,
        profileImg,
      });
      document.cookie = `accessToken=${accessToken}; path=/; max-age=${60 * 60 * 24}; secure`;
      navigate('/');
    } catch (error) {
      alert(json);
    }
  };
  return (
    <section className={style.section}>
      <Link to="/" className={style.header}>
        <h1 className={style.h1}>로그인</h1>
      </Link>

      <div className={style.formContainer}>
        <form onSubmit={onSubmit} className={style.form}>
          <div className={style.inputContainer}>
            <input
              className={style.input}
              name="email"
              placeholder="email"
              type="text"
              value={email}
              onChange={onChange}
              required
            ></input>
            <input
              className={style.input}
              name="password"
              placeholder="password"
              type="password"
              value={password}
              onChange={onChange}
              required
            ></input>
          </div>
          <input type="submit" value="로그인" className={`${style.input} ${style.btn}`} />
        </form>
      </div>
    </section>
  );
}

마이페이지

로그인 상태일 경우 헤더부분에 마이페이지 메뉴를 선택할 수 있도록 구현해 계좌정보와 주문내역을 관리할 수 있도록 구현했습니다. 마이페이지는 왼쪽에 프로필을 나타내는 부분과 오른쪽 계좌정보, 주문내역을 나타내는 부분을 컴포넌트화하여 세개로 나눠 작업했으면 계좌연결기능은 프로필 하단에 버튼을 만들어 모달창을 띄워 출력하였습니다.

주문내역 구매확정/취소/상세정보 버튼 관리

아직 상세정보 버튼은 구현이 안된 상태이지만 나름 고민을 조금해서 작성한 부분이면서도 동시에 이게 최선일까.. 분명 더 좋은 방법이 있을 거 같은데.. 하는 부분이라 자세하게 작성해보려 합니다.

주문관련 버튼 활용도를 높이기 위해 배열을 맵핑하고 컴포넌트화 시켜 주문관련 정보를 props로 전달하고 버튼을 클릭하면 해당버튼이 확정인지 취소인지 버튼의 이름속성을 구분하여 해당기능을 실행하도록 구현했습니다. 처음에는 버튼의 상태값을 각 버튼의 이름이 되도록 설정하고 setButton을 통해 각 버튼이 클릭될때마다 각 주문에대한 확정/취소 기능을 수행한 후 버튼상태값을 바꿔 useEffect의 dependency array에 버튼상태값을 넣어 버튼이 클릭된 후 주문내역을 refetch시켜주도록 구현했는데,

특정기능을 2번 연달아 수행할 경우 상태값의 변화가 없어서 주문내역이 refetch되지 않았습니다. 따라서 버튼에 할당되는 데이터를 참조형 데이터(배열)로 바꿔 버튼을 클릭할 때 마다 해당배열에 클릭한 버튼을 추가해주는 방식으로 변경했습니다. 더 좋은 방법이 있지않을까.. 궁금합니다🤷‍♀️

function MyOrder({ accessToken }) {
  const [button, setButton] = useState([]);
  const orderButton = ['구매확정', '구매취소', '상세정보'];
  const Button = ({ order, orderButton, handleClick }) => {
    return (
      <button className={style.button} onClick={handleClick} name={orderButton} data-id={order.detailId}>
        {orderButton}
      </button>
    );
  };

 useEffect(() => {
    const getOrderList = async () => {
      let json;
      try {
        const res = await fetch(`${API_URL}products/transactions/details`, {
          method: 'GET',
          headers: { ...HEADERS_USER, Authorization: accessToken },
        });
        json = await res.json();
        let sorted = [...json].sort((a, b) => new Date(b.timePaid) - new Date(a.timePaid));
        const exceptDelivery = sorted.filter((order) => order.product.title !== '배송비');
        setMyOrder([...exceptDelivery]);
      } catch {
        console.log(json);
      }
    };
    getOrderList();
  }, [button]);

  const handleClick = async (event) => {
    const menu = event.target.name;
    const detailId = event.target.dataset.id;
    if (menu === '구매확정') {
      const res = await fetch(`${API_URL}products/ok`, {
        method: 'POST',
        headers: { ...HEADERS_USER, Authorization: accessToken },
        body: JSON.stringify({ detailId }),
      });
      setButton([...button, '확정']);
      return;
    } else if (menu === '구매취소') {
      const res = await fetch(`${API_URL}products/cancel`, {
        method: 'POST',
        headers: { ...HEADERS_USER, Authorization: accessToken },
        body: JSON.stringify({ detailId }),
      });
      setButton([...button, '취소']);
      return;
    } else {
      console.log('상세정보');
    }
  };

  return (
    <section className={style.myOrder}>
      <h2 className={style.h2}>주문내역</h2>
      <hr />
      {myOrder ? (
        myOrder.map((order) => (
          <>
            <div className={style.list} key={order.detailId}>
              <div className={style.left}>
                <img src={order.product.thumbnail} alt={order.product.title} className={style.img} />
                <div className={style.text}>
                  <li className={style.title}>{order.product.title}</li>
                  <li>
                    {order.product.price.toLocaleString()}| {new Date(order.timePaid).toLocaleString()}
                  </li>
                  <li className={style.description}>
                    {order.done
                      ? '구매확정 상태입니다. 확정 후에는 취소가 불가합니다.'
                      : order.isCanceled
                      ? '결제 취소'
                      : `배송완료 후 구매확정 버튼을 눌러주세요.`}
                  </li>
                </div>
              </div>
              <div className={style.right} key={order.detailId}>
                {!order.done && !order.isCanceled ? (
                  orderButton.map((button, index) => (
                    <Button key={order.detailId + index} order={order} orderButton={button} handleClick={handleClick} />
                  ))
                ) : (
                  <Button order={order} orderButton={orderButton[2]} handleClick={handleClick} />
                )}
              </div>
            </div>
            <hr />
          </>
        ))
      ) : (
        <span>구매내역이 없습니다.</span>
      )}
    </section>
  );
}

계좌추가 연결 기능 구현

계좌연결 기능은 버튼을 클릭하면 마이페이지의 하위 페이지로 이동하고 해당 페이지에서 모달창이 나타나도록 구현하였습니다. 회원가입과 로그인과는 다르게 useForm 라이브러리를 사용해 보았는데,

유효성 검사와 에러메세지 부분에서 확실히 조금더 편하게 구현할 수 있다는 장점이 있다고 생각은 들었지만 굳이 라이브러리를 써야할까 하는 의문이 들어서 검색하던 중 https://tech.inflab.com/202207-rallit-form-refactoring/react-hook-form/#2-2-1-register 참고할만한 글을 읽어보았습니다. 제가 라이브러리의 기능과 장점을 절반도 모르고 사용한다는 생각이 들었습니다. 리팩토링할 때 코드리뷰를 반영하고 라이브러리 사용법을 익히기위해 여러 라이브러리를 도입해보려고 했는데 하기전에 좀더 사용하고자 하는 라이브러리에 대해 공부하는 시간을 가진 후 진행해야겠습니다.

1차 코드리뷰, 리팩토링 계획

  • 모듈/컴포넌트화
    api 함수를 따로 모듈화 하지않고 컴포넌트 내부에 전부 작성한 것에 대해 모듈화가 필요하다는 리뷰를 받았습니다. 3차과제에서도 반복되는 구문에 대해 코드리뷰를 받은 적이 있었는데, 기능을 구현할 때 항상 생각을 길게 하지 않고 코드부터 써내려 가다보니 모듈화/컴포넌트화가 제대로 되지 않는 문제점이 계속해서 발생한다는 생각이 들었습니다. 다른 조원들이 작성한 버튼이나 로딩화면 등 재사용을 염두에 두고 컴포넌트화 시킨 부분들을 보고 나도 반복되는 부분이나 재활용 가능한 부분을 고려하면서 코드를 작성해야겠다고 반성할 수 있었습니다. 활용도에 초점을 두고 반복되는 코드를 줄이는 방향으로 리팩토링을 진행해보려고 합니다.

  • 라이브러리 활용 마이그레이션
    개인적으로 회원가입에 있어서 그렇게 많은 input을 받고있지 않아 form태그의 기본 속성만으로도 관리가 가능하다고 판단하여 외부 라이브러리를 사용하지 않았습니다. use-hook-form 을 사용해 기본값도 지정하고 리팩토링을 해보면 좋을 거 같다는 코드리뷰를 받았고 라이브러리에 대해 더 공부하고 연습겸 마이그레이션을 해봐도 좋겠다는 생각이 들어서 시도해볼 계획입니다.

  • 토큰만료에 대한 대응
    서버에서 제공되는 토큰은 24시간 이후 만료되고 쿠키에도 24시간후에는 지워지지만 그에 따라 로그인 상태를 바꾸거나 토큰을 refresh해주는 기능은 없어서 토큰이 만료되어도 헤더나 마이페이지에는 유저정보가 남아있어 마치 로그인 상태처럼 보입니다. 이 부분은 axios interceptors를 이용해 해결해보려고 합니다.

  • refetching 관련 실수
    주문확정/취소 버튼을 클릭한 후 주문내역을 refetch하는 부분과 계좌연결 후 연결된 계좌 목록을 다시 불러오는 부분에 있어서 어떻게 구현하는 것인가 고민하게 되었습니다. 주문확정/취소 버튼은 동일한 페이지 내에서 일어나는 동작이라 useEffect에 button상태를 참조형 데이터로 만들어 버튼을 누를때 마다 refetch하도록 하였고, 계좌연결은 연결 후 원래 페이지도 돌아올 때 navigate함수가 아닌 window.location.href를 사용해 억지로 새로고침을 해주었는데요. 좋은 방법은 아닌것 같아 강사님께 여쭤보니 전체가 아닌 변경이 생긴 컴포넌트에 상태값을 변경시켜서 refetch하도록 해야겠다고 말씀해 주셔서 그부분도 함께 리팩토링 해볼 예정입니다. 그리고 button 상태를 참조형 데이터로 만들어 변화를 주는 방법외에 다른 방법이 있는지 좀 더 찾아봐야겠습니다.

2개의 댓글

comment-user-thumbnail
2022년 12월 30일

우와 열심히 하는 모습 정말 멋져요!!!
새로운 지식 많이 알아갑니다 화이팅 😊

1개의 답글