본문 바로가기
개발자 도전기/[STUDY] JS || TS

node.js | axios | 엑시오스 더 깊게 이해하고 사용하자..!

by 답수 2022. 10. 7.
728x90
SMALL

 

테스트 코드를 작성하면서 막혔던 부분이 있었다. 현재 E2E TEST를 진행 중이고, 유저 플로우에 맞게 시나리오를 작성하고 있다.

 

유저 정보 변경 테스트에서 발생한 에러

 

회원가입, 패스워드 설정, 로그인, 로그아웃, 그리고 회원탈퇴까지 기본적인 틀을 먼저 작성했다. 즉 CRUD의 C와 D를 먼저 작성한 셈인데, 테스트를 위한 계정을 생성하고 삭제하는 프로세스를 통해 개발자가 직접 DB를 건드리는 일을 없애기 위해서다.

 

여하튼! 나름 수월하게 업무 중이었는데 회원 정보 수정하는 부분을 테스트하는데 에러가 발생했다. 에러를 잡기 위해 4시간 이상을 매달렸는데 도저히 에러 원인을 찾을 수 없었다(더군다나 e2e 테스트다 보니, 에러를 찾기 위해서 여러 레이어들을 뒤졌어야 했다).

 

이곳저곳 로그를 찍어가며 문제가 되는 부분들을 추리해갔고, 그러다 axios의 메소드들을 찾아봤다. 정말 어처구니없게도 에러의 원인은 메소드별 config 속성이 다르다는 것이었다..

 

const request = axios({ baseURL: "http://localhost:3000/api/auth" });
// ...

  // ...
    it("상태코드 200 반환", async () => {
      try {
        const { status } = await request.delete("/users/me", {
          headers: { authorization },
          data: { withdrawalReason: "[TEST] DELETE /api/auth/users/me" },
        });
        expect(status).to.equal(200);
      } catch (error) {
        expect.fail(error);
      }
    });
  // ...

DELETE 메소드 같은 경우 하나의 객체 안에 헤더와 데이터를 모두 포함하여 요청한 반면,

 

  // ...
    it("상태코드 200 반환", async () => {
      try {
        const { status } = await request.put(
          "/users/me",
          { ...USER_INFO.VALID_VALIDATOR },
          {
            headers: { authorization },
          }
        );
        expect(status).to.equal(200);
      } catch (error) {
        expect.fail(error);
      }
    });
  // ...

PUT은 데이터 객체, 헤더 객체를 분리해서 요청해야 한다. 하지만 이런 차이를 몰랐던 나는 PUT메소드도 DELETE와 같은 형식으로 보냈기 때문에 발생했던 에러였다. 

 

만약 axios에 대해 조금 더 지식이 있었더라면 이렇게 많은 시간 동안 삽질하지 않았을 것이다. 기왕 이렇게 삽질한거, axios를 더 잘 사용하기 위해 메소드나 기능들 공부한다는 생각으로 글을 남긴다.

 

 

AXIOS

Axios: 브라우저, Node.js를 위한 Promise API를 활용한 HTTP 비동기 통신 라이브러리

axios는 http 요청과 응답을 JSON형태로 자동 변경하고 많은 브라우저에서 지원하기 때문에 확장성 측면에서 유리한 라이브러리다. axios의 특징에 대해서 간단하게 요약하자면 다음과 같다.

  • csrf 보호를 위한 클라이언트 사이드 지원
  • data 속성을 통해 http body의 데이터 보냄
  • 자동으로 JSON 형식으로 변환(stringify() 사용 안 해도 됨)
  • 요청 취소가 가능하고 타임아웃 걸 수 있음
  • 요청/응답에 대해 차단 가능(Intercept)

 

사용법

axios는 promise를 기반으로 통신하기 때문에 다음과 같이 사용할 수 있다.

axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })
  .then(function () {
    // ...
  });

 

이를 async/await를 활용하여 더 간단하게 표현할 수도 있다. 다만 async/await같은 경우 오래된 브라우저에서는 지원하지 않는다.

async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

 

axios의 장점으로 axios.all() 메소드를 이용하여 여러 개의 요청을 동시에 수행할 수 있다.

function getUserAccount() {
  return axios.get('/user/12345');
}

function getUserPermissions() {
  return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    // Both requests are now complete
  }));

 

 

Request 파라미터 옵션

다양한 옵션들이 존재하지만, 일하면서 자주 사용할만한 것들을 정리하려고 한다.

 

1. method: GET/POST/PUT/DELETE 등의 http 요청 방식. axios 모듈을 보면 다음과 같은 메소드들이 있다.

export type Method =
  | 'get' | 'GET'
  | 'delete' | 'DELETE'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
  | 'purge' | 'PURGE'
  | 'link' | 'LINK'
  | 'unlink' | 'UNLINK';

 

2. url: 요청 보낼 서버 주소

 

3. basURL: url을 상대경로로 사용할 때 사용. 사용의 예시를 들자면

const request = axios.create({
  baseURL: "http://localhost:3000/api/auth",
  headers: { referer: "localhost" }
});

request라는 엑시오스 인스턴스를 만들었을 때, .../api/auth/login 라는 api를 요청할 일이 있다면

        await request.post("/login", {
          email: "test@google.com",
          password: "1234",
        });

이렇게 메소드를 이용하여 요청하면 된다. 즉 확장성을 고려했을 때 매우 유용한 옵션이다.

 

4. data: body에 보내는 데이터

 

5. headers: 요청 헤더에 담을 데이터

 

