Axios

npm을 통해서 설치해야하는 모듈이다. 이 모듈은 자바스크립트 내장 fetch와 마찬가지로 promise객체를 기반으로 통신을 처리하는데, 다음과 같은 특징을 가지고 있다.

  • json변환을 자동으로 처리(request.json()을 해줄 필요가 없음. 바로 data 프로퍼티에 접근이 가능하다.
  • 요천에 대한 timeout기능이 존재(응답이 오기까지 대기할 최대시간을 설정할수 있다)
  • 요청 및 응답에 대한 인터셉트 기능이 존재한다(기본적으로 요청을 보내기 전 혹은 응답을 받은 후 실행될 코드를 지정해놓을 수 있음)

근데 굳이 얘를 왜 써?

자바스크립트는 자체적으로 특정한 서버에 네트워크 요청을 보내고, 해당서버가 가지고있는 정보나 데이터를 받아노느 기능을 가지고 있다.

네트워크 요청은

  • 쇼핑몰에서 주문요청
  • 로그인
  • 게시글 열람

등등...
웹사이트를 이용하는 동안에 수십번씩 일어난다

근데 왜 굳이 한페이지에 모든 데이터를 가직 ㅗ있지 않고, 특정 서버에 요청을 보내면서 데이터를 받아올까?

이해하기 전에 프론트엔드백엔드는 다음과 같은 역할을 가지고 있다는 걸 알아야한다.

프론트엔드(사용자가 보는 홈페이지 구현, 사용자는 홈페이지에서 각종 데이터를 읽거나 요청함) -> 클라이언트

백엔드 (사용자와 관련한 데이터를 보유하고 요청을 처리한다) -> 서버

만약에 사용자가 서버를 거치지 않고 직접 데이터를 등록하고 수정할수 있다면 허위 정보가 생성될 위험이 존재한다.

따라서 사용자가 어떤 동작을 하고싶을때 그럴수 있는 권한이 있는지, 사이트에 등록된 사용자가 맞는지를 확인하고 거기제 맞춰서 정보를 전달하는 것이다.

또한 사용자가 '모든 사용자'와 관련된 정보를 가지거나 함부로 중요한 정보에 접근 할수 없도록 정보 저장소에 보관해야하는데, 이 정보 저장소가 바로 DB 서버가 된다.

데이터의 보안을 높이기 위해서 프론트엔드와 백엔드가 나뉘어서 개발이 되는 것이다.

그럼 써보자

yarn add axios

모듈이니까 당연히

import axios from 'axios'

와 같이 import를 꼭 해줘야한다.

axios 함수의 기본적인 사용형태는 이렇다

axios.메소드(서버 URL 주소, 요청 데이터)

axios.get/ axios.post 와 같이 작성해서 요청을 보낼수도 있고,

axios({각종 옵션들, url 주소, 요청 데이터 등})

기본 옵션은 아래와 같다

axios({
	url: "서버주소",
  method: "get", // POST, PUT, DELETE 등의 요청 유형 선택
  headers: {
    Authorization: 인증 토큰 등
  },
  data: 리퀘스트 데이터
});

자바스크립트 fetch랑 거의 비슷하다.

url에 요청을 보내고 돌아오는 응답은 다음과같은 프로퍼티가 존재한다.

  • status - HTTP 상태 코드 (200~299 사이면 요청 성공, 400~500 사이라면 요청 실패)
  • statusText - 서버에서 전달된 상태 메세지 (ok 등이 올 수 있음)
  • headers - 응답의 헤더 부분
  • request - 응답을 생성한 request
  • data - 응답의 데이터가 담긴 부분
  • config - axios 자체의 설정

프로미스와 async/await

서버에 데이터를 달라는 요청을 했다면, 그 응답이 올때까지 기다렸다가 작업을 진행해야한다.

왜? 그게 언제 될줄알고 그것만 넋놓고 기다리면서 다른 작업을 안하고 있는 것은 너무 비효율적이기 때문이다.

비동기 동작을 하는 코드의 경우, 코드가 완전히 실행-완료된 후에 다음 코드가 실행되는 것이 아니기 때문에 순서를 보장할수 없는 문제가 있고, 그러기 때문에 특정한 코드의 동작이 완료될 때까지 기다렸다가 다음 작업을 진행하도록 도와주는 것이 바로 promise 객체이다.

promise 객체는 특정한 코드의 수행이 완료되면, 그 수행이 성공했는지, 실패했는지에 따라서 다른 값을 반환해주는 객체이다.

이 객체를 활용해서 아래와 같이 코드를 작성할수 있다.

promise.then(result => {
  // 코드가 성공적으로 수행 완료될 경우 실행될 코드를 명시
  // 성공한 결과값 (result = 응답, 데이터) 를 활용할 수 있음
}).catch(error => {
  // 코드의 수행이 실패한 경우 실행될 코드를 명시
  // 에러 객체 (error) 를 활용할 수 있음
})

이걸 왜 작성하냐면, axios 함수는 프로미스 객체를 반환하기 때문이며, 프로미스 객체가 있다면 then,catch문법을 쓸수 있다.

axios.get('서버url')
  .then(response => {요청 성공 시 실행할 코드})
  .catch(error => {에러 핸들링})

이것도 좋지만 가독성을 고려할때는 async await를 사용하는 것이 좋다. 이 문법은 promise.them / catch를 사용하지 않고도 프로미스를 처리할수 있게 해준다.
아래와 같이 만들수 있다.

const getData = async () => {
    const response = await axios.get('서버주소') // 응답이 올때까지 기다렸다가 response 에 저장
    // 여기서부터는 response 를 동기적으로 사용할 수 있게 됨
}

만약 에러 핸들링을 하고 싶으면 그때는 try catch 구문을 사용해야한다.

const getData = async () => {
    try {
        const response = await axios.get('서버주소')
        // 응답과 관련한 코드
    } catch (err) {
        // 위에서 발생한 에러를 전부 여기서 처리
    }
}

Axios 연습

한번 연습해보자. 이렇다 할 백엔드가 없기 때문에 jsonplaceholder 라는 가짜 백엔드를 사용해서 요청을 보내보자.

https://jsonplaceholder.typicode.com/ 해당 사이트로 요청을 보낼 것이다.

Example 컴포넌트를 먼저 생성하고

import React, { useEffect } from 'react'
import axios from 'axios'

function Example() {
    return <div></div>
}

export default Example

get

post들을 가져온다.

const getPosts = async () => {
    const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
    console.log(response.data) // .data 라고 해야 데이터에 접근
}

useEffect(() => {
    getPosts()
}, [])

이렇게 해주면 렌더링 될때 바로 요청을 보내서 useEffect 안의 내용을 실행한다.

컴포넌트에 접근하니까 바로 이렇게 데이터가 콘솔창에 나타난다!

근데 나는 하나만 가져오고 싶은데?

그러면 id값을 추가해주면 된다!

const getPost = async (id) => {
    const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
    console.log(response.data)
}

getPost(원하는 아이디값)

post

글을 작성하기 위한 post 요청을 보내보자

작성한 글을 인자로 받도록 함수를 작성하고, 해당 글을 axios 함수를 실행할 때의 2번째 인자로 명시해줘야한다.

const createPost = async (post) => {
    const response = await axios.post(`https://jsonplaceholder.typicode.com/posts`, post)
    console.log(response.data)
}

useEffect(() => {
    createPost({ title: '점심', body: '짬뽕' })
}, [])

put

put``patch method는 이미 있는 데이터를 수정하기 위해서 보내는 요청이다.

post와 거의 동일하게 작성하되, 데이터를 수정하려면 정확히 서버에 있는 어떤 데이터를 수정하고자 하는 것인지를 명시해서 보내줘야한다.

어떤 id를 가진 항목을 수정할 것인가?

const updatePost = async (post, id) => {
    const response = await axios.put(`https://jsonplaceholder.typicode.com/posts/${id}`, post)
    console.log(response.data)
}

useEffect(() => {
        updatePost({ title: '점심', body: '볶음밥으로 결정' }, 1)
    }, [])

delete

말 그대로 서버에 있는 데이터를 제거하는 것이며, 당연히 id값을 프로퍼티로 전달해줘야한다.

const deletePost = async (id) => {
    const response = await axios.delete(`https://jsonplaceholder.typicode.com/posts/${id}`)
    console.log(response.data)
}

useEffect(() => {
    deletePost(1)
}, [])

첫번째 값이 제거되어 빈 괄호만 나오는 것을 알수 있다.

get 활용하기

특정 게시글을 보고싶다.

getPost(id) 메소드를 활용해서 얻은 데이터를 활용해서 구현한다.

    const [post, setPost] = useState({ title: '', body: '' })

    const getPost = async (id) => {
        const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
        return response.data
    }

    const setPostByResponse = async (id) => {
        const data = await getPost(id)
        setPost(data)
    }

    useEffect(() => {
        setPostByResponse(1)
    }, [])

    return (
        <div>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
        </div>
    )
}

