카테고리 없음

2024.12.04

zayn 2024. 12. 4. 21:38

오늘이 체크리스트 마지막날...

나 진짜 아품...

마이아품..

 

지..진짜 아품다.....

나름 열심히 하지만 남들보단 뒤쳐지는것같은게 더 많이 느껴지는 하루....ㅠ

 

그래도 화이팅!

아 그리고 귀찮다..

누워서 물먹는 내 모습 보는거같당 ㅎㅎㅎ......

 

마지막 체크리스트 시작해보자구요.

사실 요즘 노트정리 중이라

과거내용 복기중....

쓸게 없는게 이미 다 썻단말이죠..ㅎ


Q1. 로그인 후 받은 acessToken을 저장하고 관리하는 방법에 대해 작성해보세요.

(단순히 useState가 아니고 어떤 방법들이 더 있을지 생각해보시면 좋겠습니다.)

 

A :

로그인 후 accessToken을 저장하고 관리하는 다양한 방법

accessToken은 사용자 인증 및 API 요청 시 중요한 역할을 하며, 이를 안전하고 효율적으로 관리하는 것이 중요합니다. 단순히 **useState**를 사용하는 것 외에도 다양한 방법을 활용할 수 있습니다. 아래는 주요 방법들과 각각의 장단점입니다.


1. Local Storage

방법

// 저장
localStorage.setItem('accessToken', token);

// 가져오기
const token = localStorage.getItem('accessToken');

// 삭제
localStorage.removeItem('accessToken');

장점

  • 브라우저를 새로고침해도 데이터가 유지됩니다.
  • 구현이 간단하고 빠릅니다.

단점

  • XSS(Cross-Site Scripting) 공격에 취약.
  • 민감한 데이터를 저장하기에는 보안성이 낮습니다.

2. Session Storage

방법

// 저장
sessionStorage.setItem('accessToken', token);

// 가져오기
const token = sessionStorage.getItem('accessToken');

// 삭제
sessionStorage.removeItem('accessToken');

장점

  • 브라우저 탭이 닫히면 데이터가 삭제되므로 보안성이 더 높습니다.
  • 구현이 간단합니다.

단점

  • 탭 간 데이터 공유가 불가능합니다.
  • 여전히 XSS 공격에 취약합니다.

3. HTTP Only Cookie

방법

  • accessToken을 HTTP Only Cookie로 저장하여 클라이언트 스크립트가 직접 접근하지 못하도록 설정합니다.
// 서버에서 설정 (Node.js 예제)
res.cookie('accessToken', token, {
  httpOnly: true, // 클라이언트에서 접근 불가
  secure: true,   // HTTPS에서만 전송
  maxAge: 3600000 // 1시간
});

장점

  • XSS 공격에 강함: 클라이언트에서 직접 접근할 수 없습니다.
  • 브라우저가 자동으로 쿠키를 전송하므로 인증 헤더를 수동으로 추가할 필요가 없습니다.

단점

  • CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있으므로 CSRF 토큰과 함께 사용해야 합니다.
  • 서버 설정이 필요합니다.

4. Redux 또는 Context API

방법

  • 전역 상태 관리 라이브러리를 사용해 토큰을 저장합니다.
// Redux 상태 예제
const authSlice = createSlice({
  name: 'auth',
  initialState: { accessToken: null },
  reducers: {
    setAccessToken: (state, action) => {
      state.accessToken = action.payload;
    },
    clearAccessToken: (state) => {
      state.accessToken = null;
    }
  }
});

장점

  • 컴포넌트 간 토큰 공유가 쉽습니다.
  • 상태 관리가 중앙 집중화되어 유지보수성이 높습니다.

단점

  • 브라우저 새로고침 시 상태가 초기화되므로 Local StorageSession Storage와 함께 사용해야 합니다.

5. Memory Storage

방법

  • 애플리케이션이 실행되는 동안 메모리에 저장합니다.
let accessToken = null;

// 저장
function setToken(token) {
  accessToken = token;
}

// 가져오기
function getToken() {
  return accessToken;
}

// 삭제
function clearToken() {
  accessToken = null;
}

장점

  • 브라우저 스토리지에 저장하지 않으므로 보안성이 높습니다.
  • 데이터를 쉽게 관리할 수 있습니다.

단점

  • 페이지 새로고침 시 데이터가 손실됩니다.
  • 전역 상태 관리가 어렵습니다.

6. Hybrid Approach (Local Storage + Memory)

방법

  • 로그인 시 accessToken을 Local Storage에 저장하고, 애플리케이션 실행 중에는 메모리에 저장하여 관리합니다.
// 로그인 시 저장
localStorage.setItem('accessToken', token);
let memoryToken = token;

// 가져오기
const token = memoryToken || localStorage.getItem('accessToken');

// 로그아웃 시 삭제
localStorage.removeItem('accessToken');
memoryToken = null;

장점

  • 새로고침 시에도 데이터가 유지됩니다.
  • 보안성과 성능의 균형을 맞출 수 있습니다.

단점

  • 구현이 다소 복잡할 수 있습니다.

7. Encrypted Storage

방법

  • 브라우저 스토리지에 저장하기 전에 토큰을 암호화합니다.
import CryptoJS from 'crypto-js';

// 저장
const encryptedToken = CryptoJS.AES.encrypt(token, 'secret_key').toString();
localStorage.setItem('accessToken', encryptedToken);

