본문 바로가기
항해99/프로젝트

[TIL-27] 실전 프로젝트 - 로그인 기능 구현(Intercepter, ESLint 에러)

by junvely 2023. 5. 25.

Today목표 : 05/24일 실전 프로젝트 : 로그인 기능 구현(Intercepter, ESLint 에러)

프로젝트 셋팅을 마치고, 디자인이 나오기 전까지 우선적으로 기능 구현을 할 수 있는 부분 부터 작업하기로 하여 로그인 기능 구현을 시작했다. 로그인 처리 과정에서 공부한 부분과 이 기능을 사용한 이유, ESLint 설정으로 인한 에러에 대해 정리해 보았다.


알게된점,

 

1.  내가 instance와 intercepter를 적용한 이유

- instance를 사용한 이유

1) axios마다 base url, content-type , token과 같은 헤더값 등 동일한 값 등의 처리를 줄이기 위해

2) api 호출할 때마다 반복되는, 중복되는 axios 인스턴스를 생성을 줄이기 위해

axios create를 이용해 instance를 생성하지 않으면, 모든 api 요청에서 필요한 baseUrl이나 headers 관리, token 요청 응답 등을 처리해주어야 한다. 이렇게 되면 api가 많아지면 많아질수록 중복 되는 로직들이 증가하게 되고, baseUrl 등 공통적으로 쓰이는 모든 로직 등을 유지보수 하기가 굉장히 어렵다. 예를들어 baseUrl이 변경될 경우 모든 api요청에서 baseUrl을 수정해 주어야 한다. 이런 로직들을 줄이고 공통된 instance를 생성해 관리하게 되면 코드 중복도 줄고, 유지보수가 굉장히 용이하다.

 

- intercepter를 적용한 이유

로그인 처리에서 빠질 수 없는건 access token과 refresh token의 관리다. 로그인 시 서버에게 토큰을 받으면 토큰을 저장하고, 사용자 정보가 필요한 api에서는 토큰을 헤더에 담아 서버에 전송해 주어야 한다. 이런 모든 과정들을 intercepter로 처리하지 않으면, 일일히 이런 과정이 필요한 모든 api에서 헤더에 토큰을 매번 담아서 전송해야 하고, 로그인시 또는 토큰 만료시 서버로부터 전달받는 토큰을 일일히 그 때 마다 브라우저에 저장해주는 과정을 계속해서 반복해야 한다. 이럴 경우 토큰을 저장한다던지, 토큰을 헤더에 담아 보낸다던지, 중복되는 로직들이 너무 많아진다. 

내가 instance에 intercepter를 적용하여 사용한 이유는, 이렇게 클라이언트와 서버 간의 요청과 응답을 가로채서 응답받은 토큰이 있다면 자동으로 브라우저에 저장되게 하거나, 사용자 정보가 필요한 요청 시에는 항상 헤더에 토큰을 담아 서버로 전달하거나 할 수 있도록 하여 중복 되는 로직들을 중간에서 가로채 모두 처리할 수 있게끔 하기 위해서 사용하였다.

 

2. Axios에서 intercepter의 내부 로직 살펴보기

// 요청 인터셉터 추가
const myInterceptor = axios.interceptors.request.use(
  function (config) {
    // 요청을 보내기 전에 수행할 일
    // ...
    return config;
  },
  function (error) {
    // 오류 요청을 보내기전 수행할 일
    // ...
    return Promise.reject(error);
  });
// Axios 내부 로직

class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = { // interceptors
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }
  ...
}
// interceptorManager 내부 로직

class InterceptorManager {
  constructor() {
    this.handlers = [];
  }

  use(fulfilled, rejected, options) { // 3가지를 인자로 받는다.
    this.handlers.push({
      fulfilled,
      rejected,
      synchronous: options ? options.synchronous : false,
      runWhen: options ? options.runWhen : null
    });
    return this.handlers.length - 1;
  }

  eject(id) {
    if (this.handlers[id]) {
      this.handlers[id] = null;
    }
  }
  ...
}

 

 

3.   intercepter의 요청시 동작원리, config 안에는 어떤 정보가 들어있을까?

인터셉터의 config 매개변수는 요청 또는 응답이 전달되는 시점에서의 설정 객체다. 이 객체를 통해 해당 요청이나 응답에 대한 다양한 정보와 설정을 확인하고 변경할 수 있다.