이렇게 해주면 1번째 id값에 해당하는 글의 제목과 내용이 나타난다

post 활용하기

글 제목이랑 내용을 써서 새로운 글을 만들고 싶다.

  • input값을 담는 userInput state를 생성
  • userInput을 사용자가 입력한 값으로 setUserInput을 호출하는 핸들러 생성
  • 버튼을 누르면 createPost 메소드가 호출되게 한다.
import React, { useEffect, useState } from 'react'
import axios from 'axios'

function Example() {
    const [userInput, setUserInput] = useState({ title: '', body: '' })

    const createPost = async (post) => {
        const response = await axios.post(`https://jsonplaceholder.typicode.com/posts`, post)
        return response.data
    }
    const userInputHandler = (e) => {
        const { name, value } = e.target
        setUserInput({ ...userInput, [name]: value })
    }

    return (
        <div>
            <input name="title" value={userInput.title} onChange={userInputHandler} />
            <input name="body" value={userInput.body} onChange={userInputHandler} />
            <button onClick={() => createPost(userInput)}>작성하기</button>
        </div>
    )
}

export default Example

Axios 설정하기

요청 config 활용하기 (timeout과 에러 핸들링)

위에서 post 부분에서 진행했던 코드를 활용해서, 요청 config를 활용하는 방법에 대해 알아보자.

