프로그래머스 스프린트 3: 디렉토리 구조 분석

박상하·2024년 3월 1일
0

물론 모든 강사님이 좋았지만 스프린트3의 강사님은 좀 더 실무에 가까운
(필자에게는 조금 더 어려운,, 그렇지만 배울 게 좀 더 많은) 교육을 해주시고 계신다.

아, 실무를 안해봐서 실무에 가까운지는 잘 모르겠지만 뭔가 저렇게 코드를 짜면 유지보수에 용이하겠다는 생각이 들어서 나온 표현같다.

스프린트 3를 시작하면서 대략적인 디렉토리를 잡고 프로젝트를 시작하셨다.
그런데 너무 깔끔하다는 생각이 들었다. 지금부터 그 디렉토리를 보고 왜 이렇게 짰고 그 안에서 어떤 로직으로 다른 폴더와 연결되는지를 분석해보자

프로젝트의 디렉토리 구조 📚

api 📂

  • Api 폴더
    이 폴더는 axios를 커스텀한 http.ts라는 파일
    그리고 각종 api를 호출하는 파일이 담겨있다.(auth.api, category.api)
// category.api
export const fetchCategory = async () => {
  const response = await httpClient.get<Category[]>("/category");
  return response.data;
};
// auth.api
export const signup = async (userData: SignupProps) => {
  const response = await httpClient.post("/users/join", userData);
  return response.data;
};

export const resetRequest = async (data: SignupProps) => {
  const response = await httpClient.post("/users/reset", data);
  return response.data;
};

export const resetPassword = async (data: SignupProps) => {
  const response = await httpClient.put("/users/reset", data);
  return response.data;
};

interface LoginResponse {
  token: string;
}

export const login = async (data: SignupProps) => {
  const response = await httpClient.post<LoginResponse>("/users/login", data);
  return response.data;
};

이렇게 API를 호출하는 폴더를 따로 만들어서 관리를 해준다.

여기서 폴더명이 auth.api.ts인데
api를 붙여준 이유는 단순 컨벤션이라고 말씀하셨다.

필자는 여기에서 http.ts라는 파일의 코드를 통해 axios의 커스텀화를 볼 수 있었다 .

axios customizing ❓


const BASE_URL = "http://localhost:9999";
const DEFAULT_TIMEOUT = 30000;

export const createClient = (config?: AxiosRequestConfig) => {
  const axiosInstance = axios.create({
    baseURL: BASE_URL,
    timeout: DEFAULT_TIMEOUT,
    headers: {
      "Content-Type": "application/json",
      Authorization: getToken() ? getToken() : "",
    },
    withCredentials: true,
    ...config,
  });
  axiosInstance.interceptors.response.use(
    (response) => {
      return response;
    },
    (error) => {
      if (error.response.status === 401) {
        removeToken();
        window.location.href = "/login";
      }
      return Promise.reject(error);
      //로그인 만료처리
    }
  );
  return axiosInstance;
};
export const httpClient = createClient();

먼저 일반적인 Axios의 사용은 다음과 같았다.

const requsetPost =async()=>{
  const response = await axios.get('http://localhost:5050/user',{
   headers:{
    withCredential:true 
   }
  })
}

그런데 매 요청마다 저렇게 긴 URL과 headers를 설정하는건 비효율적이다.

그래서 axios를 커스텀할 수 있는 기능을 제공한다.

axios의 공식홈페이지를 확인해보자.

기본적으로 axios의 인스턴스를 생성하는 방법은

axios.create([config])

Config안에는 baseURL,timeout,withCredentials등 다양한 옵션이 들어있다.

그래서 axios.create({baseURL:,timeout:})이런식으로 옵션을 조절하여 axios 커스텀을 진행할 수 있다.

이렇게 Axios를 커스텀하는 방법을 알 수 있었고 그 다음줄

