✍️ 7주차 복습

김수진·2025년 7월 29일

UI 구현

우선 나는 로그인 & 회원가입 api연결과 메인페이지 및 사이드바를 만든 후, 각 페이지별 UI를 구현하는 역할을 맡았다. (승윤선배님이 지도 API 모든 부분들을 맡으셨다.)
이번 프로젝트에서는 로그인/회원가입 화면과 메인 페이지 이후 화면을 분리하여 라우팅을 설정하였다.
react-router-dom을 사용하여 페이지별로 라우팅을 설정하였고, 공통 레이아웃(사이드바 + 지도)을 고정하고, 각페이지의 콘텐츠만 변경되는 구조를 만들었다.

main.jsx: 초기 진입 라우팅 구성

앱의 진입점인 main.jsx에서는 전체 페이지를 BrowserRouter로 감싼 뒤, 최상위 라우트를 나누었다.

<Routes>
  <Route path="/" element={<LoginForm />} />
  <Route path="/app/*" element={<App />} />
  <Route path="/register" element={<Register />} />
</Routes>

/: 로그인 페이지
/register: 회원가입 페이지
/app/*: 로그인 이후 페이지 (사이드바 및 전체 레이아웃 포함)

이렇게 나누어, 로그인/회원가입 페이지에서는 사이드바가 보이지 않도록 하고, 로그인 이후 /app 하위 경로에서만 사이드바와 지도 레이아웃이 유지되도록 했다.

App.jsx: 전체 레이아웃 및 내부 라우팅

다음으로는 app안에서 일어날 페이지 이동을 구현하였다.
여기에서는 왼쪽 사이드바 + 콘텐츠, 오른쪽 지도 화면으로 나뉘는 고정된 레이아웃을 설정하였다.

<div className="layout-container">
  <div className="left-section">
    <Sidebar />
    <Routes>...</Routes>
  </div>
  <div className="right-section">
    <Mapview />
  </div>
</div>

위에 있는 <Routes>...</Routes> 안에는 밑에 코드를 작성하였다.

<Routes>
  <Route path="postlist" element={<PostListPage ... />} />
  <Route path="create" element={<PostCreatePage />} />
  <Route path="post/:id" element={<PostCoursePage />} />
  <Route path="certification" element={<CertificationPage />} />
  <Route path="scrapbook" element={<ScrapbookPage />} />
  <Route path="mypage" element={<MyPage />} />
  <Route path="*" element={<Navigate to="postlist" />} />
</Routes>

main & App

main에서 App을 import 해서 합쳐주면 완성~

API 연결

제일 헤맸던 부분...

.env 설정

백엔드와 통신을 위해 .env 파일에 다음과 같이 작성했다.

VITE_API_URL="http://192.168.142.61:8080/api"

우리 프로젝트의 모든 API 주소는 /api/~ 형식으로 되어 있었기 때문에, 공통 경로인 /api를 .env에서 설정하고 코드에서는 이를 불러오는 방식으로 구성했다.

baseApi 설정

처음에는 간단히 axios.create()를 이용해 API 요청 파일인 baseApi.jsx를 만들었다... 하지만 API 호출이 계속 실패했고, 개발자 도구를 열어보아, 원인을 파악했다.

accessToken이 저장되지 않아 백엔드에서 인증이 되지 않았다는 것을 알게 되었다. (흠..사실 토큰 저장을 프론트에서 해야한다는 것을 처음 알게 되었다.) 로그인 후 발급되는 accessToken은 localStoragesessionStorage에 저장된다는 것을 알아냈다.!

baseApi코드에 accessToken을 저장할 수 있는 axios 요청을 추가했다.

import axios from "axios";

const baseApi = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true,
});

baseApi.interceptors.request.use((config) => {
  const token =
    localStorage.getItem("accessToken") ||
    sessionStorage.getItem("accessToken"); //요 부분 !!

  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default baseApi;

이렇게 설정한 이후로는 모든 요청에 토큰이 자동으로 포함된다! (토큰 자동 저장 중요 !!!!) 따라서 인증이 필요한 API도 정상적으로 동작했다.

로그인 & 회원가입

로그인 & 회원가입을 구현하기 위해 먼저 LoginForm.jsxRegister.jsx 파일을 만들었다.
프로젝트를 하면서 api연결은 처음이라 사실 뭐가 필요한지도 모르고 처음에는 LoginForm.jsx만 코딩해갔다가... Register.jsx를 코딩해야 토큰이 발급이 되어, LoginForm.jsx가 실행된다는 것을 백엔드 선배님들이 알려주셨다.. 하하

백엔드 API 명세서를 참고하여 아래와 같이 회원가입 요청을 보냈다.

const response = await baseApi.post("/auth/signup", {
  email,
  password,
  name,
  nickname,
});

이메일과 닉네임 중복 확인 API도 따로 호출해야 했기 때문에 각각 다음과 같이 작성했다.

const res = await baseApi.get("/auth/check-email", {
  params: { email },
});
const res = await baseApi.get("/auth/check-nickname", {
  params: { nickname },
});

이렇게 구현한 후 회원가입 요청이 정상적으로 이루어졌고, accessToken도 응답으로 잘 전달되었다!

그리고 나서 LoginForm.jsx 에 해당하는 코드를 작성해줘야 했었는데...

 try {
      const response = await baseApi.post("/auth/login", {
        email: id,
        password,
      });
      console.log("response 전체: ", response);

      const { accessToken } = response.data.data; //response.data X 

api 데이터 형식과 주소 모두 다 제대로 작성했었는데 api연결을 계속 실패했다..ㅜㅜ 개발자 도구를 열어서 확인해 보니,

accessToken이 response.data에 있는 것이 아니라, response.data.data에 있었다.!!!!!! 바로 수정했더니 바로 연결을 성공했다.

게시글 조회 API 연결

게시글 리스트를 불러오는 API를 작성했다.

const res = await baseApi.get("/articles", {
  params: { region, category, sort: sortOrder },
});

하지만 데이터를 받아오는 과정에서 posts.map is not a function이라는 오류가 발생했다. 원인은 API 응답 데이터가 배열이 아닌 객체 형태였기 때문이었다.

응답 구조:

{
  "data": {
    "content": [ ... ]
  }
}

프론트에서는 content 배열만 필요했기 때문에 수정해야했다.

const data = res.data.data.content;

게시글 API는 인증이 필요한 요청이므로.... API를 호출할 때 Authorization 헤더에 accessToken이 반드시 포함되어야 했고, 이를 위해 앞에서 작성한 baseApi

  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

이 부분을 추가해주었다. 이로써, 별도로 토큰을 붙이지 않아도 자동으로 인증이 처리되었다!

느낀점

이번 프로젝트를 하면서 처음으로 API를 직접 연결해보게 됐다.
사실 처음엔 "그냥 요청만 잘 보내면 되겠지"라고 생각했는데, 막상 해보니 생각보다 고려해야 할 부분이 많았다.
응답 구조 파악, 토큰 저장, 이러한 부분들을 어떤 요청에 포함시켜야 하는지까지... 으악.... 처음이라 실패를 정말 많이 했고, 이젠 뭔가 뭔지를 모르겠다! 싶은 순간들이 있었지만, 이러한 부분들을 해결하면서, 프론트와 백엔드의 역할을 명확하게 알아갈 수 있었던 시간이였다.

가장 기억에 남는 건 accessToken 관련 문제였다.
처음엔 로그인 응답이 잘 오는데도 계속 인증이 실패해서 한참을 헤맸는데, 개발자 도구를 열어 구조를 하나하나 뜯어보다가 response.data.data 안에 토큰이 있다는 걸 알게 됐고, 그걸 수정하고 나서야 API가 제대로 연결됐다. 프론트도 데이터를 볼 줄 알아야한다는 점이 중요하다는 것을 깨닫게 되었다!
이젠 api가 뭔지 좀 알 것 같다.

+ 팀원분의 활약

사실 가장 걱정되었던 부분이 지도 api를 가져와서 그 위에 마커와 경로 선들을 표시할 수 있을까? 였다. 선배님이 열심히 해주셨다!!

지도를 띄우는데 성공하시더니,

마커와 경로 선들을 지도 위에 표시하시는 것도 성공하셨다.

지도 색칠도 성공하셨다... 굿굿

정확한 로직은 모르지만... 아마 sido.json 파일로 경로 좌표들을 가져와서

if (isMyPage) {
          loadGeoJson("/json/sido.json");
        }

        kakao.maps.event.addListener(map, "zoom_changed", () => {
          const level = map.getLevel();
          if (!detailMode && level <= 10) {
            detailMode = true;
            clearPolygons();
            loadGeoJson("/json/sig.json");
          } else if (detailMode && level > 10) {
            detailMode = false;
            clearPolygons();
            loadGeoJson("/json/sido.json");
          } else {
            clearPolygons();
          }
        });

        function clearPolygons() {
          polygons.forEach((poly) => poly.setMap(null));
          polygons = [];
          areas = [];
        }

뭐.. 이렇게 하신 것 같다! 나중을 위해서 선배님께 배워야겠다! 굿굿

0개의 댓글