// 가져오기
const decryptedToken = CryptoJS.AES.decrypt(localStorage.getItem('accessToken'), 'secret_key').toString(CryptoJS.enc.Utf8);

장점

  • 스토리지에 저장된 토큰이 암호화되므로 보안성이 향상됩니다.
  • 기존의 Local Storage와 같은 방식으로 사용 가능합니다.

단점

  • 암호화 키 관리가 필요합니다.
  • 추가적인 암호화/복호화 작업으로 성능에 약간의 영향을 미칠 수 있습니다.

어떤 방법을 선택해야 할까?

  • 개발 환경:
    • 단순한 테스트나 프로토타이핑: Local Storage 또는 Session Storage
    • 실제 배포 환경: HTTP Only Cookie + CSRF 보호
  • 보안 요구 사항:
    • 높은 보안이 필요하면 HTTP Only Cookie와 같은 방법을 선택합니다.
  • 새로고침 데이터 유지:
    • 새로고침 후 데이터를 유지해야 한다면 Local StorageHybrid Approach를 고려하세요.

최종 추천

  • 보안 중심: HTTP Only Cookie (CSRF 방어 포함)
  • 개발 및 테스트 환경: Local Storage
  • 상태 관리 라이브러리를 사용하는 경우: Redux + Local Storage

Q2. 인증이 필요한 요청의 Authorization 헤더에 acessToken 을 포함해서 보내는 헤더를 코드로 작성해보세요.

 

A: 

 

1. axios를 사용한 기본 방법

import axios from 'axios';

const fetchProtectedData = async () => {
  const accessToken = localStorage.getItem('accessToken'); // 저장된 토큰 가져오기

  try {
    const response = await axios.get('https://example.com/protected', {
      headers: {
        Authorization: `Bearer ${accessToken}` // Authorization 헤더에 토큰 추가
      }
    });
    console.log('보호된 데이터:', response.data);
  } catch (error) {
    console.error('요청 실패:', error.response?.data || error.message);
  }
};

fetchProtectedData();

2. axios 인스턴스를 사용한 방법

axios.create를 통해 공통 헤더를 설정하고 재사용성을 높입니다.

import axios from 'axios';

const accessToken = localStorage.getItem('accessToken'); // 저장된 토큰 가져오기

// axios 인스턴스 생성
const apiClient = axios.create({
  baseURL: 'https://example.com/api',
  headers: {
    Authorization: `Bearer ${accessToken}` // 기본 Authorization 헤더 설정
  }
});

// 인증이 필요한 데이터 요청
const fetchProtectedData = async () => {
  try {
    const response = await apiClient.get('/protected');
    console.log('보호된 데이터:', response.data);
  } catch (error) {
    console.error('요청 실패:', error.response?.data || error.message);
  }
};

fetchProtectedData();

3. 요청마다 동적으로 Authorization 헤더 추가

토큰이 변경될 가능성이 있는 경우 요청을 보낼 때마다 Authorization 헤더를 동적으로 추가합니다.

import axios from 'axios';

const fetchProtectedData = async () => {
  const accessToken = localStorage.getItem('accessToken'); // 저장된 토큰 가져오기

  try {
    const response = await axios.get('https://example.com/protected', {
      headers: {
        Authorization: `Bearer ${accessToken}`
      }
    });
    console.log('보호된 데이터:', response.data);
  } catch (error) {
    console.error('요청 실패:', error.response?.data || error.message);
  }
};

fetchProtectedData();

4. Interceptor를 사용한 자동 헤더 설정

axios의 interceptor를 사용하면 모든 요청에 자동으로 토큰을 추가할 수 있습니다.

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://example.com/api'
});

// Interceptor로 Authorization 헤더 자동 추가
apiClient.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken');
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 인증이 필요한 데이터 요청
const fetchProtectedData = async () => {
  try {
    const response = await apiClient.get('/protected');
    console.log('보호된 데이터:', response.data);
  } catch (error) {
    console.error('요청 실패:', error.response?.data || error.message);
  }
};

fetchProtectedData();

헤더 구조 설명

headers: {
  Authorization: `Bearer ${accessToken}`
}
  • Authorization:
    • 서버 인증을 위한 표준 HTTP 헤더.
    • 토큰 기반 인증에서는 "Bearer <토큰>" 형식으로 작성.
  • Bearer:
    • 인증 유형을 나타내는 문자열. 일반적으로 OAuth2에서 사용됩니다.
  • accessToken:
    • 서버로부터 발급받은 인증 토큰.

적용 시나리오

  1. 단일 요청:
    • 특정 API 요청에만 인증 헤더가 필요한 경우, 기본 방법을 사용합니다.
  2. 다중 요청:
    • 여러 요청에 공통적으로 토큰이 필요하면 axios 인스턴스 또는 interceptor를 활용합니다.

Q3.  [useEffect] 를 사용해 컴포넌트 마운트 시에 특정한 동작을 수행하게 만드는 예시코드를 간단히 작성해보세요.

 

A :

 

기본 예제: 콘솔 메시지 출력

import React, { useEffect } from 'react';

const MyComponent = () => {
  useEffect(() => {
    console.log('컴포넌트가 마운트되었습니다.');

    // 클린업 함수 (컴포넌트 언마운트 시 실행)
    return () => {
      console.log('컴포넌트가 언마운트되었습니다.');
    };
  }, []); // 빈 배열로 의존성 설정 → 컴포넌트 마운트 시 한 번만 실행

  return <div>컴포넌트가 렌더링되었습니다.</div>;
};