const axiosInstance = axios.create({
    baseURL: BASE_URL,
    timeout: DEFAULT_TIMEOUT,
    headers: {
      "Content-Type": "application/json",
      Authorization: getToken() ? getToken() : "",
    },
    withCredentials: true,
    ...config,
  });

에서 headers: Authorization은 무엇을 의미할까?
만약 token(여기서는 localstorage에 저장함)이 있다면 token값을(string의 JWT 값)을 서버에 보낼때 사용
그 밑에 코드는 다음과 같다.

axiosInstance.interceptors.response.use(
    (response) => {
      return response;
    },
    (error) => {
      if (error.response.status === 401) {
        removeToken();
        window.location.href = "/login";
      }
      return Promise.reject(error);
      //로그인 만료처리
    }
  );

Intercepter는 then이나 catch로 데이터 페칭 결과가 클라이언트로 전달 전에 해당 데이터를 intercepter 해온다. 즉, 위 코드에서는 axios를 통해 요청을 받아오면 응답이 오류가 아닌경우에는 정상적으로 그 Response를 보내주고 요청이 오류가 발생하면 해당 토큰(localstorage에 있는 값을) 제거하고 다시 login 페이지로 라우팅한다.

이렇게 해줬을 때 각 axios요청을 보낼 때마다 예외처리를 하지 않고 앞단에서 해당 요청에 대한 에러를 처리할 수 있다.

프로젝트의 코드에서는 "권한없음"이라는 요청을 받으면 권한을 받으러 Login Page로 이동을 시켜준다.

assets 📂

assets폴더에는 정적인 파일 예를들어
이미지, 음악, 비디오, 폰트 또는 기타 미디어 파일등이 들어가게 된다.
왜 정적인 파일이 들어가냐면 assets폴더는 public 폴더와 마친가지로 빌드 프로세스의 영향을 받지 않는다. 그래서 그래도 복사되어 애플리케이션의 배포 버전에 포함이 된다.

그래서 프로젝트에서는 현재 Bookstore의 logo를 저장해 놓았다.

Components 📂

Components는 여기서 또 한번 디렉토리가 분기된다.
이렇게 분기해주신게 하나의 표준은 아니지만 그래도 필자가 보기에 깔끔하고 좋아보인다.

common, header, layout

의 폴더가 포함되어있다.먼저 Common을 살펴보자

common 폴더 📁

위 폴더에는 공통적으로 계속 사용되는 컴포넌트에 대한 Tsx파일이 들어있다.

Button, Footer, Header, InputText, Title, Error(에러시 나타나는 컴포넌트)등이 있다.

사실 이렇게 공통적으로 사용되는 컴포넌트를 사용할 수 있게 해주는건 Theme
또는 획일화된 css 디자인이라고 생각한다. 이는 theme 디렉토리를 다루면서 제대로 정리를 해보겠다.

결론적으로는 공통적으로 컴포넌트를 사용하되 그 크기와 색깔등은 다를 수 있기 때문에 그 조절을 theme로 조절해주는게 굉장히 편해보이고 재사용성을 높여준다는 경험을 할 수 있었다.

그 중 하나의 예시로 버튼 컴포넌트의 코드를 가져와 보았다.

interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  children: React.ReactNode;
  size: ButtonSize;
  schema: ButtonSchema;
  disabled?: boolean;
  isLoading?: boolean;
}

export default function Button({
  children,
  size,
  schema,
  disabled,
  isLoading,
}: Props) {
  return (
    <ButtonStyle
      size={size}
      schema={schema}
      disabled={disabled}
      isLoading={isLoading}
    >
      {children}
    </ButtonStyle>
  );
}
//react - hook - form

자, 먼저 Button이 가져올 Props에 대한 Interface를 만들어주어야한다.

children은 ReactNode 타입이고(컴포넌트합성)
size, schema(오타났네 scheme인데;;), disabled,isLoading
에 대한 타입을 지정해준후 ButtonStyle컴포넌트 내부에 Children을 포함시켜 Return하는 비교적 간단한 함수이다.

