반응형
4. 컴포넌트 분석 (1): AdminStudyList.jsx
이 컴포넌트는 관리자 페이지의 핵심 기능 중 하나로, 서버에 저장된 모든 교육 데이터(장르, 주제, 예문, 음성)를 불러와 계층 구조(아코디언 UI)로 보여주고, 각 항목을 삭제할 수 있는 기능을 제공합니다.
4.1. 코드 전문 (주석 포함)
// React에서 필요한 훅(hook)들을 가져옵니다.
import { useEffect, useState } from "react"
// Redux와 상호작용하기 위한 훅들을 가져옵니다.
import { useDispatch, useSelector } from "react-redux";
// 페이지 이동을 위한 훅을 가져옵니다.
import { useNavigate } from "react-router-dom";
// Redux 슬라이스에 정의된 액션 생성자 함수들을 가져옵니다.
import { setGenres, setStudies, setExams, setAudios } from "../store/adminSlice";
// API 요청 함수들을 가져옵니다.
import { audioApi, examApi, genreApi, studyApi } from "../api/adminApi";
export default function AdminStudyList(props) {
// [*] React, Redux, Router의 핵심 훅(Hook) 초기화
const navigate = useNavigate(); // 페이지 이동 함수
const dispatch = useDispatch(); // Redux 액션을 실행시키는 함수
// useSelector: Redux 스토어의 상태(state)를 선택(select)하여 가져옵니다.
// 스토어의 상태가 변경되면, 이 컴포넌트는 자동으로 리렌더링됩니다.
const genres = useSelector(state => state.admin.genres);
const studies = useSelector(state => state.admin.studies);
const exams = useSelector(state => state.admin.exams);
const audios = useSelector(state => state.admin.audios);
// [*] 컴포넌트 자체의 상태(Local State) 관리
// 이 상태들은 UI의 상호작용(클릭 등)을 제어하기 위해 사용됩니다.
const [selectedGenreNo, setSelectedGenreNo] = useState(null); // 사용자가 클릭한 장르의 ID를 저장 (null이면 아무것도 선택 안됨)
const [selectedStudyNo, setSelectedStudyNo] = useState(null); // 사용자가 클릭한 주제의 ID를 저장
// [*] useEffect: 컴포넌트가 처음 마운트(생성)될 때 특정 작업을 수행합니다.
useEffect(() => {
// 컴포넌트가 화면에 처음 나타날 때 모든 데이터를 서버에서 불러옵니다.
fetchAllData();
}, []); // 두 번째 인자로 빈 배열 `[]`을 전달하면, 이 효과는 마운트 시 딱 한 번만 실행됩니다.
// [1] 모든 데이터를 서버에서 조회하여 Redux 스토어에 저장하는 함수
const fetchAllData = async () => {
try {
// Promise.all: 여러 개의 비동기 요청을 동시에 보내고, 모든 요청이 완료될 때까지 기다립니다.
// 이렇게 하면 각 API를 순차적으로 기다릴 필요 없이 병렬로 처리하여 로딩 속도가 빨라집니다.
const [genreRes, studyRes, examRes, audioRes] = await Promise.all([
genreApi.getAll(),
studyApi.getAll(),
examApi.getAll(),
audioApi.getAll()
])
// dispatch: Redux의 리듀서를 호출하여 스토어의 상태를 업데이트합니다.
// 서버에서 받아온 데이터를 각각의 상태에 저장합니다.
dispatch(setGenres(genreRes.data));
dispatch(setStudies(studyRes.data));
dispatch(setExams(examRes.data));
dispatch(setAudios(audioRes.data));
} catch (e) {
console.error("데이터 조회 실패:", e);
alert("데이터 호출 중 오류가 발생했습니다.");
}
}
// [2] 삭제 관련 함수들
// [2-1] 장르 삭제
const handleDeleteGenre = async (genreNo) => {
// window.confirm: 사용자에게 정말 삭제할 것인지 확인받는 대화상자를 띄웁니다.
if (!window.confirm("이 장르를 삭제하시겠습니까?")) return; // '취소'를 누르면 함수 종료
try {
await genreApi.delete(genreNo); // API를 통해 서버에 삭제 요청
alert("장르가 삭제되었습니다.");
fetchAllData(); // 삭제 후, 최신 데이터를 다시 불러와 화면을 갱신합니다.
} catch (e) {
console.error("장르 삭제 실패:", e);
alert("장르 삭제에 실패했습니다. 하위 주제와 예문이 있는지 확인해주세요.");
}
}
// (handleDeleteStudy, handleDeleteExam, handleDeleteAudio 함수들도 동일한 구조)
// ...
// [3] 데이터 필터링 함수들
// Redux 스토어에 저장된 전체 데이터 목록에서 필요한 부분만 필터링하여 반환합니다.
// [3-1] 특정 장르에 속한 주제들만 필터링
const getStudiesByGenre = (genreNo) => {
return studies.filter(study => study.genreNo == genreNo);
};
// [3-2] 특정 주제에 속한 예문들만 필터링
const getExamsByStudy = (studyNo) => {
return exams.filter(exam => exam.studyNo == studyNo);
};
// [3-3] 특정 예문에 속한 음성들만 필터링
const getAudiosByExam = (examNo) => {
return audios.filter(audio => audio.examNo == examNo);
}
// [4] 음성 언어 코드(1, 2)를 텍스트("한국어", "영어")로 변환하는 유틸리티 함수
const getLangText = (lang) => {
const langMap = { 1: "한국어", 2: "영어" };
return langMap[lang] || '알 수 없는 언어코드입니다.';
}
// JSX: 화면에 렌더링될 UI 구조
return (<>
{/* ... */}
{/* 장르 목록을 순회하며 화면에 표시 */}
{genres.map(genre => (
<div key={genre.genreNo} /* 각 항목은 고유한 key prop이 필요 */ >
{/* 장르 헤더 */}
<div
// 클릭 시 selectedGenreNo 상태를 변경하여 아코디언 UI를 토글(열고/닫고)합니다.
onClick={() => setSelectedGenreNo(selectedGenreNo === genre.genreNo ? null : genre.genreNo)}
>
{/* ... */}
</div>
{/* 선택된 장르의 주제 목록 표시 */}
{/* selectedGenreNo가 현재 장르의 genreNo와 같을 때만 이 부분이 렌더링됩니다. (조건부 렌더링) */}
{selectedGenreNo === genre.genreNo && (
<div>
{/* ... */}
{/* 해당 장르의 주제 목록을 순회하며 화면에 표시 */}
{getStudiesByGenre(genre.genreNo).map(study => (
<div key={study.studyNo}>
{/* 주제 헤더 */}
<div
// 클릭 시 selectedStudyNo 상태를 변경하여 하위 아코디언을 토글합니다.
onClick={() => setSelectedStudyNo(selectedStudyNo === study.studyNo ? null : study.studyNo)}
>
{/* ... */}
<button onClick={(e) => {
e.stopPropagation(); // 이벤트 버블링 방지. 이 버튼을 눌렀을 때 부모 div의 onClick이 실행되지 않도록 함.
navigate(`/admin/study/edit/${study.studyNo}`); // 수정 페이지로 이동
}}>
수정
</button>
{/* ... */}
</div>
{/* 선택된 주제의 상세 정보 및 예문 목록 표시 */}
{selectedStudyNo === study.studyNo && (
<div>
{/* ... */}
{/* 해당 주제의 예문 목록을 순회하며 화면에 표시 */}
{getExamsByStudy(study.studyNo).map(exam => (
<div key={exam.examNo}>
{/* ... */}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
{/* ... */}
</>)
}
4.2. 핵심 분석
데이터 흐름:
Server→API→Redux Store→Component- 컴포넌트가 렌더링되면(
useEffect),fetchAllData함수가adminApi의 함수들을 호출하여 서버에서 모든 데이터를 가져옵니다. - 가져온 데이터는
dispatch를 통해 Redux 스토어의 각 상태(genres,studies등)에 저장됩니다. useSelector는 스토어의 데이터가 변경된 것을 감지하고, 컴포넌트에 최신 데이터를 제공하여 화면을 다시 그리게(리렌더링) 합니다.
- 컴포넌트가 렌더링되면(
상태 관리의 두 종류
- 전역 상태 (Global State, by Redux):
genres,studies등 여러 컴포넌트에서 공유되거나 앱의 핵심이 되는 데이터입니다. 앱이 살아있는 동안 계속 유지됩니다. - 지역 상태 (Local State, by
useState):selectedGenreNo,selectedStudyNo와 같이 특정 컴포넌트의 UI 상호작용만을 위해 사용되는 데이터입니다. 이 컴포넌트가 사라지면 함께 사라집니다.
- 전역 상태 (Global State, by Redux):
효율적인 데이터 렌더링
- 모든 데이터를 한 번에 불러와 Redux 스토어에 저장한 뒤,
getStudiesByGenre,getExamsByStudy같은 필터링 함수를 사용해 화면에 필요한 데이터를 조합하여 보여줍니다. - 이렇게 하면 사용자가 UI를 클릭할 때마다 서버에 다시 요청을 보낼 필요가 없어 반응성이 매우 빨라집니다. (데이터가 아주 많아지면 다른 전략이 필요할 수 있습니다.)
- 모든 데이터를 한 번에 불러와 Redux 스토어에 저장한 뒤,
아코디언(Accordion) UI 구현
selectedGenreNo와selectedStudyNo라는 지역 상태를 이용하여 구현합니다.- 사용자가 장르를 클릭하면
onClick이벤트 핸들러가setSelectedGenreNo를 호출하여 상태를 업데이트합니다. - React는 상태 변경을 감지하고 컴포넌트를 리렌더링합니다.
- 이때
{selectedGenreNo === genre.genreNo && ...}와 같은 조건부 렌더링 구문에 의해, 선택된 장르의 하위 목록만 화면에 보이게 됩니다. 다시 클릭하여 상태가null이 되면 해당 부분은 렌더링되지 않습니다.
e.stopPropagation()의 역할- 수정/삭제 버튼은 클릭 가능한
div내부에 있습니다. 만약stopPropagation이 없다면, 버튼을 클릭했을 때 버튼의onClick이벤트와 부모div의onClick이벤트가 모두 실행되어 버립니다 (이벤트 버블링). e.stopPropagation()은 이벤트가 부모 요소로 전파되는 것을 막아, 버튼 클릭 시에는 버튼 기능만 수행되도록 보장합니다.
- 수정/삭제 버튼은 클릭 가능한
반응형
'프로젝트 > 웹' 카테고리의 다른 글
| [React] 기타 컴포넌트 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (1) | 2025.10.16 |
|---|---|
| [React] 수정 컴포넌트 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] 등록 컴포넌트 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] 관리자단 슬라이스 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] API 계층 분석 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |
| [React] 프로젝트 구조 : 한국어 교육 웹 서비스 '재밌는한국어' 프로젝트 (0) | 2025.10.16 |