Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REACT_APP_BASE_URL=https://panda-market-api.vercel.app
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

훌륭합니다 ! 환경변수로 BASE_URL을 지정하셨군요 !

굿굿 ~! 환경변수는 다음과 같은 이점들이 있지요:

왜 환경 변수에 저장해야 하나요?

개발(dev), 테스트(test), 실제 사용(prod) 등 다양한 환경에서 앱을 운영하게 되는 경우, 각 환경에 따라 다른 base URL을 사용해야 할 수 있습니다. 만약 코드 내에 하드코딩되어 있다면, 각 환경에 맞춰 앱을 배포할 때마다 코드를 변경해야 하며, 이는 매우 번거로운 작업이 됩니다. 하지만, 환경 변수를 .env.production, .env.development, .env.test와 같이 설정해두었다면, 코드에서는 단지 다음과 같이 적용하기만 하면 됩니다.

const apiUrl = `${process.env.REACT_APP_BASE_URL}/api`;

이러한 방식으로 환경 변수를 사용하면, 배포 환경에 따라 쉽게 URL을 변경할 수 있으며, 코드의 가독성과 유지보수성도 개선됩니다.

실제 코드 응용과 관련해서는 다음 한글 아티클을 참고해보세요! => 보러가기

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

환경변수를 .gitignore에 추가해보시는건 어떨까요?

.env 파일은 보통 API 키, 데이터베이스 비밀번호, 서버 URL 등 중요한 설정값을 저장하는 파일이에요.
이런 파일을 형상관리(Git)에 포함하면 보안 문제가 발생할 수 있어요.

  1. 보안 문제 (API 키 & 비밀번호 노출)
    예를 들어, 아래와 같은 .env 파일이 있다고 가정해볼게요.
REACT_APP_API_KEY=123456789abcdef
DATABASE_PASSWORD=my-secret-password

이 파일을 Git에 올리면 누구나 볼 수 있어요.
만약 GitHub에 올라가면, 악의적인 사용자가 API를 도용하거나, 데이터베이스에 무단 접근할 수도 있어요! 😱

  1. 환경별 설정값이 다를 수 있음 (개발/운영 분리)
    개발할 때는 테스트 서버를 사용하지만, 실제 서비스에서는 운영 서버를 사용해야 해요.
    만약 .env가 Git에 포함되면, 모든 사람이 같은 설정을 쓰게 되어 개발 환경과 운영 환경을 구분할 수 없어요.

✅ 올바른 방법
.env.development → 개발용
.env.production → 운영용
실제 실행할 때 .env 파일을 개별적으로 설정

그래서 어떻게 설정할까?

# .gitignore
.env

위와 같이 추가해주시면 됩니다 !

