배워서 남 주자

[TWIL] 사전 프로젝트를 하며 알게 된 몇 가지 - 최종편

미래에서 온 개발자 2023. 3. 5. 23:37

1편2편에 이어 최종편으로 마무리해보려 한다. 
프로젝트를 하면서 만난 에러 모음 및 회고는 언제 포스팅할 수 있으려나..🥲
 

1. 비밀번호 유효성 검사

필요한 유효성 검사: 최소 8자, 하나 이상의 문자와 하나의 숫자 

// 8~20 글자
let regExp = /^[a-zA-Z0-9]{8,20}$/

// 8 글자만 검사 통과 
let regExp = /^[a-zA-Z0-9]{8}$/

// 8 글자 이상
let regExp = /^[a-zA-Z0-9]{8,}$/

 
🔽 최종적으로 작성한 checkPassword 함수
 

export const checkPassword = (str) => {
const regexp = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
const result = regexp.test(str);
if (!result) {
alert(
'Passwords must contain at least eight characters, including at least 1 letter and 1 number.'
);
}
return result;
};
view raw checkPassword hosted with ❤ by GitHub

 
 

2. request header의 Authorization

인증 토큰을 서버에 보낼 때 요청 헤더의 authorization에 실어서 보냈다. 여러 곳에서 post 요청을 보내야 했기에 먼저 다음과 같이 postFetch 함수를 작성했다.
 

const postFetch = async (url, newData, jwt) => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: jwt,
withCredentials: true
},
body: JSON.stringify(newData)
});
if (res.ok) {
return res;
}
} catch (err) {
console.error(err);
}
};
view raw postFetch hosted with ❤ by GitHub

 
그리고 실제 요청을 해야 하는 곳에서 다음과 같이 postFetch를 호출했다.

    // * question POST 요청
    const QUESTION_POST_URL = `${process.env.REACT_APP_URL}/questions`;
    const res = await postFetch(QUESTION_POST_URL, newData, accessToken);
    const location = res.headers.get('Location'); 
    
    if (res) {
      navigate(location);
    }

 
 

3. response header의 status code와 location

로그아웃 post 요청 후 프론트 코드에서 작성한 리다이렉션이 아닌 엉뚱한 곳으로 이동하는 일이 생겼다. 찾아보니 post 요청 후 상태 코드가 302이 떴는데, 300번대 상태코드가 뜨면 response header에 있는 location으로 자동으로 리다이렉트 되는 거였다. 
 
서버에 로그아웃 post 요청 후에 200번대 상태 코드를 보내달라하고, 다음과 같이 로그아웃 핸들러를 구현했다.

  const handleLogout = async () => {
    const LOGOUT_POST_URL = `${process.env.REACT_APP_URL}/logout`;
    const res = await postFetch(LOGOUT_POST_URL);

    if (res) {
      setIsLoggedIn(false);
      setTokens({
        accessToken: '',
        refreshToken: ''
      });

      navigate('/');
    }
  };

 

4. useContext

질문/답변 게시판 구현을 하면서 상태 관리 라이브러리를 사용할 정도의 필요성을 크게 못 느껴서 useContext hook을 사용했다. props drilling을 한다고 해도 두세번 이상 내려가지 않는 컴포넌트 구조라 props drilling으로도 해결 가능했지만, useContext 를 사용하니 custom hook으로 필요한 로직을 분리해 내는게 훨씬 편해졌다.
 

5. custom hook

프로젝트를 하면서 custom hook을 어떤 경우에 사용해야 하는지 확실히 알게 되었다. useState, useEffect, useContext 등 react hook을 사용하는 로직이 공통적으로 반복되어서 분리해야 할 때 커스텀 훅을 작성하니 코드가 많이 간결해지고 가독성도 훨씬 좋아졌다. 
아래는 프로젝트에서 사용한 hooks을 모은 코드이다. 각 hook마다 로직을 짤 때 고민들이 고스란히 담겨 있는데, 이에 대한 썰들은 다음 포스팅에서 하나하나 풀어갈 기회(라고 쓰고 시간이라고 읽는다)가 있기를 🙏🏼
 
🔽 useLoginLogic : 로그인 페이지와 회원가입 페이지에서 동일한 로직을 빼내서 구현
 

import { useState, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { userContext } from '../App';
import { postFetch } from './API';
import { checkPassword } from './checkPassword';
function useLoginLogic(initialInputs, url, alertMsg, key1, key2, key3) {
const navigate = useNavigate();
const [inputs, setInputs] = useState(initialInputs);
const { setIsLoggedIn, setTokens } = useContext(userContext);
const onChange = (e) => {
const { name, value } = e.target;
setInputs({ ...inputs, [name]: value });
};
const onSubmit = async (e) => {
e.preventDefault();
if (inputs[key1] === '' || inputs[key2] === '' || inputs[key3] === '') {
alert(alertMsg);
return;
}
const result = checkPassword(inputs.password);
if (!result) return;
const res = await postFetch(url, inputs);
const accessToken = res.headers.get('Authorization');
const refreshToken = res.headers.get('Refresh');
if (res.ok) {
setTokens({
accessToken,
refreshToken
});
setIsLoggedIn(true);
navigate('/questions');
}
};
return [inputs, onChange, onSubmit];
}
export default useLoginLogic;
view raw useLoginLogic hosted with ❤ by GitHub

 
🔽 useCheckLogin
 

import { useEffect, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { userContext } from '../App';
function useCheckLogin() {
const navigate = useNavigate();
const { isLoggedIn } = useContext(userContext);
useEffect(() => {
if (!isLoggedIn) {
navigate('/users/login');
}
}, []);
return isLoggedIn;
}
export default useCheckLogin;
view raw useCheckLogin hosted with ❤ by GitHub

 
🔽 useAccessToken
 

import { useContext } from 'react';
import { userContext } from '../App.js';
function useAccessToken() {
const { tokens } = useContext(userContext);
const accessToken = tokens && tokens.accessToken;
return accessToken;
}
export default useAccessToken;
view raw useAccessToken hosted with ❤ by GitHub

 
🔽 useLocalStorage
 

import { useState, useEffect } from 'react';
function useLocalStorage(key, initialState) {
const [state, setState] = useState(
() => JSON.parse(window.localStorage.getItem(key)) || initialState
);
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
}
export default useLocalStorage;
view raw useLocalStorage hosted with ❤ by GitHub