const ButtonStyle = styled.button<Omit<Props, "children">>`
  font-size: ${({ theme, size }) => theme.button[size].fontSize};
  padding: ${({ theme, size }) => theme.button[size].padding};
  color: ${({ theme, schema }) => theme.buttonSchema[schema].color};
  background-color: ${({ theme, schema }) =>
    theme.buttonSchema[schema].backgroundColor};
  border: 0;
  border-radius: ${({ theme }) => theme.borderRadius.default};
  opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
  pointer-events: ${({ disabled }) => (disabled ? "none" : "auto")};
`;

위 처럼 styled 컴포넌트를 통해 ThemeProvider로 감싸진 내부 컴포넌트 (App.tsx) 전체를 theme props를 통해 각종 css 코드를 관장할 수 있다.

header 📁

사실 위 파일은 내부에 ThemeSwitcher가 담겨있는데 해당 Switcher를 모든 컴포넌트에서 사용했으면 하고 이는 Header에 담겨있을 것이기 때문에 Header라는 폴더에 넣어주신거 같은데 필자는 그냥 common 폴더에 ThemeSwicher가 있는게 조금 더 보기가 좋아서 해당 파일을 옮기도록 하였다.

내부에 담겨 있는 ThemeSwitcher는 context 폴더와 함께 연동해서 설명해보겠다!
(이유는 context api를 통해 state로 dark,ligth를 변경하기 때문)

Layout 📁

필자는 사실 이러한 레이아웃을 먼저 짜고 컴포넌트를 구성한다는 걸 처음 배웠다.
바로 컴포넌트를 집어 넣고 컴포넌트에 css코드를 주면서 각 페이지마다 다른 css view가 보여졌는데(의도한건아님..) 이렇게 레이아웃 컴포넌트를 개발하고 이 컴포넌트 안에 컴포넌트 합성을 통해서 해당 페이지에 대한 ReactNode를 넣게 되면 획일화된 view를 경험할 수 있겠구나 싶었다.

코드는 간단하다.

interface LayOutProps {
  children: React.ReactNode;
}
//ReactNode라는 타입은 ? 리액트로 만든 리액트 컴포넌트들이 배치될 수 있다.
export default function LayOut({ children }: LayOutProps) {
  return (
    <>
      <Header />
      <LayoutStyled>{children}</LayoutStyled>
      <Footer />
    </>
  );
}

const LayoutStyled = styled.main`
  width: 100%;
  margin: 0 auto;
  max-width: ${({ theme }) => theme.layout.width.large};
  padding: 20px 0;
`;

대략 다음과 같은 레이아웃이었다.

간단한게 만약 좀 더 복잡한 레이아웃이라도 이렇게 레이아웃을 Return 하는 함수를 먼저 짠 후에 컴포넌트를 합성한다면 재사용성이 높아질 수 있겠구나 싶었다.

context 📂

다음은 context 폴더이다. 이 폴더가 필요한 이유는 ThemeSwitcher를 위해서이다. 왜냐면 Theme가 dark, light 버전이 있다면 이를 state로 관리를 해야 해당 context Provider 하위의 컴포넌트(아마 전체 컴포넌트)가 해당 변경사항을 인지하고 rerendering을 할 수 있기 때문이다.

context 내부의 themeContext파일은 다음과 같다.


interface State {
  themeName: ThemeName;
  toggleThemeName: () => void;
}
const DEFAULT_THEME_NAME = "light";
const THEME_LOCAL_STORAGE_KEY = "book_store_theme";

export const state = {
  themeName: "light" as ThemeName,
  toggleThemeName: () => {},
};

export const ThemeContext = createContext<State>(state);