17,305 changes: 15,615 additions & 1,690 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "1-weekly-mission",
"name": "panda-market-react",
"version": "0.1.0",
"private": true,
"dependencies": {
Expand All @@ -8,7 +8,9 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"styled-components": "^6.1.15",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand Down
Binary file removed public/favicon.ico
Binary file not shown.
57 changes: 23 additions & 34 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,43 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<html lang="ko">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>판다마켓</title>

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
<!--
Font preloading
- 페이지가 처음 로딩될 때 텍스트가 순간적으로 깜빡이거나 바뀌는 듯한 현상을 보신 적이 있으신가요? (Flash of Unstyled Text, FOUT)
브라우저가 지정된 폰트를 다운로드 받는 동안 임시로 대체 폰트("fallback font")를 사용하기 때문인데요, 폰트를 preload하면 텍스트 렌더링을 보다 매끄럽게 할 수 있습니다.
- 브라우저는 rel="preload"로 지정된 리소스를 우선적으로 준비합니다.
- 폰트, 페이지 상단의 이미지, 가장 먼저 실행되어야 하는 스크립트 등에 preload 속성을 사용하는 경우가 많아요.
- href의 리소스 유형에 따라 <link>의 `as` 속성값을 정해야 해요. 일반적인 폰트 파일(예: .woff, .ttf 등)이라면 as="font"겠지만, 우리가 사용하는 웹폰트는 css 형식이기 때문에 as="style"을 사용합니다.
-->
<title>React App</title>
<link
rel="preload"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<!-- 간혹 보안 등의 이슈로 자바스크립트를 지원하지 않는 브라우저 환경에서도 최소한의 스타일이 로딩될 수 있도록 합니다. -->
<noscript
><link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"
/></noscript>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file removed public/logo192.png
Binary file not shown.
Binary file removed public/logo512.png
Binary file not shown.
25 changes: 0 additions & 25 deletions public/manifest.json

This file was deleted.

3 changes: 0 additions & 3 deletions public/robots.txt

This file was deleted.

38 changes: 0 additions & 38 deletions src/App.css

This file was deleted.

40 changes: 22 additions & 18 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import logo from './logo.svg';
import './App.css';
import { BrowserRouter, Route, Routes } from "react-router-dom";
import HomePage from "./pages/HomePage/HomePage";
import LoginPage from "./pages/LoginPage/LoginPage";
import MarketPage from "./pages/MarketPage/MarketPage";
import AddItemPage from "./pages/AddItemPage/AddItemPage";
import CommunityFeedPage from "./pages/CommunityFeedPage/CommunityFeedPage";
import Header from "./components/Layout/Header";

function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<BrowserRouter>
{/* Global Navigation Bar */}
<Header />

<div className="withHeader">
<Routes>
{/* React Router v6부터는 path="/" 대신 간단하게 `index`라고 표기하면 돼요 */}
<Route index element={<HomePage />} />
<Route path="login" element={<LoginPage />} />
<Route path="items" element={<MarketPage />} />
<Route path="additem" element={<AddItemPage />} />
<Route path="community" element={<CommunityFeedPage />} />
</Routes>
</div>
</BrowserRouter>
);
}

Expand Down
8 changes: 0 additions & 8 deletions src/App.test.js

This file was deleted.

40 changes: 40 additions & 0 deletions src/api/itemApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//itemApi.js
const BASE_URL = process.env.REACT_APP_BASE_URL;

// export async function getProducts(params = {}) {
// // URLSearchParams을 이용하면 파라미터 값을 자동으로 쉽게 인코딩할 수 있어요.
// const query = new URLSearchParams(params).toString();

// try {
// const response = await fetch(`${BASE_URL}/products?${query}`);
// if (!response.ok) {
// throw new Error(`HTTP error: ${response.status}`);
// }
// const body = await response.json();
// return body;
// } catch (error) {
// console.error("Failed to fetch products:", error);
// throw error;
// }
// }