6. params: URL파라미터 사용

        await request.get("/users", 
          params: {
            id: "goood",
            limit: 100",
        );

이렇게 params  옵션을 사용한다면 .../users?id=goood&limit=100  이라는 URL로 요청하게 된다.

 

7. timeout: 요청 타임아웃이 발동되기 전 ms의 시간을 요청하고, timeout보다 요청이 길어지면 요청 취소

 

이 외에 auth, responseType 등의 기능들이 많지만, 다른 방식으로 우리 회사에 맞게 사용할 것이기 때문에 일단 요청은 여기까지 정리!

 

Response 스키마

axios를 통해 요청을 서버에 보내면 .then()의 콜백함수로 데이터를 받을 수 있다.

{
  // `data`는 서버가 제공한 응답(데이터)입니다.
  data: {},

  // `status`는 서버 응답의 HTTP 상태 코드입니다.
  status: 200,

  // `statusText`는 서버 응답으로 부터의 HTTP 상태 메시지입니다.
  statusText: 'OK',

  // `headers` 서버가 응답 한 헤더는 모든 헤더 이름이 소문자로 제공됩니다.
  headers: {},

  // `config`는 요청에 대해 `axios`에 설정된 구성(config)입니다.
  config: {},

  // `request`는 응답을 생성한 요청입니다.
  // 브라우저: XMLHttpRequest 인스턴스
  // Node.js: ClientRequest 인스턴스(리디렉션)
  request: {}
}

최근에 내가 사용했던 예시로는, 로그아웃 테스트케이스를 작성할 때, 성공했을 때 상태코드 200을 반환하는 것을 기대할 때 status메소드를 사용했다.

    it("상태코드 200 반환", async () => {
      try {
        const { status } = await request.post("/logout");
        expect(status).to.equal(200);
      } catch (error) {
        expect.fail(error);
      }
    });

 

HTTP 메소드

내가 고생하고 이 글을 쓰는 가장 큰 이유... 요청 메소드는 편의를 위해 메소드에 별칭이 제공된다. 별칭 메소드를 사용하면 url, method, data같은 속성을 따로 지정할 필요가 없다.

axios.get(url[, config])            // GET
axios.post(url[, data[, config]])   // POST
axios.put(url[, data[, config]])    // PUT
axios.patch(url[, data[, config]])  // PATCH
axios.delete(url[, config])         // DELETE

 

.create() 메소드로 엑시오스 인스턴스를 생성했을 때, 사용할 수 있는 인스턴스 메소드는 다음과 같다.

axios.get(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
axios.delete(url[, config])
axios.request(config)
axios.head(url[, config])
axios.options(url[, config])
axios.getUri([config])

 

내가 삽질했던 그 코드를 다시 보자.

const request = axios({ baseURL: "http://localhost:3000/api/auth" });
// ...

    // ...
    it("상태코드 201 반환", async () => {
      try {
        await request.post("/signup", { ...SIGNUP_PARAM }).then(res => {
          expect(res.status).to.equal(201);
        });
      } catch (error) {
        expect.fail(error);
      }
    });
    // ...

    // ...
    it("상태코드 200 반환", async () => {
      try {
        const { status } = await request.put(
          "/users/me",
          { ...USER_INFO.VALID_VALIDATOR },
          {
            headers: { authorization },
          }
        );
        expect(status).to.equal(200);
      } catch (error) {
        expect.fail(error);
      }
    });
    // ...

  // ...
    it("상태코드 200 반환", async () => {
      try {
        const { status } = await request.delete("/users/me", {
          headers: { authorization },
          data: { withdrawalReason: "[TEST] DELETE /api/auth/users/me" },
        });
        expect(status).to.equal(200);
      } catch (error) {
        expect.fail(error);
      }
    });
  // ...

POST와 PUT 메소드 같은 경우, data 별칭을 사용하기 때문에 data를 따로 적지 않았다. 또한 headers는 data 외의 config이기 때문에 data 객체 밖에서 다른 객체로 만들어야 한다.

 

반면 DELETE는 data 별칭이 없기 때문에 data를 명시했고, 하나의 config 객체 내에 data와 headers를 포함시켰다. 

 

인터셉터

then 이나 catch를 사용하여 처리되기 전에 요청이나 응답을 가로챌 수 있다. 가로챈다는 말이 잘 이해되지 않았는데, 내가 이해한 바로는 요청을 보내거나 응답을 받을 때, 그 전에 데이터를 내가 임의로 가공할 수 있다는 것이다. 즉 '요청을 보내기 직전'과 '응답을 받은 직후'에 인터셉터를 통해 핸들러를 관리할 수 있다.

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

// 응답 인터셉터 추가
axios.interceptors.response.use(
  function (response) {
    // 응답 데이터를 가공
    // ...
    return response;
  },
  function (error) {
    // 오류 응답을 처리
    // ...
    return Promise.reject(error);
  });

 

추후 인터셉터를 제거할 때는 다음과 같이 수행하면 된다.

const myInterceptor = axios.interceptors.request.use(function () { /*...*/ });
axios.interceptors.request.eject(myInterceptor);

 

아직까지 인터셉터 기능을 사용해본 경험이 없지만, 잘만 사용한다면 매우 유용할 것 같다. 예를 들면 공통적으로 사용하는 헤더를 요청 직전에 추가한다거나 서버로 보낸 요청이 실패했을 때 내가 작성한 인터셉터 핸들러에서 어떠한 핸들러를 동작하게 한다던가? 이부분은 차근차근 더 공부해봐야 할 것 같다.

 

728x90
LIST

댓글