export default MyComponent;

API 호출 예제

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

const FetchDataComponent = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
        setData(response.data); // API 응답 데이터를 상태에 저장
      } catch (error) {
        console.error('데이터 가져오기 실패:', error.message);
      }
    };

    fetchData(); // 데이터 가져오기 함수 호출
  }, []); // 빈 배열 → 마운트 시에만 실행

  return (
    {data.map((item) => (
  • {item.title}
  • ))}
  );
};

export default FetchDataComponent;

타이머 설정 및 클린업 예제

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

const TimerComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount((prev) => prev + 1); // 1초마다 count 증가
    }, 1000);

    // 클린업 함수: 컴포넌트가 언마운트될 때 타이머 제거
    return () => {
      clearInterval(timer);
      console.log('타이머가 정리되었습니다.');
    };
  }, []); // 빈 배열 → 마운트 시 한 번만 실행

  return <div>타이머: {count}초</div>;
};

export default TimerComponent;

설명

  1. useEffect의 역할:
    • 컴포넌트가 렌더링된 후(마운트 시)에 특정 작업을 수행합니다.
    • 의존성 배열([])에 따라 동작이 결정됩니다.
  2. 의존성 배열([]):
    • 빈 배열: 컴포넌트 마운트 시 한 번만 실행.
    • 배열에 값 포함: 특정 값이 변경될 때마다 실행.
  3. 클린업 함수:
    • useEffect의 반환값으로 함수를 지정하면 컴포넌트 언마운트 시 실행됩니다(예: 타이머 정리, 이벤트 리스너 제거).

결론

  • 컴포넌트 마운트 시 초기 작업(API 호출, 타이머 설정 등)을 처리하려면 useEffect를 활용하세요.
  • 클린업 함수로 자원을 정리하여 메모리 누수를 방지할 수 있습니다.

Q4. 기본 설정이 적용된 axios 인스턴스를 생성하는 예시 코드를 작성해보세요.

 

 

A :

 

기본 axios 인스턴스 생성

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com', // 모든 요청의 기본 URL
  timeout: 5000, // 요청 제한 시간 (밀리초 단위, 여기서는 5초)
  headers: {
    'Content-Type': 'application/json', // JSON 형식 데이터
    Authorization: `Bearer ${localStorage.getItem('accessToken')}`, // 기본 인증 헤더
  },
});

export default apiClient;

사용 예시: API 요청 보내기

import apiClient from './apiClient';

const fetchData = async () => {
  try {
    const response = await apiClient.get('/data'); // baseURL + '/data'
    console.log('데이터 가져오기 성공:', response.data);
  } catch (error) {
    console.error('데이터 가져오기 실패:', error.response?.data || error.message);
  }
};

fetchData();

Interceptors로 동적 헤더 추가

axios 인스턴스의 Interceptor를 활용하면 요청 전후에 추가 작업(예: 동적 토큰 추가)을 수행할 수 있습니다.

Interceptor를 사용해 토큰 동적으로 추가

apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('accessToken'); // 저장된 토큰 가져오기
    if (token) {
      config.headers.Authorization = `Bearer ${token}`; // Authorization 헤더 설정
    }
    return config;
  },
  (error) => Promise.reject(error)
);

Interceptor를 사용한 응답 처리

서버 응답을 가로채어 처리할 수도 있습니다.

apiClient.interceptors.response.use(
  (response) => {
    // 요청 성공 시 데이터 그대로 반환
    return response;
  },
  (error) => {
    // 요청 실패 시 에러 처리
    if (error.response?.status === 401) {
      console.error('인증 오류: 다시 로그인하세요.');
      // 추가적인 인증 실패 처리 로직
    }
    return Promise.reject(error);
  }
);

설명

  1. axios.create():
    • axios 인스턴스를 생성하며, 기본 설정(예: baseURL, timeout, headers)을 정의합니다.
  2. baseURL:
    • 모든 요청의 공통 URL을 지정합니다.
  3. timeout:
    • 요청 제한 시간을 설정합니다. 시간 초과 시 에러가 발생합니다.
  4. headers:
    • 기본 HTTP 헤더를 설정하여 모든 요청에 적용됩니다.

장점

  1. 재사용성:
    • 동일한 설정을 여러 요청에서 재사용 가능.
  2. 유지보수성:
    • 설정 변경 시 인스턴스에서 한 번만 수정하면 됩니다.
  3. 가독성:
    • baseURL 및 공통 헤더 설정으로 코드 간결화.

Q5. JSON server 를 대상으로 CRUD(Create,Read,Update,Delete) 를 수행하는 예시 코드를 작성해보세요.

 

A :

 

1. JSON Server 준비

JSON Server가 실행되고 있다고 가정하며, db.json에 다음과 같은 초기 데이터를 설정합니다:

{
  "posts": [
    { "id": 1, "title": "첫 번째 게시글", "content": "내용 1" },
    { "id": 2, "title": "두 번째 게시글", "content": "내용 2" }
  ]
}

JSON Server 실행:

json-server --watch db.json --port 3000

2. CRUD 예제 코드

공통 설정: axios 인스턴스 생성

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'http://localhost:3000/posts', // JSON Server 엔드포인트
  headers: {
    'Content-Type': 'application/json',
  },
});