export async function getProducts({ page, pageSize, orderBy, keyword }) {
const queryParams = new URLSearchParams({
page,
pageSize,
orderBy,
});
Comment on lines +22 to +26
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오우 ! URLSearchParams를 사용하셨군요 ? 😊


if (keyword) {
queryParams.append("keyword", keyword);
Comment on lines +22 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다음 코드와 똑같지 않을까 예상됩니다 !

Suggested change
const queryParams = new URLSearchParams({
page,
pageSize,
orderBy,
});
if (keyword) {
queryParams.append("keyword", keyword);
const queryParams = new URLSearchParams({
page,
pageSize,
orderBy,
keyword
});

}

const response = await fetch(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

axios를 사용해보는건 어떨까요?(제안/선택)

fetch 모듈을 잘 만든다는 것은 어렵습니다. 다음 사항들을 고려해볼 수 있어요:

  1. 만약 get이 아닌 메써드(post, patch, delete 등)일 경우는 어떻게 처리할 수 있을까요?
  2. querybody가 필요할 때는 어떻게 처리 할 수 있을까요?
  3. 로그인 인가를 위한 토큰을 request 전에 자동으로 삽입할 수는 없을까요? (인증/인가를 자동으로 할 수 없을까요?)
  4. 처음 한 번에 Base URL을 지정할 수는 없을까요?
    1. Base URL을 사용하다가 타 Domain에 보내야 될 때는 어떻게 할 수 있을까요?
      이 모든 요구사항들을 '잘 만든다는 것'은 어려워요. 따라서 이 모든걸 만들어진 fetch 모듈을 사용해보고 후에 fetch모듈을 만들어 보는 것도 좋은 학습 방법이 될 수 있어요.

axios 시작하기

어떻게 세팅하면 될까? 🤔

instance를 만들어서 export를 하고 사용해보는 것 정도로 시도해보면 좋을 것 같아요. axios-instance 파일을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:

const baseURL = process.env.NEXT_PUBLIC_LINKBRARY_BaseURL;

const instance = axios.create({
  baseURL: baseURL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default instance

axios instance

인가에 필요한 accessTokenlocalStorage가 있다면 axios의 인터셉터를 활용할 수 있습니다 !

인터셉터는 혼자 해결해보시는 것을 권장드립니다. 혹시 모르시겠으면 다음 위클리 미션에 질문해주세요. 😊

사용 방법 🚀

사용 방법은 정말 간단해요. 다음과 같이 사용할 수 있습니다:

instance.get(`/user/${userId}`)

딱 보니. 마이그레이션도 정말 쉽게 할 수 있겠죠? 😊

axios API

`https://panda-market-api.vercel.app/products?${queryParams.toString()}`
);
if (!response.ok) {
Comment on lines +32 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BASE_URL을 정의하셨기에 다음과 같이 작성해볼 수 있어요 !

Suggested change
const response = await fetch(
`https://panda-market-api.vercel.app/products?${queryParams.toString()}`
);
if (!response.ok) {
const response = await fetch(
`${BASE_URL}/products?${queryParams.toString()}`
);
if (!response.ok) {

throw new Error(`HTTP error: ${response.status}`);
}

return response.json();
}
3 changes: 3 additions & 0 deletions src/assets/images/icons/arrow_left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/images/icons/arrow_right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/images/icons/ic_heart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/images/icons/ic_search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/images/icons/ic_sort.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions src/assets/images/logo/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/placeholder.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions src/components/Layout/Header.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.headerLeft {
display: flex;
align-items: center;
}

.headerLogo {
margin-right: 16px;
}

.globalHeader nav ul {
display: flex;
list-style: none;
gap: 8px;
font-weight: bold;
font-size: 16px;
color: #4b5563;
}

.globalHeader nav ul li a:hover {
color: var(--blue);
}

.loginLink {
font-size: 16px;
font-weight: 600;
border-radius: 8px;
padding: 11.5px 23px;
}

@media (min-width: 768px) {
.globalHeader nav ul {
gap: 36px;
font-size: 18px;
}

.headerLogo {
margin-right: 35px;
}
}

@media (min-width: 1280px) {
.headerLogo {
margin-right: 47px;
}
}
42 changes: 42 additions & 0 deletions src/components/Layout/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";
import Logo from "../../assets/images/logo/logo.svg";
import { Link, NavLink } from "react-router-dom";
import "./Header.css";

// react-router-dom의 NavLink를 이용하면 활성화된 네비게이션 항목을 하이라이트해줄 수 있어요!
function getLinkStyle({ isActive }) {
return { color: isActive ? "var(--blue)" : undefined };
}

function Header() {
return (
<header className="globalHeader">
<div className="headerLeft">
<Link to="/" className="headerLogo" aria-label="홈으로 이동">
<img src={Logo} alt="판다마켓 로고" width="153" />
</Link>

<nav>
<ul>
<li>
<NavLink to="/community" style={getLinkStyle}>
자유게시판
</NavLink>
</li>
<li>
<NavLink to="/items" style={getLinkStyle}>
중고마켓
</NavLink>
</li>
</ul>
</nav>
</div>

<Link to="/login" className="loginLink button">
로그인
</Link>
</header>
);
}

export default Header;
Loading
Loading