const createPost = async (post) => {
        const response = await axios.post(`https://jsonplaceholder.typicode.com/posts`, post)
        console.log(response.data)
    }

createPost 함수를 config 옵션을 명시하는 방식으로 수정할수 있다.

const createPost = async (post) => {
    const response = await axios({
        url: `https://jsonplaceholder.typicode.com/posts`,
        method: 'post', // get, put, delete 등의 method 를 명시
        data: post,
    })
    return response.data
}

이 둘중에서 뭘 사용할지는 본인 자유이지만, 명시해야할 옵션이 많은 경우에는 아래처럼하되, url과 data만 다르게 명시하는 경우에는 위처럼 작성하는 것이 일반적이다.

-timeout 기능 : 적힌 시간 값안에 요청이 안오면 시간초과 발생

const createPost = async (post) => {
    const response = await axios({
        url: `https://jsonplaceholder.typicode.com/posts`,
        method: 'post', // get, put, delete 등의 method 를 명시
        data: post,
        timeout: 1, // 밀리초 단위 (명시한 시간 안에 요청이 안오면 시간초과 발생)
    })
    return response.data
}

위처럼 timeout을 1 밀리초로 해놓으면 아래와 같이 아래와 같이 취소가 된다.

그리고 타임아웃에 따른 에러 핸들린도 가능하다

const createPost = async (post) => {
    try {
        const response = await axios({
            url: `https://jsonplaceholder.typicode.com/posts`,
            method: 'post', // get, put, delete 등의 method 를 명시
            data: post,
            timeout: 1, // 밀리초 단위 (명시한 시간 안에 요청이 안오면 시간초과 발생)
        })
        return response.data
    } catch (err) {
        console.log(err)
    }
}

