SIU
article thumbnail

 

윈티드에서 Rest Api를 활용한 CRUD TodoList 사전 과제를 진행하였다.

윈티드 [Todolist] 과제를 진행할 시간이 부족해서 너무 급하게 만드느라 끝나고 리팩토링 필요성을 느꼈다. 

기능은 작동하지만 렌더링 최적화 부분에서 자원 낭비가 심각했다. (여기서 만족 못해)
마치 급하게 지은 집이라 지금은 살 수 있어도 시간에 지남에 따라 부실공사의 문제점이 나오기 마련이다.

일단 객관적인 속도와 최적화 문제를 살펴보고 개선방향을 정해가기로 했다.

 

 

배포 사이트를 라이트하우스로 평가
세부 문제점


세부 큰 문제점을 살펴보면
1. Reduce unused JavaScript and defer loading scripts until they are required to decrease bytes consumed by network activity
사용하지 않는 JavaScript를 줄이고 네트워크 활동에 의해 소비되는 바이트를 줄이기 위해 필요할 때까지 스크립트 로딩을 연기합니다.

2. Text-based resources should be served with compression (gzip, deflate or brotli) to minimize total network bytes.
텍스트 기반 리소스는 총 네트워크 바이트를 최소화하기 위해 압축(gzip, deflate 또는 brotli)과 함께 제공되어야 합니다.

3. A long cache lifetime can speed up repeat visits to your page
긴 캐시 수명은 페이지 반복 방문 속도를 높일 수 있습니다.

 

 

디렉토리 구조 : 기능별로 분리하자

일단 확장과 유지 보수를 위해 디렉토리 구조를 다시 설계했다.

 

<기존 디렉토리 트리>

  • 기능과 페이지가 분리되지 않았다. => 기능별 트리 레벨 분리
  • api를 모듈화 하지 않고 컴포넌트에서 바로 불러와서 사용했다. => api 모듈화 필요성
📦src
 ┣ 📂pages
 ┃ ┣ 📜LoginForm.js
 ┃ ┣ 📜SighupForm.js
 ┃ ┣ 📜TodoItems.js
 ┃ ┗ 📜TodoList.js
 ┣ 📜App.css
 ┣ 📜App.js
 ┣ 📜App.test.js
 ┣ 📜index.css
 ┣ 📜index.js
 ┣ 📜reportWebVitals.js
 ┗ 📜setupTests.js

 

회원 가입  : 유효성 검사 state와 이벤트 핸들러를 분리하자

// 레거시 코드
  
const [isValid, setIsValid] = useState({
    isEmail: false,
    isPwd: false,
  });  

useEffect(() => {
    if (isValid.isEmail && isValid.isPwd === true) {
      setIsDisabled(false);
    } else setIsDisabled(true);
  }, [isValid]);
  // 만약 2개 state로 나눴으면 [isValidEmail, isValidPassword]

  const handleChange = (e) => {
    setform({
      ...form,
      [e.target.name]: e.target.value,
    });
    if (e.target.name === "pwd" && e.target.value.length > 7) {
      setIsValid({
        ...isValid,
        isPwd: true,
      });
    }
    if (e.target.name === "email" && e.target.value.includes(`@`)) {
      setIsValid({
        ...isValid,
        isEmail: true,
      });
    }
  };

 

👿 문제점

  • 레거시 코드에선 이메일과 패스워드의 유효성 검사 이벤트 핸들러 부분을 하나로 통합하였다.
  • 그러다 보니깐 사용자가 이메일이나 비밀번호를 지워도 감지가 안 되어서 유효성 검사가  한 번 true 되면 바뀌지 않았다.
  • 유효성 검사 state도 이메일과 비밀번호를 같이 관리할 필요성을 느끼지 못했다. . 
  • disabled 속성은 true || false 형태만 넣기
  • input의 value는 null 대신에 undefined 를 초기값으로 주어야 한다.

 