export const BookStoreThemeProvider = ({
  children,
}: {
  children: ReactNode;
}) => {
  const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);

  const toggleThemeName = () => {
    setThemeName(
      themeName === DEFAULT_THEME_NAME ? "dark" : DEFAULT_THEME_NAME
    );
    localStorage.setItem(
      THEME_LOCAL_STORAGE_KEY,
      themeName === DEFAULT_THEME_NAME ? "dark" : DEFAULT_THEME_NAME
    );
  };

  useEffect(() => {
    const savedThemeName = localStorage.getItem(
      THEME_LOCAL_STORAGE_KEY
    ) as ThemeName;
    setThemeName(savedThemeName || DEFAULT_THEME_NAME);
  }, []);

  return (
    <ThemeContext.Provider value={{ themeName, toggleThemeName }}>
      <ThemeProvider theme={getTheme(themeName)}>
        <GlobalStyle themeName={themeName} />
        {children}
      </ThemeProvider>
    </ThemeContext.Provider>
  );
};

위에서부터 설명을 하면 먼저

State Interface는 themeName, toggleName이라는 변수에 타입을 지정해주는데 themeName은 light와 dark밖에 없기 때문에

type ThemeName = "light"|"dark"

로 지정해주었다. 그리고 toggleThemeName역시 SetState함수가 들어가기 때문에 해당 타입은

toggleThemeName:()=>void

그리고 ThemeContext를 형성하고 ThemeContext.Provider의 value에 각각 State와 setState함수를 넣어준다.

return <ThemeContext.Provider value={{themeName, toggleThemeName}}>
  Context의 value를 공유받는 컴포넌트들
</ThemeContext.Provider>

useState를 통해 State를 생성하고 localstorage가 관여된 이유는 사용자가 다크모드로 사용하다가 웹 페이지를 빠져나갔을 때 여전히 다크모드를 유지하기 위해서이다. (localstorage는 브라우저의 저장소니까 ㅎㅎ)

return <ThemeContext.Provider value={{themeName, toggleThemeName}}>
 <ThemeProvider theme={getTheme(themeName)}>
        <GlobalStyle themeName={themeName} />
        {children}
      </ThemeProvider>
</ThemeContext.Provider>

// getTheme
export const getTheme = (themeName: ThemeName): Theme => {
  switch (themeName) {
    case "light":
      return light;
    case "dark":
      return dark;
  }
};

다음과 같이 적용이 되는 것이다.

context의 핵심은 값 공유인데 하위의 컴포넌트(페이지) 간 값을 공유할 수 있다는 점이다.

style 📂

style 폴더는 말 그대로 style을 담당하는 폴더이다.
프로젝트의 경우에 현재 global.ts, theme.ts가 존재한다.

둘 중 필자는 theme를 적용하는 방법과 타입지정에 대해 배울 수 있었다.

export type ThemeName = "light" | "dark";

export type ColorKey =
  | "primary"
  | "background"
  | "secondary"
  | "third"
  | "border"
  | "text";

export type HeadingSize = "large" | "medium" | "small";

export type ButtonSize = "large" | "medium" | "small";

export type ButtonSchema = "primary" | "normal";

export type LayOutWidth = "large" | "medium" | "small";

interface Theme {
  name: string;
  color: Record<ColorKey, string>;
  heading: {
    [key in HeadingSize]: {
      fontSize: string;
    };
  };
  button: {
    [key in ButtonSize]: {
      fontSize: string;
      padding: string;
    };
  };
  buttonSchema: {
    [key in ButtonSchema]: {
      color: string;
      backgroundColor: string;
    };
  };
  borderRadius: {
    default: string;
  };
  layout: {
    width: {
      [key in LayOutWidth]: string;
    };
  };
}