오류가 발생하게 되면 err를 콘솔로 전달해줄수 있고, 혹은 alert를 사용해서 사용자에게 오류를 알릴수 있다.

config 기본값 지정하기 ( + 환경변수 불러오기)

보통 백엔드 주소는 고정되어이씩 때문에, 해당 주소를 요청 보낼 때마다 명시하는 것은 비효율적이다.
해당 주소를 기본값으로 지정해놓고, 요청을 보낼때 route 부분만 명시하는 방식으로 작성하면 효율적일 것이다.

이를 구현하는 방법은 여러가지가 있다

  • main.jsx에 axios.defaults 설정

axios.defaults.옵션이름 = 값

과 같은 코드를 main.jsx에 명시하면, 이제 어떤 컴포넌트에서 axios를 사용해도 위 코드대로 설정한 갑싱 유지된다.

서버의 기본 주소를 설정하는 옵션의 이름은 baseURL이다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import axios from 'axios'

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com'

ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
)

이렇게 해주면 컴포넌트에서는 아래와 같이 사용이 가능해진다.

const createPost = async (post) => {
    const response = await axios.post(`/posts`, post)
    return response.data
}
  • config가 설정된 axios 객체 만들기 (인스턴스)
    a

만약에 서비스 규모가 커서, 백엔드의 종류가 2개라면...?

utils/axiosConfig.js경로로 파일을 하나 만든다.

import axios from 'axios'

export const axiosSocket = axios.create({
    baseURL: '또 다른 서버 주소',
})
export const axiosAPI = axios.create({
    baseURL: 'https://jsonplaceholder.typicode.com',
})

이렇게 해주고 axios.create(옵션객체)를 작성하면, 해당 옵션이 적용되어있는 axios 객체를 생성할수 있게된다.

그러면 다음부터는 본인 입맛에 맞는 axios객체를 활용해서 곧바로 옵션이 적용된 요청을 보낼수 있다.

import { axiosAPI } from '../utils/axiosConfig'

const createPost = async (post) => {
    const response = await axiosAPI.post(`/posts`, post)
    return response.data
}
  • 환경변수 적용하기

서버주소를 환경변수 형태로 저장해놓고 ,필요할 때 가져와서 사용하는 방식으로도 코드를 작성할 수 있다.

서버주소는 특별한 사정이 없다면 변할 리가 없기때문에, 환경변수에 마치 상수처럼 저장해놓고 사용하는 것이 일반적이다.

환경변수는 어떤 환경이냐에 따라 설정하는 방식이 다른데, vite의 경우네는 아래와 같이 사용한다.

  1. .env파일을 가장 상위루트에 만든다.(.env.production혹은 .env.local등의 이름으로 실행환경 별 환경본수를 설정 가능)
  2. .env 파일 내부에 접두어 VITE_를 붙여서 변수를 작성한다.
  3. 앱 파일 내부에서는 import.meta.env.(작성한 변수명)의 형태로 가져와서 사용한다.

이제 axios 사용방식에 맞춰서, import.meta.env.VITE_SERVER_URL을 서버 주소로 명시해서 코드를 작성하면 된다.

방금까지 진행하던 방식에 맞춰서, axiosConfig.js 파일 내부의 axiosAPI를 아래와 같이 수정한다.

export const axiosAPI = axios.create({
    baseURL: import.meta.env.VITE_API_URL,
})

인터셉터 활용하기

인터셉터는 말그대로 요청 혹은 응답을 보내는 과정에서 요청 혹은 응답을 가로채고 특정한 작업을 수정하기 위한 기능이다.

인터셉터는 request를 다를수 있고

axios.interceptors.request.use(request => { // request.use 라고 했기 때문
    console.log(request);
    // request 관련해서 설정 진행
    return request;
}, error => {
    console.log(error);
    return Promise.reject(error);
});

response를 다룰 수도 있습니다.

axios.interceptors.response.use(response => { // response.use 라고 했기 때문
    console.log(response);
    // request 관련해서 설정 진행
    return response.data
}, error => {
    console.log(error);
    return Promise.reject(error);
});