일반적으로 요청 인터셉터의 config 객체에는 다음과 같은 정보 및 설정이 포함될 수 있다.

url: 요청의 URL 정보
method: 요청의 HTTP 메서드 정보 (GET, POST, PUT, 등)
headers: 요청의 헤더 정보
params: 요청의 URL 쿼리 매개변수 정보
data: 요청의 본문 데이터 정보

응답 인터셉터의 config 객체에는 다음과 같은 정보 및 설정이 포함될 수 있다:

status: 응답의 HTTP 상태 코드
statusText: 응답의 HTTP 상태 메시지
headers: 응답의 헤더 정보
data: 응답의 본문 데이터 정보

인터셉터의 config 객체를 사용하여 요청이나 응답에 대한 정보를 확인하고 필요에 따라 변경할 수 있다. 예를 들어, 특정 요청에 대해 인증 토큰을 추가하거나, 응답 데이터를 가공하거나 오류 처리를 수행하는 등의 작업을 수행할 수 있다.

 

[Axios] interceptor 동작원리

axios의 interceptor의 동작원리를 분석해보았습니다.

www.timegambit.com

 

 

4. headers의 content-type 지정

일반적으로 'Content-Type' 헤더는 POST 또는 PUT 요청과 함께 사용한다. 서버에 전송되는 데이터의 형식을 명시하기 위해 'Content-Type' 헤더를 사용한다. 예를 들어, JSON 형식으로 데이터를 전송하는 경우 'Content-Type' 헤더는 'application/json'으로 설정될 수 있다.

다른 예로, 웹 양식을 통해 전송되는 데이터의 형식을 지정하기 위해 'Content-Type' 헤더가 사용될 수 있다. 웹 양식에서 파일을 업로드하는 경우 'multipart/form-data'로 설정될 수 있다.

'Content-Type' 헤더는 요청을 처리하는 서버에서 전송된 데이터를 올바르게 해석하고 처리하기 위해서다. 따라서 요청의 데이터 유형에 따라 적절한 'Content-Type' 값을 설정하는 것이 중요하다.

하지만 반드시 모든 요청에 지정해 주어야 하는 것은 아니다. 기본적으로 서버에서는 일반적인 데이터 타입이 정해져 있기 때문이다.하지만 데이터 형식이 다른 경우 'Content-Type'을 명시하는 것이 좋다. 예를 들어 JSON 데이터를 전송하는 경우, JSON 데이터를 전송할 때는 'Content-Type'을 'application/json'으로 설정하여 서버가 데이터를 올바르게 이해하고 처리할 수 있도록 해야 한다.

 

5. 어째서 우리는 axios로 서버와 데이터 통신할 때, 데이터를 json으로 변환하여 서버에 전송해주지 않아도 되는가?

=> Axios는 자동으로 객체를 JSON으로 직렬화하여 요청 본문에 포함시키기 때문이다.

 

6. 'Access-Control-Allow-Origin' 헤더를 설정하는 이유

const instance = axios.create({
  baseURL: process.env.REACT_APP_SERVER,
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
});

클라이언트에서 모든 출처에서의 요청을 허용하도록 지정해주기 위해서다. 클라이언트에서 CORS를 방지할 수 있는 방법 중 하나인 것 같다. 하지만 보안상의 이유로 신뢰할 수 있는 서버에서만 이를 허용하는 것이 좋다. 

'Access-Control-Allow-Origin' 헤더를 클라이언트에서 설정하는 것은 서버에서 허용할 출처를 명시하는 것이다. 하지만 서버에서도 실제로 해당 헤더를 응답에 포함하여 클라이언트의 요청을 허용해야 CORS 에러가 발생하지 않는다.

CORS 에러를 방지하기 위해서는 클라이언트와 서버 양쪽에서 조치가 필요하다.

1. 클라이언트 측:클라이언트에서는 요청 헤더에 'Access-Control-Allow-Origin' 값을 설정할 수 있습니다. 이는 서버에게 요청을 보낼 출처를 명시적으로 알려주는 역할을 합니다. 하지만 클라이언트에서 이 헤더를 설정한다고 해서 CORS 에러를 해결할 수는 없습니다. 서버가 실제로 해당 출처를 허용하는지 확인해야 합니다.