export default apiClient;

(1) Create: 새로운 게시글 추가 (POST 요청)

const createPost = async (postData) => {
  try {
    const response = await apiClient.post('/', postData); // POST 요청
    console.log('새 게시글 추가 성공:', response.data);
  } catch (error) {
    console.error('게시글 추가 실패:', error.message);
  }
};

// 사용 예시
createPost({ title: '새로운 게시글', content: '새로운 내용' });

(2) Read: 게시글 목록 가져오기 (GET 요청)

const getPosts = async () => {
  try {
    const response = await apiClient.get('/'); // GET 요청
    console.log('게시글 목록:', response.data);
  } catch (error) {
    console.error('게시글 가져오기 실패:', error.message);
  }
};

// 사용 예시
getPosts();

(3) Update: 특정 게시글 수정 (PUT 요청)

const updatePost = async (id, updatedData) => {
  try {
    const response = await apiClient.put(`/${id}`, updatedData); // PUT 요청
    console.log(`ID ${id} 게시글 수정 성공:`, response.data);
  } catch (error) {
    console.error(`ID ${id} 게시글 수정 실패:`, error.message);
  }
};

// 사용 예시
updatePost(1, { title: '수정된 게시글 제목', content: '수정된 내용' });

(4) Delete: 특정 게시글 삭제 (DELETE 요청)

const deletePost = async (id) => {
  try {
    await apiClient.delete(`/${id}`); // DELETE 요청
    console.log(`ID ${id} 게시글 삭제 성공`);
  } catch (error) {
    console.error(`ID ${id} 게시글 삭제 실패:`, error.message);
  }
};

// 사용 예시
deletePost(2);

3. CRUD 통합 코드 예제

React 컴포넌트에서 CRUD 기능을 통합하여 사용하면 다음과 같은 형태가 됩니다:

import React, { useState, useEffect } from 'react';
import apiClient from './apiClient';

const App = () => {
  const [posts, setPosts] = useState([]);

  // Read: 게시글 목록 가져오기
  const fetchPosts = async () => {
    try {
      const response = await apiClient.get('/');
      setPosts(response.data);
    } catch (error) {
      console.error('게시글 가져오기 실패:', error.message);
    }
  };

  // Create: 새 게시글 추가
  const handleAddPost = async () => {
    try {
      const newPost = { title: '새로운 게시글', content: '새로운 내용' };
      const response = await apiClient.post('/', newPost);
      setPosts([...posts, response.data]); // 상태 업데이트
    } catch (error) {
      console.error('게시글 추가 실패:', error.message);
    }
  };

  // Update: 특정 게시글 수정
  const handleUpdatePost = async (id) => {
    try {
      const updatedPost = { title: '수정된 제목', content: '수정된 내용' };
      const response = await apiClient.put(`/${id}`, updatedPost);
      setPosts(posts.map((post) => (post.id === id ? response.data : post))); // 상태 업데이트
    } catch (error) {
      console.error('게시글 수정 실패:', error.message);
    }
  };

  // Delete: 특정 게시글 삭제
  const handleDeletePost = async (id) => {
    try {
      await apiClient.delete(`/${id}`);
      setPosts(posts.filter((post) => post.id !== id)); // 상태 업데이트
    } catch (error) {
      console.error('게시글 삭제 실패:', error.message);
    }
  };

  useEffect(() => {
    fetchPosts(); // 컴포넌트 마운트 시 게시글 목록 가져오기
  }, []);

  return (
    <div>
      <h1>게시글 목록</h1>
      <button onClick={handleAddPost}>게시글 추가</button>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
            <button onClick={() => handleUpdatePost(post.id)}>수정</button>
            <button onClick={() => handleDeletePost(post.id)}>삭제</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default App;

 


결론

위 코드를 통해 JSON Server를 사용하여 CRUD 작업(Create, Read, Update, Delete)을 구현할 수 있습니다. JSON Server는 로컬 환경에서 간단히 RESTful API를 모의(Mock)할 수 있다.


Q6.  라우트나 컴포넌트를 인증 상태에 따라 보호하는 방법( 아예 접근을 못하게 하는 방법)은 어떻게 구현 할 수 있을까요?

 

A:

 

1. 보호된 라우트(Private Route) 생성

React Router의 Route를 확장하여 인증 상태를 확인하고, 인증되지 않은 사용자를 리다이렉트하는 컴포넌트를 작성합니다.

PrivateRoute 컴포넌트

import React from 'react';
import { Navigate } from 'react-router-dom';

const PrivateRoute = ({ isAuthenticated, children }) => {
  return isAuthenticated ? children : <Navigate to="/login" />;
};

export default PrivateRoute;

2. App.js에서 사용

React Router에서 PrivateRoute를 사용하여 보호된 라우트를 설정합니다.

App.js

import React, { useState } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';
import HomePage from './HomePage';
import LoginPage from './LoginPage';
import ProtectedPage from './ProtectedPage';

const App = () => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  return (
    <Router>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage setIsAuthenticated={setIsAuthenticated} />} />
        <Route
          path="/protected"
          element={
            <PrivateRoute isAuthenticated={isAuthenticated}>
              <ProtectedPage />
            </PrivateRoute>
          }
        />
      </Routes>
    </Router>
  );
};

export default App;