<button> 태그의 disabled 속성 (boolean)
특정 조건이 충족될 때까지 사용자가 버튼을 클릭할 수 없도록 설정하고, 특정 조건이 충족되면
자바스크립트 등으로 disabled 속성값을 삭제하여 사용자가 버튼을 다시 사용할 수 있도록 조절할 수 있습니다.
불리언 속성은 해당 속성을 명시하지 않으면 속성값이 자동으로 false 값을 가지게 되며, 명시하면 자동으로 true 값을 가지게 됩니다.

 

// 개선된 코드
// 유효성 검사 state
  const [isValidEmail, setIsValidEmail] = useState(false);
  const [isValidPwd, setIsValidPwd] = useState(false);

  // 회원 가입 버튼 비활성화
  const [isDisabled, setIsDisabled] = useState(true);

  useEffect(() => {
    if (isValidEmail && isValidPwd) {
      setIsDisabled(false);
    } else setIsDisabled(true);
  }, [isValidEmail, isValidPwd]);
  // 만약 2개 state로 나눴으면 [isValidEmail, isValidPassword]

  const emailChangeHandler = (e) => {
    const value = e.target.value;
    setform({
      ...form,
      [e.target.name]: value,
    });
    if (!value.includes("@")) {
      setIsValidEmail(false);
    } else setIsValidEmail(true);
  };

  const pwdChangeHandler = (e) => {
    const value = e.target.value;
    setform({
      ...form,
      [e.target.name]: value,
    });
    if (value.length < 8) {
      setIsValidPwd(false);
    } else setIsValidPwd(true);
  };

😇 개선점

  • useEffect 배열(deps)에서 사용자가 email과 password를 변경하여도 유효성 검사 값이 바뀔 수 있게 하였다.(disabled)
  • 유효성 검사는 state를 분리해서 관리하는 것이 로직 상 효율적이라고 생각했다.왜냐하면,  2개 이상의 유효성 검사를 같은 state의 객체로 관리하게 되면 서로 다른 element에 접근하여 예상치 못한 오류가 발생할 수 있다.
  • 그래서 유효성 state  뿐만 아니라 이벤트 핸들러도 나누어서 관리하였다.

 

 

API 모듈화

왜 Api 모듈화가 필요할까?

Rest Api를 호출할 때 해당 컴포넌트에서 로직을 처리하다보니깐 Request header에서 동일한 로직이 반복되고 Api가 더 많아진다면 확장상 문제가 생길 수 있어 Api 모듈화를 진행하기로 하였다.


✔ 확장성(extensibility)
확장성을 고려하지 않은 코드는 시스템의 규모가 커질수록 문제가 생길 확률이 높다.
✔  재사용성(reusability)
반복되는 로직을 함수로 분리하는 코드상의 재사용성 뿐만 아니라, 우리가 설계한 구조가 재사용 되어야 한다.
✔  유지-보수 가능성(maintability)
여러 로직이 뒤엉켜 있는 코드는 유지 보수가 안된다.
✔  가독성(readability)
어려운 로직 일수록 더 가독성이 높아야 한다. 어려운 로직을 쉽고 간단하게 구현하는 것이 좋은 코드다.
프로젝트의 구조 또한 한 눈에 그려져야 한다.
✔  테스트 가능성(testability)
테스트를 하기 쉬운 코드는 모듈화가 잘 되어 있고, 한 가지 역할만 하는 함수 단위의 코드를 의미한다.
프로젝트의 구조도 추상화가 잘 되어있고, 역할이 잘 나뉘어 있는 구조가 테스트하기 쉬운 구조다.

출처: 깔끔한 파이썬 탄탄한 백엔드 - 송은우 저

 

 

기존 코드 

