기술 선정 이유 - dltmdrbtjd/HANG GitHub Wiki

기술 선정 이유

Typescript

정적 타입 언어 자바스크립트 슈퍼셋

자바스크립트는 동적 타입 언어로 사용자가 데이터 타입에 무신경해도 된다는 장점이 있습니다. 이러한 특징은 개발 속도를 높여주고 자유도가 높다는 장점이 있지만, 프로젝트의 크기가 커질수록 단점으로 작용합니다.

데이터 타입이 다르다면 특히 서버와의 통신에서 알 수 없는 오류가 자주 발생하는데 서버에서 요구하는 데이터의 타입을 미리 정해놓으면 잘못된 타입의 데이터가 전달되어 발생되는 오류를 줄일 수 있습니다.

const signUpDB = (image, userInfo) => {
  return (dispatch, getState, { history }) => {
    if (!image) {
      apis
        .SignUp(userInfo)
        .then(() => {
          history.replace('/signup/welcome');
        })
        .catch((err) => console.log(err));

      return;
    }

    dispatch(
      ImageCreators.uploadProfileImgDB(image, () => {
        const profileImg = getState().image.profileImg;

        apis
          .SignUp({ ...userInfo, profileImg })
          .then(() => {
            dispatch(ImageCreators.uploadProfileImg(null));
            dispatch(ImageCreators.setProfilePre(null));
          })
          .then(() => {
            history.replace('/signup/welcome');
          })
          .catch((err) => console.log(err));
      })
    );
  };
};

위 코드는 회원가입을 위한 서버와의 통신 함수입니다.

함수의 매개변수로 받는 userInfo의 gender와 age 속성이 프론트에서는 string 타입이지만 서버에서 요구하는 데이터는 number 타입입니다. 자바스크립트로 위 함수 실행 시 런타임 시점에 서버에 전달받는 상태 코드로 오류를 정확히 파악하기 힘들었습니다.

서버에서 요구하는 데이터를 타입에 맞게 정확하게 보낼 방법으로 타입 스크립트를 채택했고, 컴파일 환경에서 타입 체크를 해주기 때문에 실제 런타임 환경에서는 정확한 데이터 송신이 가능했습니다.

다음은 타입 스크립트를 사용해 개선한 코드입니다.

// 회원가입 데이터 타입
type SignUpType = {
  userId: string;
  nickname: string;
  password: string;
  region: string;
  city: string;
  profileImg: string | null;
  age: number;
  gender: number;
  pNum: string;
};

// 회원가입 api
SignUp: (user: SignUpType) => instance.post('/api/users', user);

// 서버 통신 함수
const SignUp = (userInfo: userInfo) => {
  if (!profile) {
    dispatch(
      activeAlert({
        status: true,
        errorMsg: '프로필 이미지를 등록해주세요.',
      })
    );

    return;
  }

  uploadProfileImage(profile).then((res) => {
    const profileImg = res;

    apis
      .SignUp({
        ...userInfo,
        profileImg,
        region,
        city,
        gender,
        age: parseInt(age, 10),
      })
      .then(() => setPage((page: number) => page + 1))
      .catch(() =>
        dispatch(
          activeAlert({
            status: true,
            errorMsg: '회원가입에 실패 하였습니다.\n나중에 다시 시도해주세요',
          })
        )
      );
  });
};

formik + yup

유효성 검사 form 작성

입력 값 + 유효성 통과 여부 + 입력 값의 현재 상태

React로 로그인 등의 form 양식을 작성하기 위해선 하나의 입력 창에 위와 같이 3개의 상태가 필요합니다.

또한 아래 코드와 같이 지금 어떤 유효성 체크를 통과하였고 어떤 항목을 통과하지 못했는지 여부를 조건문으로 일일히 다 체크해야 한다는 단점이 있습니다.

// 아이디 유효성 체크 state
const [id, setId] = React.useState('');
const [idConfirm, setIdConfirm] = React.useState('');
const [idWarning, setIdWarColor] = React.useState('red');

const idVal = (nickname) => {
  const _reg = /[A-Za-z0-9]{4,20}$/;

  return _reg.test(nickname);
};

// 아이디 유효성 체크
const checkID = (val) => {
  if (val === '') {
    setIdWarColor('red');
    setIdConfirm('아이디가 입력되지 않았습니다.');
    return;
  }
  if (!idVal(val)) {
    setIdWarColor('red');
    setIdConfirm('아이디가 형식에 맞지 않습니다. (영어, 알파벳 4~20자)');
    return;
  }

  setIdWarColor('green');
  setIdConfirm('중복 검사를 해주세요');
};

