미새문지

24.08.30 day57 작업 일지 본문

개발 TIL

24.08.30 day57 작업 일지

문미새 2024. 8. 30. 23:49
728x90

redux toolkit을 설치해서 사용해봤는데, 확실히 예전에 redux가 너무 어려워서 못썼던거와는 달리 코드가 간편해져서 사용하기 좋아진 것 같다. 사용은 회원가입에 사용했는데 회원가입이 총 3페이지로 구성되어 있어 리덕스를 써서 전역으로 데이터를 관리할 상황이 생겨 사용하게 되었다.

toolkit은 redux를 사용하기 위한 코드들을 압축시켜 사용할 수 있게 해줬다. 

import { configureStore } from "@reduxjs/toolkit";
import signupReducer from "./slice/signupSlice";

const store = configureStore({
  reducer: {
    signup: signupReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export default store;

store는 리덕스의 전체 상태를 보관하는 객체이며 모든 상태가 store에 저장된다. 그래서 리덕스에서 데이터를 받아오거나 저장할 때 store에 접근해야 한다.

리덕스의 configureStore를 임포트해서 store를 생성시키고 그 안에 어떤 Reducer를 사용할 지 작성해주면 된다. 본인은 현재 회원가입에 리덕스를 사용하고 있어서 signupReducer를 만들어줬다.

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

interface SignupState {
  email: string;
  password: string;
  passwordConfirm: string;
  nickname: string;
}

const initialState: SignupState = {
  email: "",
  password: "",
  passwordConfirm: "",
  nickname: "",
};

const signupSlice = createSlice({
  name: "signup",
  initialState,
  reducers: {
    setEmail: (state, action: PayloadAction<string>) => {
      state.email = action.payload;
    },
    setPassword: (state, action: PayloadAction<string>) => {
      state.password = action.payload;
    },
    setPasswordConfirm: (state, action: PayloadAction<string>) => {
      state.passwordConfirm = action.payload;
    },
    setNickname: (state, action: PayloadAction<string>) => {
      state.nickname = action.payload;
    },
  },
});

export const { setEmail, setPassword, setPasswordConfirm, setNickname } =
  signupSlice.actions;
export default signupSlice.reducer;

slice 폴더에는 현재 사용중인 signupSlice가 들어있는데, slice는 특정 기능에 대한 상태 관리 로직을 캡슐화 한 것이라고 한다.

각 slice마다 해당 데이터를 관리하며, 이 안에 reducer와 action 생성자가 들어있다.

타입스크립트이기 때문에 해당 데이터의 타입을 지정해주고 어떤 상태에 담길건지 각각의 변수를 만들어준다.

본인의 코드에는 signup이라는 slice가 작성되어있고 해당 reducer에는 현재 값을 담을 email, password, nickname등이 들어있다. 이 후 구현 상황에 따라 데이터를 더 추가해야하고 아직 3페이지 부분은 데이터를 담지 못했다.

작성된 reducer를 export로 다른 곳에서 쓸 수 있게 지정해준다.

import styled from "styled-components";
import CommonInput from "../common/commonInput";
import NextBtn from "./nextBtn";
import PwVisible from "../common/pwVisible";
import React, { useEffect, useState } from "react";
import PageCount from "../common/pageCount";
import PageGuide from "../common/pageGuide";
import { useDispatch, useSelector } from "react-redux";
import {
  setEmail,
  setPassword,
  setPasswordConfirm,
} from "../../redux/slice/signupSlice";
import { RootState } from "../../redux/store";

interface NextProps {
  onNext: () => void;
}

const Signup1: React.FC<NextProps> = ({ onNext }) => {
  const dispatch = useDispatch();

  const email = useSelector((state: RootState) => state.signup.email);
  const password = useSelector((state: RootState) => state.signup.password);
  const passwordConfirm = useSelector(
    (state: RootState) => state.signup.passwordConfirm
  );

  const [isFormValid, setIsFormValid] = useState(false);
  const [emailValid, setEmailValid] = useState(false);
  const [passwordLengthValid, setPasswordLengthValid] = useState(false);
  const [passwordComplexityValid, setPasswordComplexityValid] = useState(false);
  const [showPassword, setShowPassword] = useState(false);

  const emailRegex = /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/;
  const passwordLengthRegex = /.{8,}$/;
  const passwordComplexityRegex =
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>])[a-zA-Z\d!@#$%^&*(),.?":{}|<>]{8,}$/;

  useEffect(() => {
    const isEmailValid = emailRegex.test(email);
    const isPasswordLengthValid = passwordLengthRegex.test(password);
    const isPasswordComplexityValid = passwordComplexityRegex.test(password);
    const isPasswordConfirmed = password === passwordConfirm;

    setEmailValid(isEmailValid);
    setPasswordLengthValid(isPasswordLengthValid);
    setPasswordComplexityValid(isPasswordComplexityValid);
    setIsFormValid(
      isEmailValid &&
        isPasswordLengthValid &&
        isPasswordComplexityValid &&
        isPasswordConfirmed
    );
  }, [email, password, passwordConfirm]);

  return (
    <>
      <Signup1Wrapper>
        <SignupBox>
          <PageCount count="1" />
          <PageGuide text="이메일과 비밀번호를 입력해주세요." />
          <CommonInput
            typeValue="email"
            placeholderValue="이메일"
            value={email}
            onChange={(e) => dispatch(setEmail(e.target.value))}
          />
          <InputCheck valid={emailValid}>√ 이메일 확인</InputCheck>
          <CommonInput
            typeValue={showPassword ? "text" : "password"}
            placeholderValue="비밀번호"
            value={password}
            onChange={(e) => dispatch(setPassword(e.target.value))}
          />
          <InputCheck valid={passwordLengthValid}>√ 8자리 이상</InputCheck>
          <InputCheck valid={passwordComplexityValid}>
            √ 영문, 숫자, 특수문자 포함
          </InputCheck>
          <CommonInput
            typeValue={showPassword ? "text" : "password"}
            placeholderValue="비밀번호 확인"
            value={passwordConfirm}
            onChange={(e) => dispatch(setPasswordConfirm(e.target.value))}
          />
          <InputCheck valid={password === passwordConfirm}>
            √ 비밀번호 동일
          </InputCheck>
          <PwVisible onToggle={setShowPassword} />
          <NextBtn onClick={onNext} disabled={!isFormValid} />
        </SignupBox>
      </Signup1Wrapper>
    </>
  );
};
export default Signup1;

const Signup1Wrapper = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
`;

const SignupBox = styled.div`
  width: 100%;
  margin: 50px auto;
`;

const InputCheck = styled.div<{ valid: boolean }>`
  font-size: 11px;
  margin-left: 10px;
  margin-bottom: 5px;
  color: #c9c9c9;
  color: ${(props) => (props.valid ? "green" : "#c9c9c9")};
`;

해당 코드는 1페이지의 코드이며 먼저 리덕스에 데이터를 보내기 위한 useDispatch()를 지정해준다.

그리고 리덕스에서 데이터를 가져오기 위해 useSelector를 사용해서 각 변수에 해당 데이터를 연결해준다.

이러면 입력창에서 데이터가 입력됐을 때 dispatch를 통해 리덕스로 보내지고 리덕스에 저장된 데이터는 useSelector를 통해 변수에 들어오기 때문에 입력된 데이터가 바로 보여지게 된다.

 

그리고 회원가입에 중요한 정규식 검사가 있는데 처음 기획했던 부분을 봤더니 이메일 부분에 입력길이와 특정 값을 넣으라는 체크가 있었다. 근데 체크가 이메일부분에 필요해 보이진 않고 오히려 비밀번호에 필요할 것 같아 체크를 조금 수정해서 비밀번호에 넣어줬다. 그리고 이메일과 비밀번호 확인은 단순히 해당 값이 제대로 입력되었는지만 확인하도록 했다.

이메일은 기존에는 회색으로 비활성화 되어있다가 올바르게 입력했을 시 초록색으로 활성화가 된다.

비밀번호는 두 가지 체크가 있는데 8자리 이상 작성했을 시 첫 줄이 활성화, 대소문자, 숫자, 특수문자를 넣을 시 두 번째 줄이 활성화가 된다. 이 부분은 따로 체크되기 위해 정규식을 나눴는데 길이 부분은 단순히 8자까지만 체크 되게 해서 해당 값의 길이가 8자 이상이 되면 true가 되게 적용했다. 그 외의 입력값 체크는 기존 정규식을 사용했다.

비밀번호 확인은 단순히 위에 작성된 비밀번호값과 동일한 값이 입력되었을 때 활성화가 되게 했다. 비밀번호 체크는 단순 비교라 리덕스에 넣을 필요가 없을 것 같아 따로 useState를 사용해 상태를 관리해줬는데 오히려 다음 페이지로 넘어갔거나 했을 때 값을 수정하기 위해 되돌아가면 재 랜더링되어 비밀번호 확인만 값이 사라지는걸 확인했다. 그래서 그냥 리덕스에 포함시켜서 값을 유지하도록 했다. 

 

비밀번호 표시는 위치가 애매해서 제일 밑에다 두었고 표시를 체크하면 비밀번호와 비밀번호 확인이 둘다 보이게 된다. 해당 UI에서는 표시를 각각 분리하면 너무 난잡해보일 것 같아 하나로 통일하여 사용했다. 

이렇게 모든 정규식을 통과해야 다음 버튼이 활성화 되고 하나라도 정규식이 빠지면 버튼이 비활성화되어 접근이 불가능해진다.

 

2페이지는 닉네임 설정페이지인데 닉네임 입력값을 관리하는 건 구현했으나 랜덤 생성의 경우 어떠한 닉네임을 랜덤으로 설정할건지 정하지 못해서 보류해놨다.

import styled from "styled-components";
import CommonInput from "../common/commonInput";
import NextBtn from "./nextBtn";
import { useState } from "react";
import PageCount from "../common/pageCount";
import PageGuide from "../common/pageGuide";
import { useDispatch, useSelector } from "react-redux";
import { setNickname } from "../../redux/slice/signupSlice";
import { RootState } from "../../redux/store";

interface NextProps {
  onNext: () => void;
}

const Signup2: React.FC<NextProps> = ({ onNext }) => {
  const dispatch = useDispatch();
  const nickname = useSelector((state: RootState) => state.signup.nickname);
  const [nicknameValid, setNicknameValid] = useState<boolean | null>(null);

  const handleNicknameCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    dispatch(setNickname(value));

    if (value.length <= 10) {
      setNicknameValid(true);
    } else {
      setNicknameValid(false);
    }
  };

  return (
    <>
      <Signup2Wrapper>
        <SignupBox>
          <PageCount count="2" />
          <PageGuide text="닉네임을 설정해주세요" />
          <NicknameBox>
            <CommonInput
              typeValue="text"
              placeholderValue="닉네임"
              value={nickname}
              onChange={handleNicknameCheck}
            />
            {nicknameValid !== null && (
              <NickCheckWrapper>
                <NicknameCheck valid={nicknameValid}>
                  {nicknameValid
                    ? "사용할 수 있는 닉네임이에요"
                    : "사용할 수 없는 닉네임이에요"}
                </NicknameCheck>
                <span>{nickname.length}/10</span>
              </NickCheckWrapper>
            )}
            <RandomCreate>랜덤 생성</RandomCreate>
          </NicknameBox>
          <NextBtn onClick={onNext} disabled={!nicknameValid} />
        </SignupBox>
      </Signup2Wrapper>
    </>
  );
};
export default Signup2;

const Signup2Wrapper = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
`;

const SignupBox = styled.div`
  width: 100%;
  margin: 50px auto;
`;

const NicknameBox = styled.div`
  display: flex;
  flex-direction: column;
  margin: 10px 0;
`;

const NicknameCheck = styled.div<{ valid: boolean }>`
  font-size: 11px;
  margin-left: 10px;
  color: ${(props) => (props.valid ? "green" : "red")};
`;

const NickCheckWrapper = styled.div`
  display: flex;
  justify-content: space-between;

  span {
    font-size: 10px;
  }
`;

const RandomCreate = styled.button`
  width: 30%;
  border: 1px solid #b0b0b0;
  border-radius: 5px;
  background-color: white;
  color: #848484;
  font-size: 10px;
  font-weight: bold;
  margin: 10px 0;
  padding: 3px 0;
`;

2페이지도 1페이지와 동일하게 리덕스로 입력된 값을 보내 관리하는 방식인데 닉네임의 정규식 검사 경우에는

처음에는 아무 표시도 없고 해당 입력창에 값을 입력했을 시 현재 입력값의 길이가 실시간으로 보이며 10글자 이하로 입력되면 초록색으로 사용할 수 있다고 표시되고 다음 버튼이 활성화 된다. 그러나 10글자를 넘겨버리면 사용할 수 없는 닉네임이라고 표시되며 다음 버튼이 비활성화 되어 넘어갈 수 없게 되어있다.

 

3페이지도 구현해야 하는데 정규식에 시간을 좀 많이 쓴 것도 있고 이전 입력과 다르게 체크박스와 라디오 버튼으로 배열값으로도 들어가기 때문에 이 부분은 백엔드와 소통하여 어떻게 보낼지 회의한 이 후 구현해서 올릴 예정이다. 

728x90

'개발 TIL' 카테고리의 다른 글

24.09.03 day59 작업 일지  (1) 2024.09.03
24.09.02 day58 작업 일지  (4) 2024.09.02
24.08.29 day56 작업 일지  (0) 2024.08.29
24.08.28 day55 작업 일지  (2) 2024.08.28
24.08.27 day54 작업 일지  (0) 2024.08.27