보통은 request를 보낼 때에는 유저인증 정보 존재 여부에 따라 유저 인증 정보를 요청에 포함시키기 위해 사용하고, response를 받을 때에는 데이터 파싱 등을 처리하기 위해 사용한다.

createPost를 호출하면 request 내용과 출력 결과물을 확인할수도 있다. 이때 response결과를 data 값만 리턴에가 인터셉트를 하면 컴포넌트에서도 작성할때 data를 써줄 필요가 없다.


    const createPost = async (post) => {
        const response = await axios.post(`/posts`, post)
        console.log(response)
    }

이렇게만 해줘도 데이터가 나타나게 된다.

만약에 response 유형에 따라서 처리하는 과정을 기본적으로 내장해주고 싶으면?

axios.interceptors.response.use(
    (response) => {
        if (response.status === 200 || response.status === 201) {
            return response.data
        }
        return response
    },
    (error) => {
        console.log(error)
        return Promise.reject(error)
    },
)

status의 값에 따라서 리턴되는 response의 형태를 정한다.

Axios 더 깊이 활용해보기

params 전달하기

api에 따라, 조건을 걸어서 데이터를 가져오는 것이 가능하다.

현재 사용중인 https://jsonplaceholder.typicode.com/ 서버는 글을 가져올 때 userId 등의 조건을 걸어서 글을 가져올수 있도록 설계되어있다.

우선 getPost함수에서 주소를 명시한 다음 부분에 params라고 해서 조건을 명시해보자.

이후부터 작성되는 코드 내용에서 인터셉터를 통해 data가 자동으로 리턴되기 때문에 response 만 작성해도 data가 리턴된다는 사실을 알린다.

const getPosts = async () => {
    const response = await axios.get(`/posts`, {
        params: { userId: 1 },
    })
    return response
}

이렇게 작성하면 userId가 1인 게시글만 가져오게 된다.

example 컴포넌트에서 코드를 작성해서 , 실제로 userId가 1인 게시글만 가져오게 할수 있다.

import React, { useEffect, useState } from 'react'
import axios from 'axios'

function Example() {
    const [posts, setPosts] = useState([])

    const getPosts = async () => {
        const response = await axios.get(`/posts`, {
            params: { userId: 1 },
        })

        return response
    }
    const setPostsByResponse = async () => {
        const data = await getPosts()
        setPosts(data)
    }

    useEffect(() => {
        setPostsByResponse()
    }, [])

    return (
        <div>
            {posts.map((post) => {
                return (
                    <div key={post.id}>
                        <h1>{post.title}</h1>
                        <p>작성자: {post.userId}</p>
                        <p>{post.body}</p>
                    </div>
                )
            })}
        </div>
    )
}

export default Example

만약 파라미터가 복잡해지는 경우에는 어떻게 할까?
qs 라는 쿼리 파라미터를 처리하는 모듈을 설치하고,

yarn add qs

아래와 같이 paramSerializer라는 옵션을 설정해줘야한다. (현재같은 경우에는 main.jsx에 작성하면 된다.)

import qs from "qs";

axios.defaults.paramsSerializer = {
    serialize: (params) => Qs.stringify(params, { arrayFormat: 'brackets' }),
}

로딩/에러 관리하기

백엔드에 요청을 보낸다면, 항상 응답이 오기까지는 시간이 소요된다. 그동안에 그냥 빈화면을 보여주기 보다는, 로딩 중이라는 것을 이용자한테 표시해준다면 더욱 자연스러운 UX를 만들수 있을 것이다.

보통은 로딩중인지의 여부, 에러가 발생했는지의 여부, 에러객체 등을 저장해놓고 필요할때 사용한다.

우선 로딩 여부와 에러객체를 저장하기 위한 state를 생성한다.

const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)

그리고 getPost 함수를 아래와 같이 작성한다.

const getPosts = async () => {
    try {
        setIsLoading(true)
        const response = await axios.get(`/posts`, {
            params: { userId: 1 },
        })
        return response.data
    } catch (err) {
        setError(err)
        setIsLoading(false) // 에러가 발생했더라도 로딩 중단
    }
}