로그인처럼 입력 폼에 들어가는 항목이 적고 유효성을 검사해야 하는 부분이 적다면 위 코드도 전혀 문제가 없지만 회원가입처럼 항목이 많은 경우에는 코드의 가독성을 떨어뜨리고 실수를 유발할 가능성이 높아집니다.

1. 입력 값을 관리하는 state 줄이기

initialValues={{
          pNum: '',
          userId: '',
          password: '',
          nickname: '',
        }}

formik을 사용하면 위와 같이 한 입력 폼에 들어가는 입력 값들을 하나의 객체로 관리할 수 있습니다.

<ValidateInput
  placeholder="전화번호 입력"
  type="tel"
  width="58%"
  name="pNum"
  value={formik.values.pNum}
  _onChange={formik.handleChange('pNum')}
  status={currentType[phoneVeri.status]}
/>

값을 사용하거나 변경이 필요할 때는 값을 표시하는 input에 formik.values[찾아올 값]으로 값을 찾아오고 formik.handleChange(값) 으로 값을 변경할 수 있습니다.

2. yup으로 유효성 체크하기

다음은 yup을 사용해 유저 id의 유효성을 검사하는 코드입니다.

const idRegExp = /^[a-z]+[a-z0-9]+$/gi;

userId: yup
  .string()
  .matches(idRegExp, '아이디는 영문자로 시작해야 하며 영문자 또는 숫자만 사용 가능합니다')
  .min(6, '아이디를 6~14자로 입력해 주세요')
  .max(14, '아이디를 6~14자로 입력해 주세요');

조건에 따라 분기할 필요가 없어졌고, 글자수 같은 부분은 yup에서 체크해주기 때문에 정규 표현식에 항목을 넣을 필요도 없어졌습니다.

유효성 검사를 통과하지 못했을 때 현재 상태 텍스트를 보여주기 위해선 아래와 같이 formik.errors[값]을 사용해 미리 지정한 상태 메시지를 보여줄 수 있습니다.

{
  formik.errors.pNum ? (
    <Text fs="sm" color="danger" margin="15px 0 0">
      {formik.errors.pNum}
    </Text>
  ) : null;
}

3. 함수에 값 전달하기

// formik 적용 이전 코드
const signup = () => {
  if (!(dupState && idWarning === 'green' && pwdWarning === 'green' && pwdCheckWarning === 'green'))
    return;

  dispatch(userActions.signupDB(id, pwd, pwdCheck));

  window.alert('회원가입이 완료되었습니다. 다시 로그인해 주세요.');
  history.push('/login');
};
// formik 적용 코드
onSubmit={(values: userInfo, { setSubmitting }) => {
          SignUp(values);
          setSubmitting(false);
        }}

formik 적용 이전에는 입력 필드에 따라 각각의 state가 존재하기 때문에 각각의 state 값들을 하나의 객체로 묶는 과정을 거쳐야합니다.

하지만 formik은 회원가입에 필요한 값들을 이미 하나의 객체로 관리하기 때문에 formik의 values만 회원가입 함수에 넘겨주면 됩니다.

최종 코드

// formik 적용 이전
const [id, setId] = React.useState('');
const [pwd, setPwd] = React.useState('');
const [pwdCheck, setPwdCheck] = React.useState('');
const [idConfirm, setIdConfirm] = React.useState('');
const [pwdConfirm, setPwdConfirm] = React.useState('');
const [pwdCheckConfirm, setPwdCheckConfirm] = React.useState('');
const [idWarning, setIdWarColor] = React.useState('red');
const [pwdWarning, setPwdWarColor] = React.useState('red');
const [pwdCheckWarning, setPwdCheckWarColor] = React.useState('red');

const checkID = (val) => {
  if (val === '') {
    setIdWarColor('red');
    setIdConfirm('아이디가 입력되지 않았습니다.');
    return;
  }
  if (!idVal(val)) {
    setIdWarColor('red');
    setIdConfirm('아이디가 형식에 맞지 않습니다. (영어, 알파벳 4~20자)');
    return;
  }

  setIdWarColor('green');
  setIdConfirm('중복 검사를 해주세요');
};