설명

  • isAuthenticated:
    • 사용자 인증 상태를 나타냅니다.
    • true이면 인증된 상태, false이면 인증되지 않은 상태를 의미합니다.
  • PrivateRoute:
    • 인증되지 않은 사용자를 /login 페이지로 리다이렉트합니다.

3. 인증 상태 관리

인증 상태를 관리하는 방법으로 localStorageContext API를 활용할 수 있습니다.

localStorage로 관리

const isAuthenticated = !!localStorage.getItem('accessToken'); // 토큰 유무로 인증 상태 결정

Context API로 관리

import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  return (
    <AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

4. 더 나아가기: Role-Based Authorization

사용자의 권한(Role)에 따라 접근 권한을 제어할 수도 있습니다.

Role-Based Private Route

const RoleBasedRoute = ({ isAuthenticated, userRole, allowedRoles, children }) => {
  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }

  if (!allowedRoles.includes(userRole)) {
    return <Navigate to="/unauthorized" />;
  }

  return children;
};

사용 예시

<Route
  path="/admin"
  element={
    <RoleBasedRoute
      isAuthenticated={isAuthenticated}
      userRole={userRole}
      allowedRoles={['admin']}
    >
      <AdminPage />
    </RoleBasedRoute>
  }
/>

5. 예외 페이지 구성

인증되지 않은 사용자나 권한이 부족한 사용자를 위해 별도의 페이지를 구성합니다.

Unauthorized.js

const Unauthorized = () => {
  return <h1>권한이 없습니다. 관리자에게 문의하세요.</h1>;
};

export default Unauthorized;

결론

  • 인증 상태에 따라 라우트나 컴포넌트를 보호하려면:
    1. PrivateRoute를 생성하여 인증 상태를 확인합니다.
    2. 인증되지 않은 사용자를 리다이렉트합니다.
    3. 필요에 따라 Role-Based Authorization을 추가하여 사용자 권한을 제어합니다.
  • Context API, Redux 등 상태 관리 도구를 활용하면 더 효율적으로 인증 상태를 관리할 수 있습니다.

Q7. axios 인스턴스는 무엇이며, 프로젝트에서 이 인스턴스를 사용하면 어떤 장점이 있나요?

 

A:

axios 인스턴스란?

axios 인스턴스는 axios.create() 메서드를 사용하여 공통 설정이 포함된 커스텀 axios 객체를 생성하는 것입니다. 이를 통해 매번 요청마다 반복적인 설정을 하지 않고, 기본값을 미리 정의하여 재사용할 수 있습니다.


axios 인스턴스 생성 예시

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com', // 기본 URL
  timeout: 5000, // 요청 제한 시간 (5초)
  headers: {
    'Content-Type': 'application/json', // JSON 데이터 형식
  },
});

export default apiClient;

axios 인스턴스를 사용하는 장점

1. 공통 설정의 재사용

  • 모든 요청에서 공통적으로 사용하는 baseURL, timeout, headers를 미리 정의할 수 있습니다.
  • 코드의 중복을 줄이고 가독성을 높일 수 있습니다.
// 인스턴스 없이 요청
axios.get('https://api.example.com/users');

// 인스턴스 사용
apiClient.get('/users');

2. 코드 간결화

  • baseURL이나 인증 토큰 등을 매번 설정하지 않아도 되므로, 코드가 간결해지고 유지보수가 용이해집니다.

3. 요청 및 응답 처리 로직 중앙화 (Interceptors)

  • 요청 전후 또는 응답 전후에 로직을 추가할 수 있습니다.
  • 예를 들어, 모든 요청에 인증 토큰을 자동으로 추가하거나, 에러 처리 로직을 중앙화할 수 있습니다.
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      console.error('Unauthorized: Redirecting to login...');
    }
    return Promise.reject(error);
  }
);

4. 유지보수성

  • API URL이 변경되거나 헤더 설정이 바뀌어도, 인스턴스 내 설정만 수정하면 됩니다.
  • 개별 요청 코드를 수정할 필요가 없으므로 유지보수가 편리합니다.

5. 다중 인스턴스 지원

  • 여러 API 엔드포인트를 사용하는 경우, 각각의 API에 맞는 인스턴스를 생성하여 사용할 수 있습니다.
const userApi = axios.create({
  baseURL: 'https://api.example.com/users',
});

const postApi = axios.create({
  baseURL: 'https://api.example.com/posts',
});

axios 인스턴스 활용 예시

1. 데이터 가져오기 (GET)

apiClient.get('/users')
  .then((response) => {
    console.log('사용자 데이터:', response.data);
  })
  .catch((error) => {
    console.error('데이터 가져오기 실패:', error.message);
  });

2. 데이터 추가하기 (POST)

apiClient.post('/users', { name: 'Alice', age: 30 })
  .then((response) => {
    console.log('사용자 추가 성공:', response.data);
  })
  .catch((error) => {
    console.error('사용자 추가 실패:', error.message);
  });