const loginCheck = () => {
    axios(
      {
        method: "post",
        url: "https://~~~~~~~~~/signin",
        headers: {
          "Content-Type": "application/json",
        },
        data: JSON.stringify({
          email: form.email,
          password: form.pwd,
        }),
      },
      { withCredentials: true }
    ).then((res) => {
      let access_token = res.data.access_token;
      localStorage.setItem("access_token", access_token);
      console.log(`access_token: ${access_token}`);
      if (res.request.status === 200) {
        navigate("/todo");
      }
    });
  };

👿 문제점

  • Api마다 동일한 url을 상태 관리 안 했다.
  • Api 마다 동일한 로직이 있다 (header)

 

😇 개선점

1) .env 환경 변수 

API주소를 변수처리했다. 백엔드 서버 IP 주소가 변경되면 모든 API 의 IP 주소를 수정해야 하는데 이때 .env로 관리하면 전체 IP를 한번에 수정할 수 있습니다. Node.js에서는 보통 process.env를 통해서 환경 변수에 접근하게 되는데요. process는 Node.js에 기본적으로 내장된 전역 객체여서 별도로 불러올(import) 필요없이 프로그램의 어디에서든지 사용할 수 있습니다. 일부 환경 변수들은 우리가 직접 설정해주지 않더라도 운영 체제 수준에서 이미 설정이 되어 있는데요. 예를 들어, 터미널(terminal)을 열고 Node.js 인터프리터를 실행하시면 어떤 환경 변수들이 이미 설정되어 있는지 간단하게 확인해볼 수 있습니다.

REACT_APP_SERVER_URL="https://~~~~~~~~~~/"

 

 

2) config.js에서 Api 주소 변수 관리

config.js라는 파일을 새로 만들고 그 안에서 api를 인스턴화 만들었다.
http request 에서 공통적인 헤더 부분을 인스턴트화 하였다.
create() 매서드를 사용해 사용자 정의 구성을 사용하는 axios 인스턴스르를 생성한다.

// axios 인스턴스
export const instance = axios.create({
  baseURL: process.env.REACT_APP_SERVER_URL,
  headers: {
    "Content-Type": "application/json",
  },
});

 

3. 기능별로 객체화 

export const authApi = {
  signup: () => api.get("/auth/signin"),
  signin: (email, pwd) =>
    api.post("auth/signin", {
      data: {
        email: email,
        password: pwd,
      },
    }),
};

회사 같은 대규모 서비스 사이트라면 기능별로 api 가 많을 것이다.
그래서 과제에서는 auth api 2개, Todolist는 api 4개 밖에 없지만 auth와 todo의 api를 분리하였다.
위의 객체는 로그인 관련 api를 관리하는 auth.js이다. (api는 위에서 만든 인스턴트인데 이름을 바꿨다)
이렇게 기능별로 Api를 객체화하면 해당 컴포넌트에서 바로 함수 호출이 가능하다. 

 

4. 해당 컴포넌트에서 api 호출

  //로그인 api
  const loginCheck = async () => {
    const res = await authApi.signin(form.email, form.pwd);
    if (res.request.status === 200) {
      let access_token = res.data.access_token;
      localStorage.setItem("access_token", access_token);
      navigate("/todo");
    }
  };

로그인 버튼을 눌렸는데 안 된다. 안돼

401 에러가 떴다. status code 401(Unauthorized)
클라이언트 오류 상태 응답 코드는 해당 리소스에 유효한 인증 자격 증명이 없기 떄문에 접근할 자격이 없다는 뜻이다.

차분히 다시 코드를 확인해봤다.

401 에러


브라우저 개발자 도구에서 401 응답을 받았다. 

원인은 크게 2가지로 접근할 수 있는데 



첫 번째는 JWT Token이 만료된 문제이다.
post 매서드 중 토큰이 필요한 예를 들어 글 작성 같은 경우에 토큰을 request  헤더에 넣어서 보낸다. JWT_EXPIRATION_DELTA  항목을 보시면 datetime.timedelta(seconds=시간)과 같이 명시되어있습니다.