그리고 setPosts가 되면 로딩을 중단하도록 함수를 수정한다.

const setPostsByResponse = async () => {
    const data = await getPosts()
    setPosts(data)
    setIsLoading(false) // setPosts 가 되면 로딩 중단
}

렌더링을 할때 조건문을 걸어서 로딩중과 오류가 발생할때 다른 텍스트가 나타나게 한다.

최종 코드는 이렇고

import React, { useEffect, useState } from 'react'
import axios from 'axios'

function Example() {
    const [posts, setPosts] = useState([])
    const [isLoading, setIsLoading] = useState(false)
    const [error, setError] = useState(null)

    const getPosts = async () => {
        try {
            setIsLoading(true)
            const response = await axios.get(`/posts`, {
                params: { userId: 1 },
            })
            return response.data
        } catch (err) {
            setError(err)
        }
    }
    const setPostsByResponse = async () => {
        const data = await getPosts()
        setPosts(data)
        setIsLoading(false)
    }

    useEffect(() => {
        setPostsByResponse()
    }, [])

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>{error.message}</div>
    return (
        <div>
            {posts.map((post) => {
                return (
                    <div key={post.id}>
                        <h1>{post.title}</h1>
                        <p>작성자: {post.userId}</p>
                        <p>{post.body}</p>
                    </div>
                )
            })}
        </div>
    )
}

export default Example

지금은 로딩시간이 아주 짧기 때문에 네트워크 속성에서 느린 3G로 설정하고 새로고침을 하면 Loading...이라는 텍스트를 볼수 있다.

그리고 axios url 경로를 엉터리로 작성해서 404 오류를 만들면 결과값 대신 오류메시지가 나타난다.

axios 요청을 위한 커스텀 훅 만들어보기

하나의 컴포넌트에서 axios 요청을 처리하고, 저장하는 기능까지 담당하려고 하니 코드가 길고 지저분하다.

axios 요청을 보내고 응답을 처리하는 과정을, 하나의 함수로 만들어놓고 사용하려고 한다.

hooks/useAsync.jsx
요청보내는 axios 함수 부분을 인자로 받도록 해서 ,요청보내는 axios 함수만 추가하면, 자동으로 로딩 -> 요청보내기 -> 받아서 저장까지 진행할수 있도록 작성한다.

import { useEffect, useState } from 'react'

function useAsync(callback) {
    // 데이터, 에러, 로딩을 저장하는 state 작성
    const [data, setData] = useState([])
    const [isLoading, setIsLoading] = useState(false)
    const [error, setError] = useState(null)

    // 요청보내는 함수 (로딩, 에러, 데이터를 set)
    const fetchData = async () => {
        try {
            setIsLoading(true)
            const responseData = await callback()
            setData(responseData)
        } catch (err) {
            setError(err)
        }
        setIsLoading(false)
    }

    // 이 함수를 실행하면 아래 코드가 실행됨
    useEffect(() => {
        fetchData()
    }, [])

    // 데이터, 에러, 로딩을 한번에 리턴
    return { data, isLoading, error }
}

export default useAsync

이렇게 해주면 example 컴포넌트의 코드를 아래와 같이 줄일수 있게 된다.

import React, { useEffect, useState } from 'react'
import axios from 'axios'
import useAsync from '../hooks/useAsync'

function Example() {
    const getPosts = async () => {
        const response = await axios.get('/posts')
        return response
    }
    const { isLoading, data: posts, error } = useAsync(getPosts)

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>{error.message}</div>
    return (
        <div>
            {posts.map((post) => {
                return (
                    <div key={post.id}>
                        <h1>{post.title}</h1>
                        <p>작성자: {post.userId}</p>
                        <p>{post.body}</p>
                    </div>
                )
            })}
        </div>
    )
}

export default Example

데이터를 api로부터 요청해서 state를 변경하는 것까지 커스텀 훅으로 관리하니 코드가 대폭 줄어들었다.

이런 방법이 실무에서 많이 사용하는 방법이다.
지저분하지 않게, 어렵지 않게,

profile
개발자 꿈나무

0개의 댓글