3. Interceptor를 통한 인증 처리

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('accessToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

axios 인스턴스를 사용해야 하는 이유

  1. 코드 중복 감소:
    • 공통 설정을 한 번만 정의하여 여러 요청에서 재사용할 수 있습니다.
  2. 가독성과 유지보수성 향상:
    • 기본 설정과 요청 로직이 분리되므로, 코드가 간결해지고 수정이 쉬워집니다.
  3. 확장성:
    • 요청 또는 응답에 대해 추가 로직(예: 인증 토큰 처리, 로깅)을 쉽게 추가할 수 있습니다.
  4. 효율성:
    • API 요청이 많아질수록 설정을 반복하지 않아도 되므로, 개발 시간을 절약할 수 있습니다.

결론: axios 인스턴스를 사용하면 효율적이고 확장 가능한 방식으로 API 요청을 관리할 수 있습니다.


Q8. [useQuery] 를 사용해 데이터를 가져오고, 로딩 상태를 관리하는 코드를 간단히 작성해보세요.

 

A:

 

React Query 설치

먼저 React Query 패키지를 설치해야 합니다:

npm install @tanstack/react-query

코드 예제

import React from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

// 데이터를 가져오는 함수
const fetchPosts = async () => {
  const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
  return response.data;
};

const PostsList = () => {
  // useQuery를 사용하여 데이터 가져오기
  const { data, isLoading, isError } = useQuery(['posts'], fetchPosts);

  // 로딩 상태 처리
  if (isLoading) return

로딩 중...

;

  // 에러 상태 처리
  if (isError) return

데이터를 가져오는 데 실패했습니다.

;

  // 성공적으로 데이터를 가져왔을 때 렌더링
  return (
    {data.map((post) => (
  • {post.title}
  • ))}
  );
};

export default PostsList;

코드 설명

  1. fetchPosts 함수:
  2. useQuery:
    • React Query의 훅으로 데이터를 가져오고 로딩, 에러, 성공 상태를 관리합니다.
    • 첫 번째 인자: 쿼리 키(['posts']), 데이터를 구분하고 캐싱하는 데 사용됩니다.
    • 두 번째 인자: 데이터를 가져오는 함수(fetchPosts).
  3. 상태 관리:
    • isLoading: 데이터가 로딩 중일 때 true.
    • isError: 데이터 가져오기 실패 시 true.
    • data: 성공적으로 가져온 데이터.
  4. 렌더링 조건:
    • 로딩 중 → "로딩 중..." 메시지 표시.
    • 에러 발생 → "데이터를 가져오는 데 실패했습니다." 메시지 표시.
    • 성공 시 데이터 목록 렌더링.

장점

  • 자동 상태 관리:
    • React Query는 로딩, 성공, 실패 상태를 자동으로 관리해줍니다.
  • 데이터 캐싱:
    • 동일한 키(['posts'])로 요청 시 캐시를 사용하여 불필요한 네트워크 요청을 줄입니다.
  • 리페치(refetch):
    • 데이터가 변경되거나 캐시가 만료되면 자동으로 최신 데이터를 가져옵니다.

Q9 . 이번엔 [useMutation] 을 사용해 데이터 수정(POST,PUT,DELETE 요청)을 수행하는 코드를 간단히 작성해보세요.

 

A :

 

React Query 설치

React Query가 설치되어 있지 않다면 먼저 설치합니다:

npm install @tanstack/react-query

코드 예제: POST, PUT, DELETE 요청

1. 기본 설정

import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

// API 함수 정의
const addPost = async (newPost) => {
  const response = await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
  return response.data;
};

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

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

2. React Component

const PostManager = () => {
  const queryClient = useQueryClient(); // React Query의 캐시를 관리하는 객체
  const [postId, setPostId] = useState('');
  const [postTitle, setPostTitle] = useState('');

  // useMutation for POST
  const addPostMutation = useMutation(addPost, {
    onSuccess: (data) => {
      console.log('새 게시글 추가 성공:', data);
      queryClient.invalidateQueries(['posts']); // 캐시 무효화로 데이터 새로고침
    },
  });

  // useMutation for PUT
  const updatePostMutation = useMutation(updatePost, {
    onSuccess: (data) => {
      console.log('게시글 수정 성공:', data);
      queryClient.invalidateQueries(['posts']);
    },
  });

  // useMutation for DELETE
  const deletePostMutation = useMutation(deletePost, {
    onSuccess: () => {
      console.log('게시글 삭제 성공');
      queryClient.invalidateQueries(['posts']);
    },
  });

  // Form Handlers
  const handleAddPost = () => {
    addPostMutation.mutate({ title: postTitle, body: '내용', userId: 1 });
    setPostTitle('');
  };

  const handleUpdatePost = () => {
    updatePostMutation.mutate({ id: postId, updatedPost: { title: postTitle, body: '수정된 내용' } });
    setPostTitle('');
    setPostId('');
  };

  const handleDeletePost = () => {
    deletePostMutation.mutate(postId);
    setPostId('');
  };

  return (
    <div>
      <h1>게시글 관리</h1>

      {/* Add Post */}
      <input
        type="text"
        placeholder="게시글 제목"
        value={postTitle}
        onChange={(e) => setPostTitle(e.target.value)}
      />
      <button onClick={handleAddPost} disabled={addPostMutation.isLoading}>
        {addPostMutation.isLoading ? '추가 중...' : '게시글 추가'}
      </button>

      {/* Update Post */}
      <input
        type="number"
        placeholder="게시글 ID"
        value={postId}
        onChange={(e) => setPostId(e.target.value)}
      />
      <button onClick={handleUpdatePost} disabled={updatePostMutation.isLoading}>
        {updatePostMutation.isLoading ? '수정 중...' : '게시글 수정'}
      </button>

      {/* Delete Post */}
      <button onClick={handleDeletePost} disabled={deletePostMutation.isLoading}>
        {deletePostMutation.isLoading ? '삭제 중...' : '게시글 삭제'}
      </button>
    </div>
  );
};

export default PostManager;

코드 설명

  1. useMutation:
    • addPost, updatePost, deletePost와 같은 서버 데이터를 수정하는 작업에 사용됩니다.
  2. onSuccess:
    • 요청이 성공적으로 완료되었을 때 실행됩니다.
    • 예를 들어, React Query의 캐시를 무효화(invalidateQueries)하여 UI를 최신 상태로 유지합니다.
  3. React Query의 캐시 무효화:
    • queryClient.invalidateQueries(['posts'])를 호출하여 기존 데이터를 새로고침합니다.
  4. 버튼 상태 관리:
    • useMutation의 isLoading 상태를 활용해 로딩 중 버튼을 비활성화하고, 사용자에게 피드백을 제공합니다.

장점

  1. 자동 상태 관리:
    • isLoading, isError와 같은 상태를 자동으로 처리합니다.
  2. 캐시 무효화:
    • 데이터가 변경되면 기존 쿼리의 캐시를 무효화해 최신 데이터를 가져옵니다.
  3. 코드 간결화:
    • useMutation을 사용하면 비동기 로직을 깔끔하게 관리할 수 있습니다.

Q10. 쿼리 클라이언트를 이용해 쿼리를 무효화하고, 데이터를 최신 상태로 유지할 때 필요한 메서드는 무엇인가요?

 

A:

쿼리 클라이언트를 이용해 쿼리를 무효화하고 데이터를 최신 상태로 유지하기 위해 사용하는 주요 메서드는 invalidateQueries입니다.


queryClient.invalidateQueries란?

**queryClient.invalidateQueries**는 특정 쿼리 키와 일치하는 쿼리의 캐시를 무효화하여, React Query가 해당 데이터를 다시 가져오도록 강제하는 메서드입니다.

사용 이유

  • 서버에서 데이터가 변경된 후(예: POST, PUT, DELETE 요청) 캐시를 무효화하여 최신 데이터를 가져오기 위해 사용합니다.
  • 데이터가 변경되었음에도 불구하고 캐시에 오래된 데이터가 남아있을 때 이를 해결합니다.

invalidateQueries 메서드의 사용법

queryClient.invalidateQueries(queryKey, options);

매개변수

  1. queryKey (필수):
    • 무효화할 쿼리를 식별하는 키입니다.
    • 키가 없으면 모든 쿼리를 무효화합니다.
  2. options (선택):
    • 추가적인 무효화 조건을 설정합니다.
    • 예: { exact: true }는 주어진 키와 정확히 일치하는 쿼리만 무효화합니다.

예제 코드

1. 기본 사용법

import { useQueryClient } from '@tanstack/react-query';

const MyComponent = () => {
  const queryClient = useQueryClient();

  const handleInvalidate = () => {
    queryClient.invalidateQueries(['posts']); // 'posts' 키를 가진 쿼리 무효화
  };

  return <button onClick={handleInvalidate}>캐시 무효화</button>;
};

2. 데이터 추가 후 캐시 무효화

useMutation의 onSuccess 옵션에서 invalidateQueries를 호출하여 데이터를 최신 상태로 유지합니다.

import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

const addPost = async (newPost) => {
  return await axios.post('https://jsonplaceholder.typicode.com/posts', newPost);
};

const AddPostComponent = () => {
  const queryClient = useQueryClient();

  const mutation = useMutation(addPost, {
    onSuccess: () => {
      queryClient.invalidateQueries(['posts']); // 'posts' 키 캐시 무효화
    },
  });

  const handleAddPost = () => {
    mutation.mutate({ title: '새 게시글', body: '내용' });
  };

  return 게시글 추가;
};

3. 조건부 무효화

exact 옵션을 사용하여 정확히 일치하는 키만 무효화합니다.

queryClient.invalidateQueries(['posts', { page: 1 }], { exact: true });

추가적으로 사용할 수 있는 메서드

  1. queryClient.refetchQueries
    • 데이터를 즉시 새로고침하여 최신 상태를 가져옵니다.
    queryClient.refetchQueries(['posts']);
    
  2. queryClient.removeQueries
    • 특정 쿼리를 캐시에서 제거합니다.
    queryClient.removeQueries(['posts']);
    
  3. queryClient.resetQueries
    • 쿼리를 초기 상태로 리셋합니다.
    queryClient.resetQueries(['posts']);
    

invalidateQueries와 refetchQueries의 차이점

메서드 역할 주요 사용 시점

invalidateQueries 캐시를 무효화하고 쿼리가 새로고침되도록 트리거 데이터 변경 후
refetchQueries 즉시 데이터를 다시 가져옴 강제로 새 데이터가 필요할 때

결론

  • **invalidateQueries**는 서버에서 데이터가 변경된 후 데이터를 최신 상태로 유지하는 데 필수적인 메서드입니다.
  • useMutation과 함께 사용하여 데이터 추가, 수정, 삭제 후 쿼리 캐시를 무효화하여 UI가 항상 최신 상태를 반영하도록 합니다.
  • 상황에 따라 refetchQueries 또는 removeQueries를 조합하여 사용할 수도 있습니다.

Q11. 클라이언트 사이드 애플리케이션에서 데이터의 동기화는 반드시 필요합니다. 무엇 때문에 중요한 걸까요?

 

A: 

클라이언트 사이드 애플리케이션에서 데이터 동기화가 중요한 이유

클라이언트 사이드 애플리케이션에서 데이터 동기화는 서버와 클라이언트 간의 데이터 일관성을 유지하고, 사용자 경험을 향상시키며, 애플리케이션의 신뢰성과 안정성을 보장하는 데 필수적


1. 데이터 일관성 유지

  • 클라이언트와 서버의 데이터가 불일치하면 사용자가 잘못된 정보를 보거나 예기치 않은 오류가 발생할 수 있습니다.
  • 동기화를 통해 클라이언트와 서버 간 데이터의 상태를 항상 일관되게 유지합니다.

예시

  • 서버에서 업데이트된 게시글 내용이 클라이언트에 동기화되지 않으면 사용자는 오래된 데이터를 보게 됩니다.
  • React Query나 SWR과 같은 데이터 관리 도구를 사용하여 데이터 변경 시 자동으로 최신 데이터를 가져올 수 있습니다.

2. 사용자 경험 개선

  • 사용자에게 항상 최신 상태의 데이터를 보여주는 것은 사용자 경험(UX)을 크게 향상시킵니다.
  • 동기화가 잘 이루어지면 사용자와 애플리케이션 간의 신뢰도가 높아집니다.

예시

  • 사용자가 실시간 채팅 애플리케이션을 사용 중일 때, 동기화가 잘못되면 상대방의 메시지를 확인하지 못하거나 잘못된 순서로 보이게 됩니다.

3. 실시간 데이터 반영

  • 여러 사용자가 동일한 데이터에 접근하거나 수정할 때, 동기화를 통해 데이터 충돌을 방지하고 실시간으로 변경 사항을 반영합니다.

예시

  • 협업 도구(예: Google Docs)에서 모든 사용자가 같은 문서의 최신 상태를 볼 수 있어야 합니다.
  • WebSocket, SignalR 같은 기술을 통해 실시간 동기화를 구현할 수 있습니다.

4. 데이터 정합성 및 무결성 보장

  • 서버의 데이터 변경이 클라이언트에 제대로 반영되지 않으면 데이터 정합성 문제가 발생할 수 있습니다.
  • 동기화를 통해 클라이언트와 서버 간 데이터의 무결성을 보장합니다.

예시

  • 이커머스 애플리케이션에서 재고 상태가 클라이언트에 동기화되지 않으면, 사용자는 품절된 상품을 구매하려 할 수 있습니다.

5. 충돌 방지

  • 동기화가 없거나 잘못 관리되면 같은 데이터를 동시에 수정하는 경우 충돌이 발생할 수 있습니다.
  • 클라이언트와 서버 간 동기화를 통해 데이터 충돌을 방지하거나 해결할 수 있습니다.

예시

  • 버전 관리를 통해 클라이언트가 수정한 데이터가 서버에 업데이트되기 전에 다른 사용자가 동일 데이터를 수정하는 경우를 처리합니다.

6. 효율적인 네트워크 사용

  • 잘못된 데이터 상태를 방지하기 위해 필요 없는 API 요청을 반복적으로 보내는 대신, 동기화를 통해 필요한 데이터만 요청하고 캐시를 활용하여 네트워크 사용량을 줄일 수 있습니다.

예시

  • React Query는 캐시를 사용해 중복 요청을 방지하고 필요한 경우에만 서버와 동기화합니다.

7. 데이터 최신성에 대한 신뢰성 확보

  • 애플리케이션이 클라이언트에서 오래된 데이터를 보여주면, 사용자는 해당 애플리케이션을 신뢰하지 않을 가능성이 높습니다.
  • 동기화를 통해 데이터를 최신 상태로 유지하고 신뢰성을 제공합니다.

8. 애플리케이션 오류 방지

  • 동기화가 이루어지지 않으면, 애플리케이션의 로직이 예상과 다르게 동작하거나 치명적인 버그가 발생할 수 있습니다.
  • 동기화는 이러한 오류를 예방합니다.

예시

  • 클라이언트에서 이미 삭제된 데이터를 다시 표시하면 사용자에게 혼란을 줄 수 있습니다.

9. 분산 시스템에서의 중요성

  • 클라이언트와 서버가 분리된 분산 시스템 환경에서는 동기화가 없으면 데이터의 불일치가 빈번하게 발생합니다.
  • 동기화는 이러한 분산 시스템의 안정적인 작동을 보장합니다.

10. 데이터 기반 의사결정

  • 사용자 인터페이스(UI)가 최신 데이터를 기반으로 업데이트되지 않으면, 사용자는 잘못된 정보를 기반으로 결정을 내릴 수 있습니다.

예시

  • 주식 거래 애플리케이션에서 가격 데이터가 실시간으로 동기화되지 않으면, 사용자가 잘못된 가격으로 거래를 시도할 수 있습니다.

결론

클라이언트 사이드 애플리케이션에서 데이터 동기화는 사용자 경험, 데이터 일관성, 안정성을 보장하기 위해 반드시 필요합니다. React Query, SWR, Apollo Client 같은 데이터 관리 도구를 활용하거나 WebSocket과 같은 기술을 통해 실시간 동기화를 구현하면, 사용자와 서버 간의 데이터 상태를 효과적으로 관리할 수 있습니다.