물론 로그인 같은 경우에 http request 요청 바디에 토큰을 넣지 않기 때문에 이 문제는 아니였다. 
 


두 번째는 request 헤더에 서버가 원하는 정보를 넣지 않았을 때이다.
리팩토링 전에는 로그인 되는데 인증 문제면 헤더 값 문제일 수도 있었다.
브라우저 개발자 도구에서 401 응답을 받는 요청의 요청 헤더에서 Authorization 헤더의 값이 명확하게 잘 들어있는 지 확인했다. 헤더 확인해보니깐 데이터 요청이 잘 들어갔다고 생각했는데 data 객체로 감싸져 있었다. (!!!!!!!)

과제 api 문서에서 body에 페이로드를 넣으면 되었다. 

 

 

문제의 요청 페이로드(401에러)
data로 감싸지 않고 성공한 로그인 성공(200)

 

axios 공식문서를 찾아봤다.

https://yamoo9.github.io/axios/guide/api.html#http-%EB%A9%94%EC%84%9C%EB%93%9C-%EB%B3%84%EC%B9%AD

 

API | Axios 러닝 가이드

API 구성(configuration) 설정을axios()에 전달하여 요청할 수 있습니다. axios(config) axios({ method: 'post', url: '/user/12345', data: { firstName: 'Fred', lastName: 'Flintstone' } }); 1 2 3 4 5 6 7 8 9 axios({ method:'get', url:'http://bit

yamoo9.github.io

 

HTTP 메서드 별칭에서는 config에서 data 속성을 지정할 필요가 없다고 한다. 

 

 

위에서 내가 짠 모듈 코드

export const authApi = {
  signup: () => api.get("/auth/signin"),
  signin: (email, pwd) =>
    api.post("auth/signin", {
      data: {
        email: email,
        password: pwd,
      },
    }),
};

 

data 속성 삭제하고 고친 코드

export const authApi = {
  signup: () => api.get("/auth/signin"),
  signin: (email, pwd) =>
    api.post("auth/signin", {
      email: email,
      password: pwd,
    }),
};

 

새로 리팩토링한 api 모듈로 로그인이 되었다. 구글링 보다 공식 문서를 가까이 해야겠다. 

이 방법으로 todo Api도 모듈화 하여 사용했다.



 

 

토큰 만료 시간을 디코더로 확인하기

이 토큰의 만료 시간은 어떻게 될까? 
access token의 만료 시간이 보통 생명 주기가 짧은데 갑자기 궁금해졌다.

response 구조를 확인해보니 토큰 만료 시간 정보는 없었다.

 

<로그인 response> 

<회원가입 response>

 

공부하다가 발견할 건데 토큰을 decode해서 정보를 알아낼 수 있었다.

너무 신기해서 기록해둔다. 

디코더 사이트에서 토큰 정보를 넣었는데 응답 바디에 없는 여러가지 정보를 얻을 수 있었다.

열람은 누구나 가능하지만 서명은 위조가 불가능하다.

(이래서 토큰에 보안상 중요한 정보를 넣지 말라했구나)

 

PAYLOAD

  • sub는 userid 이다. 같은 아이디로 접속하면 똑같은 값이 나오고 todo list에 작성해보면 userid 정보와 일치한다
  • lat은 토큰 생성 시간이고 exp은 토큰 만료 시간이다. 한국 표준시 milisecond로 리턴하는 형태 같다.
  • 이 토큰은 유효 기간이 일주일 인것을 디코더로 확인했다.

 

 

 

 

 

 

 

참고자료

https://axios-http.com/docs/intro

 

Getting Started | Axios Docs

Getting Started Promise based HTTP client for the browser and node.js What is Axios? Axios is a promise-based HTTP Client for node.js and the browser. It is isomorphic (= it can run in the browser and nodejs with the same codebase). On the server-side it u

axios-http.com

 

profile

SIU

@웹 개발자 SIU

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!