export const light: Theme = {
  name: "light",
  color: {
    primary: "#ff5800",
    background: "lightgrey",
    secondary: "#5F5F5F",
    third: "green",
    border: "grey",
    text: "black",
  },
  heading: {
    large: {
      fontSize: "2rem",
    },
    medium: {
      fontSize: "1.5rem",
    },
    small: {
      fontSize: "1rem",
    },
  },
  button: {
    large: {
      fontSize: "1.5rem",
      padding: "1rem 2rem",
    },
    medium: {
      fontSize: "1rem",
      padding: "0.5rem 1rem",
    },
    small: {
      fontSize: "0.5rem",
      padding: "0.25rem 0.5rem",
    },
  },
  buttonSchema: {
    primary: {
      color: "white",
      backgroundColor: "midnightblue",
    },
    normal: {
      color: "black",
      backgroundColor: "lightgrey",
    },
  },
  borderRadius: {
    default: "4px",
  },
  layout: {
    width: {
      large: "1020px",
      medium: "760px",
      small: "320px",
    },
  },
};

export const dark: Theme = {
  ...light,
  name: "dark",
  color: {
    primary: "coral",
    background: "midnightblue",
    secondary: "darkblue",
    third: "darkgreen",
    border: "grey",
    text: "black",
  },
};

먼저 필자는 타입스크립트를 본격적으로 사용해본 적이 없어서 타입에 대한 학습을 먼저 진행했다.
코딩악마_유튜브강의

해당 강의를 듣는다면 위 타입에 대한 설정을 이해할 수 있다.

일단 theme는 "light" 버전과 "dark" 버전이 존재한다. 각 테마는 객체로 설정을 한다. 그리고 themeName을 매개변수로 받는 getTheme함수를 생성하여 switch case 문을 사용해 각각 theme에 대한 return 값을 보낸다.

이를 theme provider를 통해 전달해줄 수 있는 것이다.

import { ThemeProvider } from "styled-components";

Styled-components에서 가져오는 모듈이다.

위 themeProvider 하위의 컴포넌트들은 theme라는 Props를 받아올 수 있는데 이를 통해 각 스타일 속성값을 전달해 줄 수 있다.

이는 짝발란스 theme를 직접 설정하면서 다시 스스로 해봐야 더욱 와닿을 거 같다.

global.ts ❓

global 파일은 전역 css를 설정한다.

import "sanitize.css";
import { createGlobalStyle } from "styled-components";
import { ThemeName } from "./theme";

interface Props {
  themeName: ThemeName;
}

export const GlobalStyle = createGlobalStyle<Props>`
body{
    padding: 0;
    margin: 0;
}
h1{
    margin: 0;
}
*{
    color: ${(props) => (props.themeName === "light" ? "black" : "white")};
    background-color: ${(props) =>
      props.themeName === "light" ? "white" : "black"};
}
`;

웹은 브라우저마다 약간의 스타일 서식이 붙어있어 그 Css를 초기화 시키는게 필요하다.

이를 위한 다양한 방법이 있다.

에릭마이어의 css reset, sanitize.css등

각각 장단점이 있는데 에릭마이어의 css를 예전에 React 실습을 진행하면서 사용해본 기억이 있다. 그때 눈에 바로바로 보이는 css 초기값을 집어 넣을 수 있어서 좋았고 또, 오래된 reset css이다 보니 믿고 사용할 수 있었다.

sanitize.css의 장점은 경량화, 웹 표준을 준수하여 브라우저 간 일관된 동작, 정기적 업데이트 등이 있다.

model 📂

model 폴더는 각종 데이터들의 Interface를 저장하는 공간으로 이해했다.

즉, 클라이언트가 axios통신을 통해 받아오는 데이터 (DB데이터)에 대한 Interface를 제공한다. 보통 JSON 형태로 데이터가 들어오기 때문에 Interface로 type을 지정해주게 된다.

hooks 📂

사실 이 hooks 폴더가 평소에 필자는 생각을 못했던 폴더이다 이외의 다른 api폴더나 context폴더, pages등은 리펙토링 과정에서 나올 수 있는 아이디어 였다면 hooks는 상상하지 못한 정체다ㅋㅋ

커스텀 Hook을 넣어놓는 공간이다.

그럼 custom hook을 왜 사용할까?

  • 로직의 재사용
  • 상태 로직 추상화
    등이 있다.

일단 규칙으로는 use로 사용하는 것이 규칙이다. 그 예시를 가져와 보았다.