2. 서버 측:서버에서는 실제로 CORS 정책을 설정해야 합니다. 'Access-Control-Allow-Origin' 헤더를 포함하여 클라이언트의 요청 출처를 허용하도록 응답 헤더를 설정해야 합니다. 이를 통해 클라이언트로부터의 CORS 요청을 처리하고, 허용할 출처를 명시적으로 지정할 수 있습니다.

클라이언트와 서버 간의 출처 관련 설정은 서버 측에서 수행되어야 한다. 서버에서 출처를 허용하도록 'Access-Control-Allow-Origin' 헤더를 설정하고, 필요에 따라 다른 CORS 관련 헤더도 설정하여 CORS 에러를 방지할 수 있다.

 

 

 

7. interceptors의 request요청에서 if (config.headers === undefined) return config;를 사용한 이유

const instance = axios.create({
  baseURL: process.env.REACT_APP_SERVER,
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
});

instance.interceptors.request.use((config) => {
  if (config.headers === undefined) return config;

 

instance에서는 headers를 기본적으로 설정해 놓고 있다. 따라서 일반적으로는 해당 구문이 실행되지 않을 것이다. 하지만 예기치 않게 undefined로 설정되는 경우를 대비하여(headers설정을 빼먹었다던지..?) if (config.headers === undefined) return config; 구문을 사용하여 코드의 안정성을 높이고, 요청 인터셉터에서 발생할 수 있는 잠재적인 에러를 방지하기 위해 사용했다.

if (!config.headers)로 변경하여 사용할 수 있을까?

참고, 일반적으로 config.headers는 undefined가 아니라 기본값인 빈 객체 {}로 설정되어 있다.

if (config.headers === undefined)는 엄격한 비교를 수행한다. 반면에 if (!config.headers)config.headers 값이 거짓으로 평가되는 모든 값을 포함한다. JavaScript에서 거짓으로 평가되는 값은 undefined, null, false, 0, NaN, '' (빈 문자열)이다. 따라서 config.headers가 이러한 값을 가지고 있다면 if (!config.headers) 조건문은 참이 된다. 따라서 개발자의 명확한 의도를 전달하기 위해서는 if (config.headers === undefined)를 사용하는 것이 옳다고 판단된다.

 

 

8. headers에 추가되는 과정 => headers의 값들이 초기화되는게 아니라 계속 추가되어 전송된다.

const postAdd = async (postData) => {
  const config = {
    headers: { // 1. 헤더에 추가해 요청
      "content-type": "multipart/form-data",
    },
  };
  try {
    const response = await instance.post("/api/posts", postData, config);
    return response.data;
  } catch (error) {
    throw error;
  }
};


const instance = axios.create({
  baseURL: process.env.REACT_APP_SERVER,
  headers: {
    "Access-Control-Allow-Origin": "*", //2. 기본적으로 헤더에 추가됨
  },
});

instance.interceptors.request.use((config) => {
  if (config.headers === undefined) return config; // 기존 헤더가 undefined가 아닐 경우 토큰이 추가되어 전달됨
  const accessToken = sessionStorage.getItem("accessToken");
  const refreshToken = sessionStorage.getItem("refreshToken");
  const userId = sessionStorage.getItem("userId");
  if (accessToken || refreshToken) {
    config.headers["accessToken"] = accessToken;
    config.headers["refreshToken"] = refreshToken;
    config.headers["userId"] = userId;
  }
  return config;
});

 

 

목표 달성 여부,

1. 로그인 기능 구현 진행중 (컴포넌트 단의 로그인 구현과 api등은 완료했으나 액세스 토큰 재발급과 Refresh 토큰 사용 등 서버 api를 확인해야 할 부분이 있어 내일 진행 예정, 추가로 ESLint에서 에러들이 꽤 발생하여 내일 확인하여 수정해볼 예정) ✅

2. intercepter에 대한 이해 

 

느낀 점,

이 전까지 로그인 기능 구현을 계속 담당해 왔지만 내가 왜 이 기술을 이렇게 사용했는가? 에 대해 확실한 정리는 해본 적이 없는 것 같다. 그냥 이런 기능이 있기 때문에 이렇게 사용하면 좋겠네... 했었는데 그동안 궁금했던 intercepter에 대한 정리와 이해를 하면서 왜 내가 이 기능을 이렇게 적용했는지 확실히 정리하게 되었다. 이제부터는 명확히 사용하는 이유를 생각해 보고 목적과 의도에 맞게 생각하면서 사용하도록 해야 겠다.