당니의 개발자 스토리
리액트 컴포넌트 만들기 실습 본문
물론입니다! 컴포넌트 페이지 설명과 실습 요구조건 충족 설명을 위한 대본을 작성해드리겠습니다.
### 컴포넌트 페이지 대본
안녕하세요, 오늘 저는 `NameInsight` 컴포넌트를 소개하고, 이 컴포넌트가 어떻게 구성되어 있는지, 어떤 기능들을 구현했는지 설명드리겠습니다.
1. **소개**
- 먼저, 이 컴포넌트는 사용자가 입력한 이름에 대한 성별 정보와 인기도 정보를 제공하는 페이지입니다. `Genderize.io`와 `Agify.io` API를 사용하여 이러한 정보를 가져옵니다.
2. **기능 설명**
- 사용자가 이름을 입력하면, 두 개의 버튼 중 하나를 눌러 이름의 성별 정보 또는 인기도 정보를 가져올 수 있습니다.
- "Get Gender" 버튼을 클릭하면 `Genderize.io` API를 호출하여 이름의 성별 정보를 가져옵니다.
- "Get Popularity" 버튼을 클릭하면 `Agify.io` API를 호출하여 이름의 인기도 정보를 가져옵니다.
- 입력 필드를 초기화할 수 있는 "Delete" 버튼도 제공됩니다.
- API 요청 중에는 로딩 메시지가 표시되며, 요청 실패 시 에러 메시지가 표시됩니다.
3. **코드 설명**
- **상태 관리**: `useState` 훅을 사용하여 이름, 성별 정보, 인기도 정보, 로딩 상태, 에러 메시지를 관리합니다.
- **API 호출**: `axios` 라이브러리를 사용하여 API 요청을 처리합니다. 각각의 API 요청 함수는 성공 시 데이터를 상태에 저장하고, 실패 시 에러 메시지를 상태에 저장합니다.
- **렌더링**: 컴포넌트는 입력 필드와 버튼, 그리고 API 응답 데이터를 렌더링합니다. 로딩 상태와 에러 메시지도 조건부로 렌더링됩니다.
- **스타일링**: CSS를 사용하여 컴포넌트의 스타일을 정의하고, 호버 시 효과를 추가했습니다. 예를 들어, 입력 필드와 버튼에 호버 시 색상 및 크기 변화를 주어 사용자 인터랙션을 강조했습니다.
### 실습 요구조건 충족 설명 대본
이번 프로젝트에서 요구된 실습 조건을 모두 충족하는지 확인해보겠습니다:
1. **개인별로 REST API를 이용해서 서비스 화면(컴포넌트)을 구성**:
- `NameInsight` 컴포넌트는 `Genderize.io`와 `Agify.io` API를 사용하여 이름의 성별 정보와 인기도 정보를 가져오는 기능을 구현하였습니다.
2. **개인별로 만든 서비스 화면(컴포넌트)을 리액트 라우터를 이용해서 메뉴로 구성**:
- `App` 컴포넌트에서 리액트 라우터를 사용하여 `Home` 컴포넌트와 `NameInsight` 컴포넌트를 각각의 경로에 매핑하였습니다. 네비게이션 바를 통해 사용자가 각 페이지로 이동할 수 있도록 구성하였습니다.
3. **사용한 API에 대한 설명 (요청 데이터, 응답 데이터 구조)**:
- **Genderize.io API**:
- 요청 데이터: 이름 (name)
- 응답 데이터 구조:
{
"name": "example",
"gender": "male/female",
"probability": 0.99,
"count": 12345
}
- Agify.io API:
- 요청 데이터: 이름 (name)
- 응답 데이터 구조:
{
"name": "example",
"age": 25,
"count": 12345
}
이와 같이 각 API의 요청 데이터와 응답 데이터 구조를 설명할 수 있습니다. 이렇게 해서 전체 요구조건을 모두 충족하였음을 확인할 수 있습니다.
`Agify.io` API의 응답 데이터에서 `count` 필드는 이름이 주어진 연령대에서 얼마나 자주 사용되는지를 나타냅니다. `NameInsight` 컴포넌트에서 이 데이터를 표시할 때 "Popularity Count:"로 라벨링하여 사용자의 이해를 돕도록 변경한 것입니다.
해당 부분은 `NameInsight.js` 파일의 아래 코드에서 설정되어 있습니다:
{popularityInfo && (
<div>
<p><span className="popularity-label">Popularity Count:</span> {popularityInfo.count}</p>
<p><span className="age-label">Average Age:</span> {popularityInfo.age}</p>
</div>
)}
### 설명 대본
**API 응답 데이터 구조 설명 시**:
- `Agify.io` API의 `count` 필드는 이름이 주어진 연령대에서 얼마나 자주 사용되는지를 나타냅니다.
- 이를 사용자에게 더 쉽게 이해시키기 위해 "Popularity Count:"로 라벨링하여 표시했습니다.
- 코드에서는 `popularityInfo.count` 값을 가져와 "Popularity Count:"라는 라벨과 함께 화면에 출력합니다.
- count: 이 필드는 특정 이름이 분석된 데이터 집합에서 얼마나 자주 사용되는지를 나타냅니다.
- age: 이 필드는 특정 이름의 예상 평균 연령을 나타냅니다. 예를 들어, "age": 25는 해당 이름을 가진 사람들의 평균 연령이 25세임을 의미합니다.
"호버(Hover)"**는 웹에서 마우스를 특정 요소 위에 올렸을 때 일어나는 상태를 말합니다. 이 상태에서는 해당 요소에 스타일 변화를 줄 수 있습니다.
button {
padding: 12px 25px; /* 내부 여백 설정 */
font-size: 16px; /* 폰트 크기 설정 */
background: linear-gradient(90deg, #00c6ff, #0072ff); /* 배경을 그라디언트로 설정 */
color: white; /* 텍스트 색상을 흰색으로 설정 */
border: none; /* 테두리 제거 */
border-radius: 4px; /* 둥근 테두리 설정 */
cursor: pointer; /* 커서를 포인터로 설정 */
transition: background-color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease; /* 배경색, 변형, 그림자에 애니메이션 추가 */
margin: 5px; /* 마진 설정 */
}
button:hover {
background: linear-gradient(90deg, #0072ff, #00c6ff); /* 호버 시 배경 그라디언트 반전 */
transform: scale(1.05); /* 호버 시 살짝 확대 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); /* 호버 시 그림자 효과 강화 */
}
이 부분에서 버튼 배경이 선형 그라디언트로 설정되어 있습니다. 기본 상태에서 왼쪽에서 오른쪽으로 파란색(#00c6ff)에서 어두운 파란색(#0072ff)으로 그라디언트가 적용되며, 호버 시 그라디언트가 반전되어 나타납니다.
전체 코드
NamingInsight.js
import React, { useState } from 'react'; // 리액트와 useState 훅을 가져옴
import axios from 'axios'; // HTTP 요청을 위한 axios 라이브러리 가져옴
import './NameInsight.css'; // NameInsight 컴포넌트에 대한 CSS 파일 가져옴
const NameInsight = () => { // NameInsight 컴포넌트 정의
const [name, setName] = useState(''); // 이름 입력 값을 위한 상태 변수와 setter
const [genderInfo, setGenderInfo] = useState(null); // 성별 정보 상태 변수와 setter
const [popularityInfo, setPopularityInfo] = useState(null); // 인기도 정보 상태 변수와 setter
const [loading, setLoading] = useState(false); // 로딩 상태 변수와 setter
const [error, setError] = useState(''); // 에러 메시지 상태 변수와 setter
const fetchGender = () => { // 성별 정보를 가져오기 위한 함수
setLoading(true); // 로딩 상태를 true로 설정
setError(''); // 에러 메시지 초기화
const genderEndpoint = `https://api.genderize.io/?name=${name}`; // genderize.io API 엔드포인트 설정
axios.get(genderEndpoint) // axios를 사용해 API 요청
.then(res => {
setGenderInfo(res.data); // 성공적으로 데이터를 받아온 경우 성별 정보 상태 업데이트
setLoading(false); // 로딩 상태를 false로 설정
})
.catch(err => {
setError('Failed to fetch gender data. Please try again.'); // 에러 발생 시 에러 메시지 설정
setLoading(false); // 로딩 상태를 false로 설정
});
};
const fetchPopularity = () => { // 인기도 정보를 가져오기 위한 함수
setLoading(true); // 로딩 상태를 true로 설정
setError(''); // 에러 메시지 초기화
const popularityEndpoint = `https://api.agify.io?name=${name}`; // agify.io API 엔드포인트 설정
axios.get(popularityEndpoint) // axios를 사용해 API 요청
.then(res => {
setPopularityInfo(res.data); // 성공적으로 데이터를 받아온 경우 인기도 정보 상태 업데이트
setLoading(false); // 로딩 상태를 false로 설정
})
.catch(err => {
setError('Failed to fetch popularity data. Please try again.'); // 에러 발생 시 에러 메시지 설정
setLoading(false); // 로딩 상태를 false로 설정
});
};
const clearInput = () => { // 입력 값을 초기화하기 위한 함수
setName(''); // 이름 상태를 빈 문자열로 초기화
setGenderInfo(null); // 성별 정보 상태를 초기화
setPopularityInfo(null); // 인기도 정보 상태를 초기화
setError(''); // 에러 메시지 상태를 초기화
};
return (
<div className="name-insight-container"> // 컴포넌트 컨테이너
<h1>Name Insight</h1> // 제목
<p>This page uses the Genderize.io and Agify.io APIs</p> // 페이지 설명
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} placeholder="Enter name" /> // 이름 입력 필드
<button onClick={fetchGender}>Get Gender</button> // 성별 정보 요청 버튼
<button onClick={fetchPopularity}>Get Popularity</button> // 인기도 정보 요청 버튼
<button onClick={clearInput}>Delete</button> // 입력 값 초기화 버튼
</div>
{loading && <p>Loading...</p>} // 로딩 중일 때 표시할 텍스트
{error && <p className="error">{error}</p>} // 에러 발생 시 표시할 텍스트
{genderInfo && ( // 성별 정보가 있을 때 표시할 내용
<div>
<p><span className="name-label">Name:</span> {genderInfo.name}</p>
<p><span className="gender-label">Gender:</span> {genderInfo.gender}</p>
<p><span className="probability-label">Probability:</span> {genderInfo.probability}</p>
</div>
)}
{popularityInfo && ( // 인기도 정보가 있을 때 표시할 내용
<div>
<p><span className="popularity-label">Popularity Count:</span> {popularityInfo.count}</p>
<p><span className="age-label">Average Age:</span> {popularityInfo.age}</p>
</div>
)}
</div>
);
};
export default NameInsight; // NameInsight 컴포넌트를 기본으로 내보냄
NamingInsight.css
/* Existing styles retained */
.name-insight-container {
width: 80%; /* 컨테이너 너비를 80%로 설정 */
max-width: 600px; /* 최대 너비를 600px로 설정 */
margin: 40px auto; /* 상하단에 40px, 좌우에 auto 마진을 설정하여 중앙 정렬 */
padding: 30px; /* 내부 여백 설정 */
background: #ffffff; /* 배경색을 흰색으로 설정 */
border-radius: 12px; /* 둥근 테두리 설정 */
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); /* 그림자 효과 */
text-align: center; /* 텍스트 중앙 정렬 */
transition: transform 0.3s ease, box-shadow 0.3s ease; /* 변형과 그림자 효과에 애니메이션 추가 */
}
.name-insight-container:hover {
transform: scale(1.03); /* 호버 시 살짝 확대 */
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); /* 호버 시 그림자 효과 강화 */
}
input[type="text"] {
padding: 12px; /* 내부 여백 설정 */
font-size: 16px; /* 폰트 크기 설정 */
width: 70%; /* 너비를 70%로 설정 */
margin-right: 10px; /* 오른쪽에 10px 마진 설정 */
border-radius: 4px; /* 둥근 테두리 설정 */
border: 1px solid #ccc; /* 테두리 색상을 회색으로 설정 */
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); /* 내부 그림자 효과 */
transition: border-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease; /* 테두리, 그림자, 색상에 애니메이션 추가 */
}
input[type="text"]:focus {
border-color: #0072ff; /* 포커스 시 테두리 색상을 파란색으로 설정 */
outline: none; /* 기본 포커스 테두리 제거 */
box-shadow: 0 0 10px rgba(0, 114, 255, 0.3); /* 포커스 시 그림자 효과 추가 */
color: #0072ff; /* 포커스 시 텍스트 색상을 파란색으로 설정 */
}
button {
padding: 12px 25px; /* 내부 여백 설정 */
font-size: 16px; /* 폰트 크기 설정 */
background: linear-gradient(90deg, #00c6ff, #0072ff); /* 배경을 그라디언트로 설정 */
color: white; /* 텍스트 색상을 흰색으로 설정 */
border: none; /* 테두리 제거 */
border-radius: 4px; /* 둥근 테두리 설정 */
cursor: pointer; /* 커서를 포인터로 설정 */
transition: background-color 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease; /* 배경색, 변형, 그림자에 애니메이션 추가 */
margin: 5px; /* 마진 설정 */
}
button:hover {
background: linear-gradient(90deg, #0072ff, #00c6ff); /* 호버 시 배경 그라디언트 반전 */
transform: scale(1.05); /* 호버 시 살짝 확대 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); /* 호버 시 그림자 효과 강화 */
}
.name-label {
color: #0072ff; /* 이름 텍스트 색상을 파란색으로 설정 */
}
.gender-label {
color: #ff5722; /* 성별 텍스트 색상을 주황색으로 설정 */
}
.probability-label {
color: #4caf50; /* 확률 텍스트 색상을 녹색으로 설정 */
}
.popularity-label {
color: #ffeb3b; /* 인기 텍스트 색상을 노란색으로 설정 */
}
.age-label {
color: #9c27b0; /* 나이 텍스트 색상을 보라색으로 설정 */
}
.error {
color: red; /* 에러 메시지 색상을 빨간색으로 설정 */
font-weight: bold; /* 에러 메시지를 굵게 표시 */
}
App.js
import React from 'react'; // 리액트 라이브러리에서 React를 가져옴
import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; // 리액트 라우터에서 필요한 컴포넌트들 가져옴
import NameInsight from './components/NameInsight'; // NameInsight 컴포넌트를 가져옴
import Home from './components/Home'; // Home 컴포넌트를 가져옴
import './App.css'; // 글로벌 스타일 적용을 위한 CSS 파일 가져옴
const App = () => { // App 컴포넌트 정의
return (
<Router> // Router 컴포넌트로 전체 앱을 감싸서 라우팅 기능 제공
<div>
<nav> // 네비게이션 바 정의
<ul> // 네비게이션 링크 리스트
<li>
<Link to="/">Home</Link> // Home 경로로 연결되는 링크
</li>
<li>
<Link to="/name-insight">Name Insight</Link> // Name Insight 경로로 연결되는 링크
</li>
</ul>
</nav>
<Routes> // Routes 컴포넌트로 경로별로 컴포넌트를 매핑
<Route path="/" element={<Home />} /> // Home 컴포넌트를 '/' 경로에 매핑
<Route path="/name-insight" element={<NameInsight />} /> // NameInsight 컴포넌트를 '/name-insight' 경로에 매핑
</Routes>
</div>
</Router>
);
};
export default App; // App 컴포넌트를 기본으로 내보냄
App.css
body {
font-family: 'Poppins', Arial, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
color: #343a40;
line-height: 1.6;
}
nav {
background: linear-gradient(90deg, #00c6ff, #0072ff);
padding: 15px;
text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-bottom: 3px solid #0072ff;
}
nav ul {
list-style-type: none;
padding: 0;
margin: 0;
display: flex;
justify-content: center;
}
nav ul li {
margin: 0 15px;
}
nav ul li a {
color: white;
text-decoration: none;
font-size: 18px;
transition: color 0.3s ease, background-color 0.3s ease;
padding: 5px 10px;
border-radius: 4px;
}
nav ul li a:hover {
color: #fff176;
background-color: rgba(255, 255, 255, 0.2);
}
Index.js
import React from 'react'; // 리액트 라이브러리에서 React를 가져옴
import ReactDOM from 'react-dom'; // 리액트 DOM 라이브러리에서 ReactDOM을 가져옴
import './index.css'; // CSS 파일을 가져와서 글로벌 스타일로 적용
import App from './App'; // App 컴포넌트를 가져옴
import reportWebVitals from './reportWebVitals'; // 웹 성능 측정 함수 가져옴
ReactDOM.render(
<React.StrictMode> // React.StrictMode로 감싸서 개발 모드에서 문제 감지
<App /> // App 컴포넌트를 렌더링
</React.StrictMode>,
document.getElementById('root') // 루트 DOM 노드에 렌더링
);
reportWebVitals(); // 웹 성능 측정을 위한 함수 호출
Index.css
body {
margin: 0;
/* 모든 여백을 0으로 설정합니다. */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
/* 여러 가지 폰트를 지정하여 사용자의 시스템에 맞는 폰트를 사용하게 합니다. */
-webkit-font-smoothing: antialiased;
/* 웹킷 기반 브라우저에서 폰트를 부드럽게 렌더링합니다. */
-moz-osx-font-smoothing: grayscale;
/* 모질라 기반 브라우저에서 폰트를 회색 음영으로 렌더링합니다. */
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
/* 코드 텍스트에 대한 폰트를 지정합니다. */
}
Home.js
import React from 'react';
const Home = () => {
return (
<div>
<h1>Home</h1>
<p>Welcome to the Name Insight application!</p>
</div>
);
};
export default Home;