export const useCategory = () => {
  const [category, setCategory] = useState<Category[]>([]);

  useEffect(() => {
    fetchCategory().then((category) => {
      if (!category) {
        return;
      }
      const categoryWithAll = [{ id: null, name: "전체" }, ...category];

      setCategory(categoryWithAll);
    });
  }, []);

  return { category };
};

위 코드는 category data를 axios Function을 통해 데이터를 페칭해온 후 해당 데이터가 없다면? undefined를 return하고
데이터가 있다면 전체라는 데이터를 추가해 객체의 형태로 데이터를 전달한다.

즉, state형식의 데이터(카테고리데이터)를 전달해주는 역할을 한다.

export const useAlert = () => {
  const showAlert = useCallback((message: string) => {
    window.alert(message);
  }, []);
  return showAlert;
};

다음과 같은 코드도 있다.

useCallback ❓

그런데 useCallback은 왜 사용할까? 그냥 useAlert라는 함수에 매개변수로 message를 받아와서 window.alert(message)하면되지 않나?

메모이제이션을 해준다. 함수를 계속해서 선언하면 메모리에 부하가 있고 재사용성에 있어서 부족한 부분이 있다.
useCallback을 사용하면 동일한 입력에 대해 동일한 함수 인스턴스를 반환하고 함수를 생성하는 것을 최소화해준다.

즉, useCallback은 리렌더링 사이에 함수 정의를 캐시 수 있게 해주는 React Hook

위 코드에서 useCallback을 사용하지 않는다면

 const showAlert = useAlert()

에서 showAlert가 매번 메모리에 할당되는데 useCallback을하면 해당 내부 함수가 caching되어 메모리를 계속해서 차지하지 않게된다.

store 📂

위 Store에서는 저장하는 로직이 담겨있는 것 같다.

authStore라는 파일을 넣었고 해당 파일은 다음과 같다.

import { create } from "zustand";

interface StoreState {
  isloggedIn: boolean;
  storeLogin: (token: string) => void;
  storeLogout: () => void;
}

export const getToken = () => {
  const token = localStorage.getItem("token");
  return token;
};

const setToken = (token: string) => {
  localStorage.setItem("token", token);
};

export const removeToken = () => {
  localStorage.removeItem("token");
};

export const useAuthStore = create<StoreState>((set) => ({
  isloggedIn: getToken() ? true : false, // 초기값
  storeLogin: (token: string) => {
    set({ isloggedIn: true });
    setToken(token);
  }, //state변경
  storeLogout: () => {
    set({ isloggedIn: false });
    removeToken();
    //state변경
  },
}));

위 코드에서는 JWT토큰을 localstorage에 저장하고 localstorage에 있는 JWT string 값을 axios 통신을 할 때마다 header에 담아 보내게된다.

그럼 만약 그 값이 통과 되면 login이 통과가 되고
실패하면 localstorage에 있는 JWT값은 삭제가 된다.

사실 이 로직을 이해하는게 쉽지 않았다. 필자는 바로 그냥 cookie로 받아온 JWT값을 브라우저 cookie에 저장해두고 통신할 때 마다 cookie는 전달되니 localstorage에 담기는 과정은 필요가 없지 않나? 라는 생각을 했다.

그런 생각이 있다보니 이 흐름이 잘 이해가 안됐다. 일단 지금까지 필자가 이해한 흐름은 다음과 같다.

//login.tsx
  const onSubmit = (data: SignupProps) => {
    login(data).then(
      (res) => {
        //res에는 Token

        //상태변화
        storeLogin(res.token);
        showAlert("로그인이 성공했습니다.");
        navigate("/");
      },
      (error) => {
        showAlert("로그인이 실패했습니다.");
      }
    );
  };

여기에서 fetcher 함수를 통해 token을 받아오고 이미 백엔드 단에서 로그인이 실패하면? 이곳에 올 수 없다. 왜? axios를 그렇게 설정했으니까