const checkPWD = (val) => {
  if (val === '') {
    setPwdWarColor('red');
    setPwdConfirm('패스워드가 입력되지 않았습니다.');
    return;
  }
  if (!pwdVal(val)) {
    setPwdWarColor('red');
    setPwdConfirm('패스워드가 형식에 맞지 않습니다. (영어, 알파벳 6~30자)');
    return;
  }
  setPwdWarColor('green');
  setPwdConfirm('사용가능한 패스워드 입니다.');
};

const checkPWD2nd = (val) => {
  if (val === '') {
    setPwdCheckWarColor('red');
    setPwdCheckConfirm('패스워드 확인란이 입력되지 않았습니다.');
    return;
  }
  if (val.length < 6) {
    setPwdCheckWarColor('red');
    setPwdCheckConfirm('');
    return;
  }
  if (val !== pwd) {
    setPwdCheckWarColor('red');
    setPwdCheckConfirm('입력된 패스워드가 서로 다릅니다.');
    return;
  }
  setPwdCheckWarColor('green');
  setPwdCheckConfirm('패스워드가 올바르게 입력되었습니다.');
};

const signup = () => {
  if (!(dupState && idWarning === 'green' && pwdWarning === 'green' && pwdCheckWarning === 'green'))
    return;

  dispatch(userActions.signupDB(id, pwd, pwdCheck));

  window.alert('회원가입이 완료되었습니다. 다시 로그인해 주세요.');
  history.push('/login');
};

const nickname = () => {
  dispatch(userActions.nickCheck(id));
  setIdConfirm('');
};
// formik 적용
<Formik
  initialValues={{
    pNum: '',
    userId: '',
    password: '',
    nickname: '',
  }}
  validationSchema={yup.object({
    pNum: yup
      .string()
      .matches(phoneRegExp.hyphen, '-을 제외한 숫자만 입력해 주세요')
      .matches(phoneRegExp.number, '숫자만 입력해 주세요')
      .matches(phoneRegExp.phoneNumber, '전화번호 형식이 아닙니다.'),
    userId: yup
      .string()
      .matches(idRegExp, '아이디는 영문자로 시작해야 하며 영문자 또는 숫자만 사용 가능합니다')
      .min(6, '아이디를 6~14자로 입력해 주세요')
      .max(14, '아이디를 6~14자로 입력해 주세요'),
    password: yup
      .string()
      .matches(pwdRegExp, '비밀번호는 숫자, 영문자, 특수문자(!@#$%^&*()?_~)만 사용할 수 있습니다')
      .min(8, '비밀번호를 8자 이상 입력해 주세요'),
    nickname: yup
      .string()
      .min(1, '닉네임을 입력해주세요')
      .max(16, '닉네임은 16자까지 입력할 수 있습니다'),
  })}
  onSubmit={(values: userInfo, { setSubmitting }) => {
    SignUp(values);
    setSubmitting(false);
  }}
>
  {(formik) => (
    <form onSubmit={formik.handleSubmit}>
      <PhoneValidationCheck formik={formik} />
      <EnterIdPwd formik={formik} />
      <FillOutProfile formik={formik} />
    </form>
  )}
</Formik>

formik 적용 이전보다 입력 필드가 늘어났지만 오히려 코드가 더 줄었고 유효성 검사 부분도 더 직관적으로 바뀌었습니다.


Redux Toolkit

쉬운 상태 관리 라이브러리

Redux는 store, initialState, action creators, reducer 이렇게 나누어집니다. 하나의 Redux 파일에는 들어가는 코드의 수가 많아 한 눈에 원하는 정보를 찾기가 어렵습니다.

Redux Toolkit은 store와 createSlice만 있으면 하나의 Redux 파일을 생성할 수 있어 단순한 구조를 띠고 기본적으로 immer가 내장되어 있기 때문에 별도의 패키지 설치 없이도 편하게 불변성을 유지할 수 있습니다. 이러한 장점때문에 이번 프로젝트에서 Toolkit을 사용했습니다.


socket.io

이번 프로젝트에서 실시간 채팅과 알람 기능 구현을 위해 socket io를 도입했습니다.

다른 대안으로 WebSocket이 있지만 비교적 최신 기술이기 때문에 오래된 브라우저에서는 지원하지 않는다는 점과 socket io가 javascript 기반으로 작성되었기 때문에 React와 Node.js와 잘 맞다는 특성이 있어 선택하게 되었습니다.

⚠️ **GitHub.com Fallback** ⚠️