export const createClient = (config?: AxiosRequestConfig) => {
  const axiosInstance = axios.create({
    baseURL: BASE_URL,
    timeout: DEFAULT_TIMEOUT,
    headers: {
      "Content-Type": "application/json",
      Authorization: getToken() ? getToken() : "",
    }, // 어차피 JWT는 만료되면? 그 토큰은 쓸 수 없게되니까
    withCredentials: true,
    ...config,
  });
  axiosInstance.interceptors.response.use(
    (response) => {
      return response;
    },
    (error) => {
      if (error.response.status === 401) {
        removeToken();
        window.location.href = "/login";
      }
      return Promise.reject(error);
      //로그인 만료처리
    }
  );
  return axiosInstance;
};

위 interceptor 메소드를 통해 만약 권한이 없다면? login페이지로 다시 돌아가는 로직이 들어가 있다.

그래서 만약 통과가 된다면

storeLogin(res.token);

을 통해 해당 토큰값이 storeLogin이라는 함수로 전달되는 것을 볼 수 있다.

//authStore.ts

export const useAuthStore = create<StoreState>((set) => ({
  isloggedIn: getToken() ? true : false, // 초기값
  storeLogin: (token: string) => {
    set({ isloggedIn: true });
    setToken(token);
  }, //state변경
  storeLogout: () => {
    set({ isloggedIn: false });
    removeToken();
    //state변경
  },
}));

여기에서 zustand를 통해 전역으로 해당 값이 들어오고 set을 통해 isloggedIn이 True로 그리고 localstorage에 값이 저장이 된다.

그리고 localstorage에 담겨있는 JWT string과 계속 통신을 하게 된다 어떻게??

export const createClient = (config?: AxiosRequestConfig) => {
  const axiosInstance = axios.create({
    baseURL: BASE_URL,
    timeout: DEFAULT_TIMEOUT,
    headers: {
      "Content-Type": "application/json",
      Authorization: getToken() ? getToken() : "",
    }, // 어차피 JWT는 만료되면? 그 토큰은 쓸 수 없게되니까
    withCredentials: true,
    ...config,
  });

위 코드에서 Authorization이라는 header 속성에 담겨 보내지고 만약 만료되었다면? 백엔드에서 return 값을 보내게될 것이다. 아마 그 http status code는 401(권한없음)으로 전달이 될 것이다. 그럼 다시 login page로 돌아가는 로직이다!

그런데 사실 아직 cookie에 굳이 저장하지 않은 이유는 나의 개인적인 생각으로 보안상 계속 해당 Jwt string을 넘겨주는 건 좋지 않기 때문이 아닐까 싶다.

그래서 Localstorage에 jwt를 저장하는 것과 Cookie에 jwt를 저장하는 것에 대한 차이를 비교해보았다.

Localstorage
장점

  • 클라이언트 측에서 쉽게 엑세스 가능하다.
  • 서버와의 통신이 필요하지 않으므로 빠르게 엑세스 가능하다.
  • 데이터가 영구적으로 보관
    단점
  • XXS공격에 취약하다.
  • 보안을 위한 조치가 필요

Cookie
장점

  • 서버와의 통신이 필요하지 않아 빠르게 엑세스
  • 특정 도메인 또는 경로에서만 엑세스 가능
    단점
  • 서버로 매번 요청을 할 때 마다 쿠키가 전송되어 오버헤드가 발생할 수 있다.
  • 쿠키는 도메인에서 개수가 제한된다.
  • 클라이언트에서 수정이 가능하기 때문에 보안상 주의가 필요하다.

utils 📂

utils는 비교적 넓은 범위의 함수가 들어가는 거 같다.
뭐 예를 들어 날짜를 계산해주는 함수, 시간을 알려주는 함수, 숫자를 3자리 단위로 끊어주는 함수 등
자주 사용은 되지만 그 범위를 제한하기 힘든 함수는 utils 폴더에 들어간다.

0개의 댓글

관련 채용 정보