feat: 학습 센터 아카데미 확장 — 카테고리당 10강좌, 총 55강좌 체제
- 신규 45강좌: 컴퓨터 기초 8, 네트워크 9, 서버와 데이터 8, 소프트웨어 7, 디자인 8, 부록 툴 가이드 5(Docker Desktop·DBeaver·VS Code·IntelliJ·Postman) - courseCatalog.jsx 신설: 코스 목록을 데이터로 일원화, lazy 지연 로딩으로 코스별 청크 분리 - App.jsx 라우트를 카탈로그 기반 자동 생성으로 리팩터링, 허브는 카탈로그를 그리기만 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3adebdb44a
commit
96f92eceba
@ -1,22 +1,17 @@
|
||||
// 이 파일이 하는 일: 앱의 라우팅(주소 → 페이지 연결)과 공통 레이아웃(상단 네비게이션)을 정의한다.
|
||||
// /login 이외의 모든 페이지는 로그인해야 볼 수 있고, /mentor는 MENTOR 역할만 접근 가능하다.
|
||||
//
|
||||
// 학습 포인트: 코스 라우트는 여기 일일이 쓰지 않는다 — courseCatalog.jsx의 데이터를 .map으로 돌려
|
||||
// 자동 생성한다. 코스가 100개가 되어도 이 파일은 그대로다.
|
||||
import { Suspense } from 'react';
|
||||
import { Routes, Route, NavLink, Navigate, Outlet } from 'react-router-dom';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { ALL_COURSES } from './courseCatalog';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import SignupPage from './pages/SignupPage';
|
||||
import FindAccountPage from './pages/FindAccountPage';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
import LearnHubPage from './pages/LearnHubPage';
|
||||
import CodingBasicsPage from './pages/CodingBasicsPage';
|
||||
import HardwarePage from './pages/HardwarePage';
|
||||
import AssemblyPage from './pages/AssemblyPage';
|
||||
import NetworkPage from './pages/NetworkPage';
|
||||
import LinuxPage from './pages/LinuxPage';
|
||||
import SqlPage from './pages/SqlPage';
|
||||
import HtmlCssPage from './pages/HtmlCssPage';
|
||||
import JavascriptPage from './pages/JavascriptPage';
|
||||
import UiUxPage from './pages/UiUxPage';
|
||||
import FigmaPage from './pages/FigmaPage';
|
||||
import DocsPage from './pages/DocsPage';
|
||||
import DocViewerPage from './pages/DocViewerPage';
|
||||
import AssignmentsPage from './pages/AssignmentsPage';
|
||||
@ -92,7 +87,11 @@ function Layout() {
|
||||
</div>
|
||||
</header>
|
||||
<main className="container">
|
||||
<Outlet />
|
||||
{/* 학습 포인트: 코스 페이지들은 lazy(지연 로딩)라서 처음 열 때 파일을 내려받는다.
|
||||
그 잠깐 동안 보여줄 대체 화면(fallback)을 Suspense가 담당한다. */}
|
||||
<Suspense fallback={<p className="empty">코스를 불러오는 중...</p>}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
@ -111,18 +110,15 @@ export default function App() {
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
{/* 학습 센터: 허브 + 코스들. /learn 아래로 모아 메뉴 하나로 정리했다. */}
|
||||
{/* 학습 센터: 허브 + 코스들(카탈로그에서 자동 생성) */}
|
||||
<Route path="/learn" element={<LearnHubPage />} />
|
||||
<Route path="/learn/coding" element={<CodingBasicsPage />} />
|
||||
<Route path="/learn/hardware" element={<HardwarePage />} />
|
||||
<Route path="/learn/assembly" element={<AssemblyPage />} />
|
||||
<Route path="/learn/network" element={<NetworkPage />} />
|
||||
<Route path="/learn/linux" element={<LinuxPage />} />
|
||||
<Route path="/learn/sql" element={<SqlPage />} />
|
||||
<Route path="/learn/htmlcss" element={<HtmlCssPage />} />
|
||||
<Route path="/learn/javascript" element={<JavascriptPage />} />
|
||||
<Route path="/learn/uiux" element={<UiUxPage />} />
|
||||
<Route path="/learn/figma" element={<FigmaPage />} />
|
||||
{ALL_COURSES.map((course) => (
|
||||
<Route
|
||||
key={course.slug}
|
||||
path={`/learn/${course.slug}`}
|
||||
element={<course.Component />}
|
||||
/>
|
||||
))}
|
||||
<Route path="/setup" element={<SetupGuidePage />} />
|
||||
<Route path="/docs" element={<DocsPage />} />
|
||||
<Route path="/docs/:slug" element={<DocViewerPage />} />
|
||||
|
||||
175
frontend/src/courseCatalog.jsx
Normal file
175
frontend/src/courseCatalog.jsx
Normal file
@ -0,0 +1,175 @@
|
||||
// 이 파일이 하는 일: 학습 센터의 "코스 카탈로그" — 모든 코스의 목록(데이터)과
|
||||
// 화면 컴포넌트 연결을 한곳에서 관리한다. 코스를 추가하려면 이 파일에 한 항목만 넣으면 된다.
|
||||
//
|
||||
// 학습 포인트 1: 코스가 55개가 되면 App.jsx에 라우트를 일일이 쓰는 방식은 못 버틴다.
|
||||
// "데이터(배열) → 화면(map)" 패턴으로 바꾸면 코드는 그대로 두고 데이터만 늘리면 된다.
|
||||
// 학습 포인트 2: lazy(지연 로딩) — 사용자가 그 코스를 열 때만 해당 파일을 내려받는다.
|
||||
// 안 그러면 첫 접속 때 55개 코스 전체를 다 내려받아 첫 화면이 느려진다.
|
||||
// 빌드 결과(dist/assets)를 보면 코스마다 파일이 쪼개져(chunk) 있는 걸 확인할 수 있다.
|
||||
import { lazy } from 'react';
|
||||
|
||||
// ── 기존 코스 (pages/) ──
|
||||
const HardwarePage = lazy(() => import('./pages/HardwarePage'));
|
||||
const AssemblyPage = lazy(() => import('./pages/AssemblyPage'));
|
||||
const NetworkPage = lazy(() => import('./pages/NetworkPage'));
|
||||
const LinuxPage = lazy(() => import('./pages/LinuxPage'));
|
||||
const SqlPage = lazy(() => import('./pages/SqlPage'));
|
||||
const CodingBasicsPage = lazy(() => import('./pages/CodingBasicsPage'));
|
||||
const HtmlCssPage = lazy(() => import('./pages/HtmlCssPage'));
|
||||
const JavascriptPage = lazy(() => import('./pages/JavascriptPage'));
|
||||
const UiUxPage = lazy(() => import('./pages/UiUxPage'));
|
||||
const FigmaPage = lazy(() => import('./pages/FigmaPage'));
|
||||
|
||||
// ── 신규 코스 (pages/courses/) ──
|
||||
// 컴퓨터 기초
|
||||
const OsPage = lazy(() => import('./pages/courses/OsPage'));
|
||||
const CpuDeepPage = lazy(() => import('./pages/courses/CpuDeepPage'));
|
||||
const MemoryStoragePage = lazy(() => import('./pages/courses/MemoryStoragePage'));
|
||||
const FilesPage = lazy(() => import('./pages/courses/FilesPage'));
|
||||
const BinaryPage = lazy(() => import('./pages/courses/BinaryPage'));
|
||||
const TroubleshootingPage = lazy(() => import('./pages/courses/TroubleshootingPage'));
|
||||
const PeripheralsPage = lazy(() => import('./pages/courses/PeripheralsPage'));
|
||||
const HistoryPage = lazy(() => import('./pages/courses/HistoryPage'));
|
||||
// 네트워크
|
||||
const IpDeepPage = lazy(() => import('./pages/courses/IpDeepPage'));
|
||||
const HomeNetworkPage = lazy(() => import('./pages/courses/HomeNetworkPage'));
|
||||
const DnsDeepPage = lazy(() => import('./pages/courses/DnsDeepPage'));
|
||||
const HttpPage = lazy(() => import('./pages/courses/HttpPage'));
|
||||
const TcpUdpPage = lazy(() => import('./pages/courses/TcpUdpPage'));
|
||||
const NetToolsPage = lazy(() => import('./pages/courses/NetToolsPage'));
|
||||
const WifiSecurityPage = lazy(() => import('./pages/courses/WifiSecurityPage'));
|
||||
const FirewallPage = lazy(() => import('./pages/courses/FirewallPage'));
|
||||
const CloudNetworkPage = lazy(() => import('./pages/courses/CloudNetworkPage'));
|
||||
// 서버와 데이터
|
||||
const LinuxAdvancedPage = lazy(() => import('./pages/courses/LinuxAdvancedPage'));
|
||||
const ServerAnatomyPage = lazy(() => import('./pages/courses/ServerAnatomyPage'));
|
||||
const AwsIntroPage = lazy(() => import('./pages/courses/AwsIntroPage'));
|
||||
const DockerIntroPage = lazy(() => import('./pages/courses/DockerIntroPage'));
|
||||
const SqlIntermediatePage = lazy(() => import('./pages/courses/SqlIntermediatePage'));
|
||||
const DataModelingPage = lazy(() => import('./pages/courses/DataModelingPage'));
|
||||
const ApiPage = lazy(() => import('./pages/courses/ApiPage'));
|
||||
const OpsBasicsPage = lazy(() => import('./pages/courses/OpsBasicsPage'));
|
||||
// 소프트웨어
|
||||
const GitDeepPage = lazy(() => import('./pages/courses/GitDeepPage'));
|
||||
const ReactIntroPage = lazy(() => import('./pages/courses/ReactIntroPage'));
|
||||
const JavaBasicsPage = lazy(() => import('./pages/courses/JavaBasicsPage'));
|
||||
const SpringIntroPage = lazy(() => import('./pages/courses/SpringIntroPage'));
|
||||
const DataStructuresPage = lazy(() => import('./pages/courses/DataStructuresPage'));
|
||||
const DebuggingPage = lazy(() => import('./pages/courses/DebuggingPage'));
|
||||
const CleanCodePage = lazy(() => import('./pages/courses/CleanCodePage'));
|
||||
// 디자인
|
||||
const ColorPage = lazy(() => import('./pages/courses/ColorPage'));
|
||||
const TypographyPage = lazy(() => import('./pages/courses/TypographyPage'));
|
||||
const LayoutPage = lazy(() => import('./pages/courses/LayoutPage'));
|
||||
const IconsImagesPage = lazy(() => import('./pages/courses/IconsImagesPage'));
|
||||
const DesignSystemPage = lazy(() => import('./pages/courses/DesignSystemPage'));
|
||||
const MobileDesignPage = lazy(() => import('./pages/courses/MobileDesignPage'));
|
||||
const UserResearchPage = lazy(() => import('./pages/courses/UserResearchPage'));
|
||||
const PortfolioPage = lazy(() => import('./pages/courses/PortfolioPage'));
|
||||
// 부록: 도구
|
||||
const ToolDockerPage = lazy(() => import('./pages/courses/ToolDockerPage'));
|
||||
const ToolDbeaverPage = lazy(() => import('./pages/courses/ToolDbeaverPage'));
|
||||
const ToolVscodePage = lazy(() => import('./pages/courses/ToolVscodePage'));
|
||||
const ToolIntellijPage = lazy(() => import('./pages/courses/ToolIntellijPage'));
|
||||
const ToolPostmanPage = lazy(() => import('./pages/courses/ToolPostmanPage'));
|
||||
|
||||
// 카탈로그 본체. slug = /learn/<slug> 주소가 된다.
|
||||
// external: true 인 항목은 코스 라우트가 아니라 별도 주소로 이동(설치 가이드).
|
||||
export const COURSE_CATEGORIES = [
|
||||
{
|
||||
category: '컴퓨터 기초',
|
||||
desc: '소프트웨어 이전에, 기계 그 자체를 이해해요.',
|
||||
items: [
|
||||
{ slug: 'hardware', icon: '💻', title: '컴퓨터의 구성', desc: 'CPU·RAM·스토리지·메인보드 — 각 부품의 역할과 스펙표 읽는 법.', level: '입문', Component: HardwarePage },
|
||||
{ slug: 'assembly', icon: '🔧', title: '데스크탑 조립하기', desc: '부품 고르기부터 9단계 조립 순서, 초보 실수까지.', level: '입문', Component: AssemblyPage },
|
||||
{ slug: 'os', icon: '🪟', title: '운영체제의 이해', desc: '윈도우·맥·리눅스가 하는 일 — 자원 관리부터 멀티태스킹까지.', level: '입문', Component: OsPage },
|
||||
{ slug: 'cpu-deep', icon: '🧠', title: 'CPU 깊이 보기', desc: '명령어 사이클·코어·캐시·발열 — 두뇌의 작동 원리.', level: '심화', Component: CpuDeepPage },
|
||||
{ slug: 'memory-storage', icon: '💾', title: '메모리와 저장장치 깊이 보기', desc: 'RAM·SSD의 내부, 스왑과 느려짐의 연쇄.', level: '심화', Component: MemoryStoragePage },
|
||||
{ slug: 'files', icon: '📁', title: '파일과 폴더의 원리', desc: '확장자의 진실, 경로, 휴지통과 완전삭제.', level: '입문', Component: FilesPage },
|
||||
{ slug: 'binary', icon: '🔢', title: '2진수와 데이터 표현', desc: '문자·색·이미지가 전부 숫자인 이유.', level: '입문', Component: BinaryPage },
|
||||
{ slug: 'troubleshooting', icon: '🩺', title: '컴퓨터 문제 해결', desc: '느려질 때 점검 순서, 백업 3-2-1, 검색의 기술.', level: '실습', Component: TroubleshootingPage },
|
||||
{ slug: 'peripherals', icon: '🖥️', title: '노트북과 주변기기 고르기', desc: '노트북·모니터·키보드 스펙 읽기 — 개발자 관점.', level: '입문', Component: PeripheralsPage },
|
||||
{ slug: 'history', icon: '📜', title: '컴퓨터의 역사', desc: '에니악에서 AI까지 — 지금 우리 개발에 남긴 것들.', level: '교양', Component: HistoryPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '네트워크',
|
||||
desc: '랜선 한 가닥에서 클라우드까지.',
|
||||
items: [
|
||||
{ slug: 'network', icon: '🌐', title: '네트워크의 이해', desc: '랜선(UTP) 구조 → 인터넷의 구조 → DNS·TCP/IP 한 흐름.', level: '입문', Component: NetworkPage },
|
||||
{ slug: 'ip-deep', icon: '🔍', title: 'IP 주소 깊이 보기', desc: '사설 대역·서브넷·DHCP·IPv6 — 주소 체계의 모든 것.', level: '심화', Component: IpDeepPage },
|
||||
{ slug: 'home-network', icon: '📡', title: '공유기와 홈 네트워크 실전', desc: '관리자 페이지, WiFi 채널, 포트포워딩까지.', level: '실습', Component: HomeNetworkPage },
|
||||
{ slug: 'dns-deep', icon: '📖', title: 'DNS 깊이 보기', desc: '재귀 조회의 여행, 레코드 타입, TTL과 전파.', level: '심화', Component: DnsDeepPage },
|
||||
{ slug: 'http', icon: '📨', title: 'HTTP와 HTTPS', desc: '메서드·상태코드·쿠키(우리 로그인!)·TLS 인증서.', level: '입문', Component: HttpPage },
|
||||
{ slug: 'tcp-udp', icon: '🤝', title: 'TCP와 UDP', desc: '3-way 핸드셰이크, 패킷, 왜 게임은 UDP인가.', level: '심화', Component: TcpUdpPage },
|
||||
{ slug: 'net-tools', icon: '🧰', title: '네트워크 명령어 실전', desc: 'ping·tracert·nslookup·curl — 진단의 무기들.', level: '실습', Component: NetToolsPage },
|
||||
{ slug: 'wifi-security', icon: '🔐', title: '무선 네트워크와 보안', desc: '공용 와이파이의 위험, 피싱 구별, 2단계 인증.', level: '입문', Component: WifiSecurityPage },
|
||||
{ slug: 'firewall', icon: '🛡️', title: '방화벽과 보안 기초', desc: '포트의 여닫음, 우리 AWS 보안그룹 실례, 최소 권한.', level: '입문', Component: FirewallPage },
|
||||
{ slug: 'cloud-network', icon: '☁️', title: '클라우드 네트워크 입문', desc: '리전·VPC·탄력적 IP — 우리 플랫폼의 실제 구성도.', level: '심화', Component: CloudNetworkPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '서버와 데이터',
|
||||
desc: '백엔드 개발자가 매일 쓰는 도구들.',
|
||||
items: [
|
||||
{ slug: 'linux', icon: '🐧', title: '리눅스 / 터미널 기초', desc: '터미널 명령어부터 SSH 서버 접속까지.', level: '입문', Component: LinuxPage },
|
||||
{ slug: 'linux-advanced', icon: '⚙️', title: '리눅스 심화', desc: '셸 스크립트, 크론, 파이프 — 자동화의 시작.', level: '심화', Component: LinuxAdvancedPage },
|
||||
{ slug: 'server-anatomy', icon: '🏗️', title: '서버란 무엇인가', desc: '웹서버·WAS·DB 3계층 — 우리 구성 그대로 해부.', level: '입문', Component: ServerAnatomyPage },
|
||||
{ slug: 'aws-intro', icon: '🌩️', title: '클라우드와 AWS 입문', desc: 'EC2·요금·리전 — 우리 인프라가 교재.', level: '입문', Component: AwsIntroPage },
|
||||
{ slug: 'docker-intro', icon: '🐳', title: '도커 입문', desc: '컨테이너 개념과 우리 docker-compose.yml 한 줄씩 해부.', level: '입문', Component: DockerIntroPage },
|
||||
{ slug: 'sql', icon: '🗄️', title: 'SQL 입문', desc: '우리 플랫폼의 진짜 테이블로 SELECT부터 JOIN까지.', level: '입문', Component: SqlPage },
|
||||
{ slug: 'sql-intermediate', icon: '📊', title: 'SQL 중급', desc: 'GROUP BY·집계·인덱스 — 데이터에서 답을 꺼내기.', level: '심화', Component: SqlIntermediatePage },
|
||||
{ slug: 'data-modeling', icon: '🗂️', title: '데이터 모델링 기초', desc: 'ERD·키·정규화 — 테이블 설계는 정리정돈.', level: '심화', Component: DataModelingPage },
|
||||
{ slug: 'api', icon: '🔌', title: 'API의 이해', desc: 'REST·JSON — 이미 다 써본 우리 API로 배우기.', level: '입문', Component: ApiPage },
|
||||
{ slug: 'ops-basics', icon: '🚨', title: '운영과 장애 대응 기초', desc: '장애의 신호, 로그 읽기, 재시작의 정석.', level: '실습', Component: OpsBasicsPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '소프트웨어',
|
||||
desc: '코드를 읽고, 쓰고, 함께 다듬는 법.',
|
||||
items: [
|
||||
{ slug: 'coding', icon: '⌨️', title: '코딩 기초', desc: '변수·함수·조건·반복을 이 플랫폼의 실제 코드로.', level: '입문', Component: CodingBasicsPage },
|
||||
{ slug: 'htmlcss', icon: '🧱', title: 'HTML / CSS 기초', desc: '웹의 뼈대와 옷 — 박스 모델·플렉스를 우리 스타일로.', level: '입문', Component: HtmlCssPage },
|
||||
{ slug: 'javascript', icon: '⚡', title: '자바스크립트 기초', desc: 'F12 콘솔이 실습장 — 변수에서 fetch까지.', level: '입문', Component: JavascriptPage },
|
||||
{ slug: 'git-deep', icon: '🌳', title: 'Git 깊이 보기', desc: '브랜치·충돌 해결·되돌리기 3형제·PR 매너.', level: '심화', Component: GitDeepPage },
|
||||
{ slug: 'react-intro', icon: '⚛️', title: 'React 입문', desc: '컴포넌트·props·state — 우리 프론트가 교재.', level: '입문', Component: ReactIntroPage },
|
||||
{ slug: 'java-basics', icon: '☕', title: 'Java 기초', desc: '타입·클래스·객체 — Spring으로 가는 첫걸음.', level: '입문', Component: JavaBasicsPage },
|
||||
{ slug: 'spring-intro', icon: '🍃', title: 'Spring Boot 입문', desc: 'DI·어노테이션·계층 구조 — 우리 백엔드 여행.', level: '심화', Component: SpringIntroPage },
|
||||
{ slug: 'data-structures', icon: '🧺', title: '자료구조 맛보기', desc: '배열·스택·큐·해시 — 그릇마다 쓰임이 다르다.', level: '입문', Component: DataStructuresPage },
|
||||
{ slug: 'debugging', icon: '🐞', title: '디버깅과 에러 읽는 법', desc: '스택트레이스·브레이크포인트 — 에러는 단서다.', level: '실습', Component: DebuggingPage },
|
||||
{ slug: 'clean-code', icon: '✨', title: '클린 코드와 코드리뷰', desc: '이름 짓기·작은 함수·리뷰 매너.', level: '심화', Component: CleanCodePage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '디자인',
|
||||
desc: '보기 좋은 것을 넘어, 쓰기 좋은 것을.',
|
||||
items: [
|
||||
{ slug: 'uiux', icon: '🎨', title: 'UI/UX 기초', desc: '좋은 화면의 원칙들 — 우리 플랫폼을 뜯어보며.', level: '입문', Component: UiUxPage },
|
||||
{ slug: 'color', icon: '🌈', title: '색 이론 기초', desc: '색상환·배색·우리 브랜드 블루의 비밀.', level: '입문', Component: ColorPage },
|
||||
{ slug: 'typography', icon: '🔤', title: '타이포그래피', desc: '폰트·크기 위계·행간 — 가독성의 과학.', level: '입문', Component: TypographyPage },
|
||||
{ slug: 'layout', icon: '📐', title: '레이아웃과 그리드', desc: '8pt 그리드·정렬·여백 — 보이지 않는 뼈대.', level: '입문', Component: LayoutPage },
|
||||
{ slug: 'icons-images', icon: '🖼️', title: '아이콘과 이미지', desc: 'SVG vs PNG, 해상도, 그리고 저작권!', level: '입문', Component: IconsImagesPage },
|
||||
{ slug: 'design-system', icon: '🧩', title: '디자인 시스템 입문', desc: '토큰과 컴포넌트 — 우리 global.css가 실물.', level: '심화', Component: DesignSystemPage },
|
||||
{ slug: 'mobile-design', icon: '📱', title: '모바일 디자인', desc: '엄지의 법칙, 터치 타깃, 웹 vs 앱.', level: '입문', Component: MobileDesignPage },
|
||||
{ slug: 'user-research', icon: '🔬', title: '사용자 리서치 기초', desc: '인터뷰·관찰·사용성 테스트 — 감이 아니라 근거로.', level: '심화', Component: UserResearchPage },
|
||||
{ slug: 'figma', icon: '🖌️', title: '피그마와 디자인 툴', desc: '프레임·오토레이아웃·컴포넌트·핸드오프.', level: '실습', Component: FigmaPage },
|
||||
{ slug: 'portfolio', icon: '💼', title: '포트폴리오 만들기', desc: '수습 8주가 곧 재료 — 과정을 이야기로.', level: '실습', Component: PortfolioPage },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '부록 · 도구',
|
||||
desc: '손에 익으면 두 배 빨라지는 도구들.',
|
||||
items: [
|
||||
{ slug: null, external: '/setup', icon: '🛠️', title: '개발 환경 설치 가이드', desc: 'JDK·PATH·Node·Git·IntelliJ — 환경 구축 9단계.', level: '실습' },
|
||||
{ slug: 'tool-docker', icon: '🐳', title: 'Docker Desktop 사용법', desc: '대시보드 읽기, 컨테이너 GUI 관리, 용량 정리.', level: '실습', Component: ToolDockerPage },
|
||||
{ slug: 'tool-dbeaver', icon: '🦫', title: 'DBeaver로 DB 들여다보기', desc: '우리 DB 연결, ERD 자동 생성, GUI로 쿼리.', level: '실습', Component: ToolDbeaverPage },
|
||||
{ slug: 'tool-vscode', icon: '📝', title: 'VS Code 200% 활용', desc: '단축키 15개, 확장 추천, 검색의 기술.', level: '실습', Component: ToolVscodePage },
|
||||
{ slug: 'tool-intellij', icon: '💡', title: 'IntelliJ 꿀팁 모음', desc: 'Shift 두 번, Alt+Enter, 로컬 히스토리의 구원.', level: '실습', Component: ToolIntellijPage },
|
||||
{ slug: 'tool-postman', icon: '🚀', title: 'Postman으로 API 테스트', desc: '화면 없이 API를 직접 두드리는 법.', level: '실습', Component: ToolPostmanPage },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 라우팅용: slug가 있는 모든 코스를 평평한 배열로.
|
||||
// 학습 포인트: flatMap = "각 카테고리의 items를 꺼내 한 배열로 합치기".
|
||||
export const ALL_COURSES = COURSE_CATEGORIES.flatMap((c) => c.items.filter((i) => i.slug));
|
||||
@ -1,118 +1,8 @@
|
||||
// 이 파일이 하는 일: "학습 센터" 허브 — 코스들을 카테고리별 카드로 보여주고
|
||||
// 클릭하면 각 코스 페이지로 이동한다.
|
||||
// 학습 포인트: 코스 목록을 JSX에 일일이 쓰지 않고 데이터(배열)로 만들어 .map으로 그린다.
|
||||
// 나중에 코스가 늘면 배열에 한 줄만 추가하면 된다 — "데이터와 화면의 분리".
|
||||
// 이 파일이 하는 일: "학습 센터" 허브 — courseCatalog의 데이터를 카테고리별 카드로 그린다.
|
||||
// 학습 포인트: 이 파일엔 코스 목록이 없다! 목록은 courseCatalog.jsx(데이터)에 있고,
|
||||
// 여기는 "그리는 법"만 안다. 코스를 추가해도 이 파일은 한 글자도 안 바뀐다.
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const COURSES = [
|
||||
{
|
||||
category: '컴퓨터 기초',
|
||||
desc: '소프트웨어 이전에, 기계 그 자체를 이해해요.',
|
||||
items: [
|
||||
{
|
||||
to: '/learn/hardware',
|
||||
icon: '💻',
|
||||
title: '컴퓨터의 구성',
|
||||
desc: 'CPU·RAM·스토리지·메인보드·파워·그래픽카드 — 각 부품이 무슨 일을 하고, 스펙표를 어떻게 읽는지.',
|
||||
level: '입문',
|
||||
},
|
||||
{
|
||||
to: '/learn/assembly',
|
||||
icon: '🔧',
|
||||
title: '데스크탑 조립하기',
|
||||
desc: '부품 고르는 기준부터 조립 순서, 초보가 자주 하는 실수까지. 조립을 알면 컴퓨터가 두렵지 않아요.',
|
||||
level: '입문',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '네트워크',
|
||||
desc: '랜선 한 가닥에서 인터넷 전체까지.',
|
||||
items: [
|
||||
{
|
||||
to: '/learn/network',
|
||||
icon: '🌐',
|
||||
title: '네트워크의 이해',
|
||||
desc: '랜선(UTP)의 8가닥 구조 → 공유기와 IP → 인터넷의 구조 → TCP/IP와 DNS까지 한 흐름으로.',
|
||||
level: '입문',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '서버와 데이터',
|
||||
desc: '백엔드 개발자가 매일 쓰는 도구들.',
|
||||
items: [
|
||||
{
|
||||
to: '/learn/linux',
|
||||
icon: '🐧',
|
||||
title: '리눅스 / 터미널 기초',
|
||||
desc: '세상의 서버 대부분은 리눅스로 돌아가요. 터미널 명령어부터 SSH로 서버 접속까지.',
|
||||
level: '입문',
|
||||
},
|
||||
{
|
||||
to: '/learn/sql',
|
||||
icon: '🗄️',
|
||||
title: 'SQL 입문',
|
||||
desc: '데이터베이스에게 말 거는 언어. 우리 플랫폼의 진짜 테이블로 SELECT부터 JOIN까지.',
|
||||
level: '입문',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '소프트웨어',
|
||||
desc: '이제 코드를 읽고 쓰는 세계로.',
|
||||
items: [
|
||||
{
|
||||
to: '/learn/coding',
|
||||
icon: '⌨️',
|
||||
title: '코딩 기초',
|
||||
desc: '변수·함수·조건·반복을 이 플랫폼의 실제 코드로 배워요. 여러분이 누른 로그인 버튼도 코드예요.',
|
||||
level: '입문',
|
||||
},
|
||||
{
|
||||
to: '/learn/htmlcss',
|
||||
icon: '🧱',
|
||||
title: 'HTML / CSS 기초',
|
||||
desc: '웹페이지의 뼈대와 옷. 박스 모델·플렉스 레이아웃을 우리 플랫폼의 실제 스타일로 배워요.',
|
||||
level: '입문',
|
||||
},
|
||||
{
|
||||
to: '/learn/javascript',
|
||||
icon: '⚡',
|
||||
title: '자바스크립트 기초',
|
||||
desc: '웹을 움직이게 하는 언어. 변수에서 이벤트·fetch까지 — React를 배우기 전 필수 코스.',
|
||||
level: '입문',
|
||||
},
|
||||
{
|
||||
to: '/setup',
|
||||
icon: '🛠️',
|
||||
title: '개발 환경 설치 가이드',
|
||||
desc: 'JDK·PATH·Node·Git·IntelliJ까지, 내 PC를 개발자의 PC로 만드는 9단계.',
|
||||
level: '실습',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '디자인',
|
||||
desc: '보기 좋은 것을 넘어, 쓰기 좋은 것을 만드는 법.',
|
||||
items: [
|
||||
{
|
||||
to: '/learn/uiux',
|
||||
icon: '🎨',
|
||||
title: 'UI/UX 기초',
|
||||
desc: '사용자는 왜 헤맬까? 좋은 화면의 원칙들을 우리 플랫폼 화면을 뜯어보며 배워요.',
|
||||
level: '입문',
|
||||
},
|
||||
{
|
||||
to: '/learn/figma',
|
||||
icon: '🖌️',
|
||||
title: '피그마와 디자인 툴',
|
||||
desc: '디자이너의 작업대. 프레임·오토레이아웃·컴포넌트, 그리고 개발자에게 넘기는 핸드오프까지.',
|
||||
level: '실습',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
import { COURSE_CATEGORIES, ALL_COURSES } from '../courseCatalog';
|
||||
|
||||
export default function LearnHubPage() {
|
||||
return (
|
||||
@ -121,38 +11,43 @@ export default function LearnHubPage() {
|
||||
<div className="eyebrow">Learning Center</div>
|
||||
<h1>어썸데브 학습 센터</h1>
|
||||
<p>
|
||||
하드웨어에서 네트워크, 그리고 코드까지 — 아래에서 위로 쌓는 순서 그대로 배워요.
|
||||
아침 수업(CS 커리큘럼)과 짝을 이루는 자율 학습 코너입니다.
|
||||
하드웨어에서 네트워크, 서버, 코드, 디자인까지 — 아래에서 위로 쌓는 순서 그대로.
|
||||
총 {ALL_COURSES.length + 1}개 강좌, 아침 수업(CS 커리큘럼)과 짝을 이루는 자율 학습 코너예요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">추천 순서: 컴퓨터 → 네트워크 → 코딩</span>
|
||||
<span className="chip">각 코스 30~60분</span>
|
||||
<span className="chip">추천 순서: 컴퓨터 → 네트워크 → 서버 → 소프트웨어 → 디자인</span>
|
||||
<span className="chip">강좌당 40~90분</span>
|
||||
<span className="chip">입문부터 차근차근</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{COURSES.map((group) => (
|
||||
{COURSE_CATEGORIES.map((group) => (
|
||||
<div key={group.category}>
|
||||
<div className="day-head">
|
||||
<span className="day-badge">{group.category}</span>
|
||||
<span className="day-count">{group.desc}</span>
|
||||
<span className="day-count">
|
||||
{group.desc} · {group.items.length}강좌
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid-cards">
|
||||
{group.items.map((course) => (
|
||||
// 학습 포인트: <Link>는 <a>와 달리 페이지 전체를 새로 안 불러오고
|
||||
// React 라우터가 화면만 갈아끼운다. 그래서 앱이 빠르게 느껴진다.
|
||||
<Link key={course.to} to={course.to} style={{ color: 'inherit' }}>
|
||||
<div className="card card-clickable" style={{ height: '100%' }}>
|
||||
<div style={{ fontSize: 26, marginBottom: 8 }}>{course.icon}</div>
|
||||
<div className="assign-title-row">
|
||||
<span className="assign-title">{course.title}</span>
|
||||
<span className="badge badge-primary">{course.level}</span>
|
||||
{group.items.map((course) => {
|
||||
// external이 있으면 그 주소로, 아니면 /learn/<slug>로 이동한다.
|
||||
const to = course.external || `/learn/${course.slug}`;
|
||||
return (
|
||||
<Link key={to} to={to} style={{ color: 'inherit' }}>
|
||||
<div className="card card-clickable" style={{ height: '100%' }}>
|
||||
<div style={{ fontSize: 26, marginBottom: 8 }}>{course.icon}</div>
|
||||
<div className="assign-title-row">
|
||||
<span className="assign-title">{course.title}</span>
|
||||
<span className="badge badge-primary">{course.level}</span>
|
||||
</div>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginTop: 6 }}>
|
||||
{course.desc}
|
||||
</p>
|
||||
</div>
|
||||
<p className="muted" style={{ fontSize: 13.5, marginTop: 6 }}>
|
||||
{course.desc}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
437
frontend/src/pages/courses/ApiPage.jsx
Normal file
437
frontend/src/pages/courses/ApiPage.jsx
Normal file
@ -0,0 +1,437 @@
|
||||
// 이 파일이 하는 일: "API의 이해" 코스 — 식당 메뉴판 비유에서 출발해
|
||||
// REST 규칙, JSON, 우리 플랫폼의 실제 API, 에러 응답, 직접 호출 실습까지
|
||||
// 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (메뉴판 비유 → REST → JSON → 우리 API 목록 → 요청·응답 흐름 → 에러 설계 → 호출 실습 → Postman 부록)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램·예시 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 예시 상수들 ──
|
||||
|
||||
const CODE_MENU = `식당과 API — 등장인물이 정확히 일치해요
|
||||
|
||||
식당 웹 서비스
|
||||
─────────────────────────────────────────────────
|
||||
손님 프론트엔드(React, 브라우저)
|
||||
메뉴판 API 문서 (뭘 시킬 수 있는지 목록)
|
||||
주문서 요청(Request)
|
||||
주방 백엔드(Spring Boot) + DB(PostgreSQL)
|
||||
나온 음식 응답(Response)
|
||||
|
||||
핵심 약속 두 가지:
|
||||
1) 손님은 주방에 직접 들어가지 않는다
|
||||
→ 프론트는 DB를 직접 만지지 않는다
|
||||
2) 메뉴판에 있는 것만 시킬 수 있다
|
||||
→ 정해진 요청에 정해진 응답. 그래서 서로 예측 가능!`;
|
||||
|
||||
const CODE_REST = `REST의 두 기둥 — "무엇을(URL)" + "어떻게(메서드)"
|
||||
|
||||
자원(무엇을)은 URL로:
|
||||
/api/checklists 체크리스트 전체
|
||||
/api/checklists/3 그중 3번 하나
|
||||
/api/submissions 제출물 전체
|
||||
|
||||
행위(어떻게)는 HTTP 메서드로:
|
||||
GET 조회 "3번 체크리스트 보여줘" (읽기만, 안전)
|
||||
POST 생성 "새 제출물 등록해줘" (새로 만듦)
|
||||
PUT 수정 "3번 항목 내용을 이걸로 바꿔" (통째로 교체)
|
||||
DELETE 삭제 "3번 항목 지워줘"
|
||||
|
||||
같은 URL이라도 메서드가 다르면 전혀 다른 주문이에요:
|
||||
GET /api/checklists/3 → 3번을 '보여줘'
|
||||
DELETE /api/checklists/3 → 3번을 '지워줘'
|
||||
|
||||
나쁜 예) GET /api/deleteChecklist?id=3
|
||||
→ 동사를 URL에 넣지 않아요. 자원은 명사, 행위는 메서드!`;
|
||||
|
||||
const CODE_JSON = `JSON — 프론트와 백엔드가 쓰는 공용어
|
||||
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Docker 기초 실습",
|
||||
"done": false,
|
||||
"tags": ["docker", "infra"],
|
||||
"author": {
|
||||
"name": "김수습",
|
||||
"team": "백엔드"
|
||||
}
|
||||
}
|
||||
|
||||
읽는 법 딱 4가지만 알면 끝:
|
||||
1) { } 객체 — "이름표: 값" 묶음 (author처럼 안에 또 객체 가능)
|
||||
2) [ ] 배열 — 값 여러 개를 순서대로 (tags)
|
||||
3) 이름표(key)는 항상 큰따옴표 "..."
|
||||
4) 값은 문자열·숫자·true/false·null·객체·배열 중 하나
|
||||
|
||||
함정 주의:
|
||||
- 작은따옴표 '...' 금지 (JS에선 되지만 JSON에선 에러!)
|
||||
- 마지막 항목 뒤 쉼표(,) 금지
|
||||
- 주석(//) 못 씀`;
|
||||
|
||||
const CODE_OUR_API = `우리 플랫폼 API — 여러분이 오늘도 이미 다 써본 것들!
|
||||
|
||||
메서드 경로 하는 일 언제 호출됐나
|
||||
──────────────────────────────────────────────────────────────────
|
||||
POST /api/auth/login 로그인, 토큰 발급 로그인 버튼 누를 때
|
||||
GET /api/auth/me 내 정보 조회 페이지 새로고침 직후
|
||||
GET /api/checklists 체크리스트 목록 체크리스트 화면 진입
|
||||
PUT /api/checklists/{id} 항목 완료/해제 체크박스 클릭!
|
||||
GET /api/submissions 내 제출물 목록 제출 현황 화면
|
||||
POST /api/submissions 과제 제출 제출 버튼 누를 때
|
||||
|
||||
→ 체크박스 하나 딸깍한 것도 사실은 PUT 요청 한 발이었어요.
|
||||
섹션 7에서 F12로 이걸 직접 눈으로 확인합니다.`;
|
||||
|
||||
const CODE_FULL_FLOW = `체크박스를 딸깍 — 그 0.3초 동안 벌어지는 일
|
||||
|
||||
① React: 클릭 이벤트 발생
|
||||
fetch('/api/checklists/3', { method: 'PUT', ... }) 실행
|
||||
|
||||
② 요청이 네트워크를 달린다 (네트워크 코스에서 배운 그 길!)
|
||||
PUT /api/checklists/3
|
||||
Authorization: Bearer eyJhbGci... ← "저 로그인한 사람이에요" 신분증
|
||||
Content-Type: application/json
|
||||
{ "done": true } ← 본문(body): 주문 상세
|
||||
|
||||
③ 서버 도착: Caddy(현관) → Spring Boot(주방)
|
||||
- 토큰 검사: 신분증 진짜야? 유효기간 안 지났어?
|
||||
- 권한 검사: 3번 항목, 이 사람 거 맞아?
|
||||
- PostgreSQL에 UPDATE 실행
|
||||
|
||||
④ 응답 귀환
|
||||
200 OK
|
||||
{ "id": 3, "done": true, "updatedAt": "2026-07-16T..." }
|
||||
|
||||
⑤ React: 응답을 받아 화면의 체크박스를 채운다 ✓
|
||||
|
||||
요청 = 메서드 + URL + 헤더 + (본문)
|
||||
응답 = 상태코드 + 헤더 + (본문)
|
||||
이 구조는 어떤 API든 똑같아요.`;
|
||||
|
||||
const CODE_ERRORS = `에러 응답 — 숫자 세 자리에 담긴 서버의 대답
|
||||
|
||||
401 Unauthorized — "누구세요?"
|
||||
신분증(토큰)이 없거나 만료됨.
|
||||
식당 비유: 회원 전용 식당인데 회원증을 안 보여줌
|
||||
해결: 다시 로그인 (프론트가 로그인 화면으로 보내는 이유!)
|
||||
|
||||
403 Forbidden — "누군지는 아는데, 그건 안 돼요"
|
||||
로그인은 했지만 권한이 없음.
|
||||
식당 비유: 회원증은 있는데 VIP룸 전용 메뉴를 주문함
|
||||
예) 수습이 관리자 전용 API를 부르면 403
|
||||
|
||||
404 Not Found — "그런 건 없는데요?"
|
||||
자원이 존재하지 않음.
|
||||
식당 비유: 메뉴판에 없는 음식을 주문함
|
||||
예) GET /api/checklists/99999 (없는 번호)
|
||||
|
||||
셋을 구분하는 게 디버깅의 절반이에요:
|
||||
401 → 내 로그인 상태를 의심
|
||||
403 → 내 권한을 의심
|
||||
404 → 내가 적은 URL(오타·없는 id)을 의심
|
||||
|
||||
그 외 자주 만나는 친구들:
|
||||
400 Bad Request 본문이 이상함 (JSON 문법 오류, 필수값 누락)
|
||||
500 Internal Error 서버 쪽 버그 — 이건 백엔드 개발자를 부를 차례`;
|
||||
|
||||
const CODE_CURL = `# curl — 터미널에서 API를 직접 호출하는 도구 (Windows 10+ 기본 내장)
|
||||
|
||||
# 1) 일단 아무나 볼 수 있는 공개 API로 몸풀기:
|
||||
curl https://edu.awesomedevapp.com/api/health
|
||||
|
||||
# 이런 JSON이 오면 성공:
|
||||
{"status":"UP"}
|
||||
|
||||
# 2) 응답 본문뿐 아니라 상태코드·헤더까지 보기 (-i):
|
||||
curl -i https://edu.awesomedevapp.com/api/health
|
||||
|
||||
HTTP/2 200 ← 상태코드가 맨 윗줄에!
|
||||
content-type: application/json
|
||||
...
|
||||
|
||||
# 3) 로그인 없이 보호된 API를 불러 보기 — 일부러 401 맞아 보기:
|
||||
curl -i https://edu.awesomedevapp.com/api/auth/me
|
||||
|
||||
HTTP/2 401 ← "누구세요?" 방금 배운 그 401이에요.`;
|
||||
|
||||
const CODE_FETCH = `// 브라우저 콘솔 fetch — 지금 이 사이트에 로그인돼 있다면
|
||||
// 브라우저가 이미 내 신분증을 갖고 있어요. 그걸 그대로 써 봅시다.
|
||||
|
||||
// 1) 우리 플랫폼에 로그인된 탭에서 F12 → Console 탭 → 붙여넣기:
|
||||
fetch('/api/auth/me')
|
||||
.then((res) => {
|
||||
console.log('상태코드:', res.status); // 200이 나와야 정상!
|
||||
return res.json(); // 본문을 JSON으로 해석
|
||||
})
|
||||
.then((data) => console.log(data)); // 내 정보 객체가 출력됨
|
||||
|
||||
// 2) 이번엔 없는 자원을 불러서 404를 직접 만나 보기:
|
||||
fetch('/api/checklists/99999')
|
||||
.then((res) => console.log('상태코드:', res.status)); // 404!
|
||||
|
||||
// 시크릿 창(로그인 안 된 상태)에서 1)을 다시 하면? → 401.
|
||||
// 401/403/404를 전부 '내 손으로' 만들어 봤다면 이 코스 절반은 끝!`;
|
||||
|
||||
const CODE_POSTMAN = `curl과 fetch가 익숙해졌다면 — GUI 도구로 갈아탈 시간
|
||||
|
||||
Postman 같은 API 테스트 도구가 해 주는 것:
|
||||
- 메서드·URL·헤더·본문을 폼으로 입력 (오타 지옥 탈출)
|
||||
- 보낸 요청을 저장해 두고 재사용 (컬렉션)
|
||||
- 토큰을 한 번 등록하면 모든 요청에 자동 첨부
|
||||
- 팀원과 요청 모음을 공유 → "이 API 어떻게 불러요?"가 사라짐
|
||||
|
||||
백엔드 개발자의 하루:
|
||||
코드 수정 → 도구에서 요청 발사 → 응답 확인 → 반복
|
||||
(매번 프론트 화면을 거치지 않고 API만 따로 테스트!)
|
||||
|
||||
부록 과제:
|
||||
1) API 테스트 도구를 하나 설치한다 (멘토에게 팀 표준 도구를 물어보기)
|
||||
2) 섹션 7의 health 체크 요청을 GUI로 재현한다
|
||||
3) 로그인 요청(POST /api/auth/login)을 만들어 토큰을 받아 본다
|
||||
4) 그 토큰으로 /api/auth/me 를 호출해 200을 받으면 졸업!`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'API = 메뉴판' },
|
||||
{ n: 2, label: 'REST 규칙' },
|
||||
{ n: 3, label: 'JSON 읽고 쓰기' },
|
||||
{ n: 4, label: '우리 플랫폼 API' },
|
||||
{ n: 5, label: '요청→응답 흐름' },
|
||||
{ n: 6, label: '에러 응답 설계' },
|
||||
{ n: 7, label: '직접 호출 실습' },
|
||||
{ n: 8, label: 'Postman 부록' },
|
||||
];
|
||||
|
||||
export default function ApiPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>API의 이해</h1>
|
||||
<p>
|
||||
여러분이 이 플랫폼에서 누른 로그인 버튼, 딸깍한 체크박스는 전부 API 호출이었어요.
|
||||
이미 매일 쓰고 있던 그것의 정체를 메뉴판 비유 하나로 꿰뚫고, 마지막엔
|
||||
<strong> 내 손으로 직접 API를 호출</strong>해 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 부록 과제</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="API는 식당의 메뉴판이에요" sub="정해진 요청, 정해진 응답 — 그게 전부">
|
||||
<p>
|
||||
<strong>API</strong>(Application Programming Interface)라는 말이 어렵게 들리지만,
|
||||
정체는 <strong>"프로그램끼리 대화하는 약속된 창구"</strong>예요. 식당에 비유하면
|
||||
완벽하게 맞아떨어집니다.
|
||||
</p>
|
||||
<Code>{CODE_MENU}</Code>
|
||||
<p>
|
||||
손님(프론트엔드)은 주방(DB)이 어떻게 생겼는지 몰라도 돼요. 메뉴판(API)에 있는
|
||||
대로 주문하면, 정해진 음식(응답)이 나온다는 것만 알면 됩니다. 반대로 주방은
|
||||
손님이 누구든 <strong>주문서 양식만 지키면</strong> 요리를 해 줘요. 이렇게
|
||||
서로의 속사정을 몰라도 협업할 수 있게 해 주는 게 API의 진짜 힘이에요 —
|
||||
그래서 우리 회사에서도 프론트 담당과 백엔드 담당이 <strong>API 명세부터 합의</strong>하고
|
||||
각자 개발을 시작합니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>용어 한 입</b> "인터페이스(Interface)"는 <strong>서로 다른 두 세계가 만나는 접점</strong>이라는
|
||||
뜻이에요. 키보드는 사람↔컴퓨터의 인터페이스, API는 프로그램↔프로그램의 인터페이스.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="REST — 메뉴판을 쓰는 문법" sub="자원은 URL로, 행위는 메서드로">
|
||||
<p>
|
||||
메뉴판을 아무렇게나 쓰면 손님이 헷갈리겠죠? 그래서 업계에는 메뉴판 작성의
|
||||
표준 문법이 있어요 — 그게 <strong>REST</strong>입니다. 규칙은 딱 두 줄로 요약돼요.
|
||||
<strong> "무엇을"은 URL에, "어떻게"는 HTTP 메서드에</strong> 담는다.
|
||||
</p>
|
||||
<Code>{CODE_REST}</Code>
|
||||
<p>
|
||||
이 규칙의 좋은 점: URL과 메서드만 봐도 <strong>무슨 일이 일어날지 읽혀요</strong>.
|
||||
<span className="icode">DELETE /api/submissions/7</span>을 보면 설명 없이도
|
||||
"7번 제출물을 지우는구나"를 알 수 있죠. 마치 잘 정리된 메뉴판처럼요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>초보 단골 실수</b> — "조회니까 아무거나 GET으로" 는 맞지만, 반대로
|
||||
<strong> 데이터를 바꾸는 일을 GET에 넣으면 사고</strong>가 나요. GET은 브라우저·검색엔진이
|
||||
"안전하다"고 믿고 마음대로 미리 불러가기도 하거든요. 삭제 링크를 GET으로 만들었다가
|
||||
크롤러가 지나가며 데이터를 전부 지운 전설의 사고가 실제로 있었습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="JSON — 요청서와 응답서의 언어" sub="중괄호와 대괄호만 읽으면 끝">
|
||||
<p>
|
||||
주문서(요청 본문)와 음식(응답 본문)에 담기는 데이터는 대부분 <strong>JSON</strong>이라는
|
||||
형식으로 적어요. 프론트(JavaScript)와 백엔드(Java)가 언어는 달라도, JSON이라는
|
||||
<strong> 공용어</strong>로 대화하는 거죠.
|
||||
</p>
|
||||
<Code>{CODE_JSON}</Code>
|
||||
<p>
|
||||
생긴 게 JavaScript 객체와 거의 같아서(이름부터 JavaScript Object Notation)
|
||||
React에서 다루기 편해요. Spring Boot 쪽에서도 Java 객체를 JSON으로,
|
||||
JSON을 Java 객체로 자동 변환해 줍니다 — 양쪽 다 "번역기"를 내장하고 있는 셈이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 브라우저 콘솔(<span className="kbd">F12</span> →
|
||||
<span className="kbd">Console</span>)에서{' '}
|
||||
<span className="icode">{'JSON.parse(\'{"a": 1,}\')'}</span> 를 실행해 보세요 —
|
||||
마지막 쉼표 때문에 에러가 나요. 쉼표를 지우고 다시 실행하면 성공.
|
||||
방금 위에서 배운 "함정"을 몸으로 확인한 겁니다. 거꾸로 변환은{' '}
|
||||
<span className="icode">{'JSON.stringify({ a: 1 })'}</span> 로 해 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="우리 플랫폼의 API 목록" sub="오늘 여러분이 이미 호출한 것들">
|
||||
<p>
|
||||
이론은 충분해요. 이제 <strong>진짜 메뉴판</strong>을 봅시다 — 지금 접속해 있는
|
||||
이 학습 플랫폼의 API예요. 놀랍게도, 여러분은 이걸 전부 이미 써 봤습니다.
|
||||
</p>
|
||||
<Code>{CODE_OUR_API}</Code>
|
||||
<p>
|
||||
섹션 2에서 배운 규칙이 그대로 보이죠? 자원은 명사(<span className="icode">checklists</span>,{' '}
|
||||
<span className="icode">submissions</span>), 행위는 메서드(GET·POST·PUT).{' '}
|
||||
<span className="icode">{'{id}'}</span>처럼 중괄호로 쓴 부분은 실제 호출 때
|
||||
진짜 번호로 바뀌는 자리예요(<span className="icode">/api/checklists/3</span>).
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>관찰 포인트</b> 로그인(<span className="icode">POST /api/auth/login</span>)만
|
||||
유일하게 "자원 생성"이 아닌데도 POST예요. 아이디·비밀번호를 <strong>본문에 숨겨</strong> 보내야
|
||||
하기 때문이에요 — GET은 값이 URL에 노출되거든요. 규칙에는 이렇게 "이유 있는 예외"도 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="요청 → 응답, 전체 흐름 복습" sub="체크박스 한 번의 왕복 여행">
|
||||
<p>
|
||||
네트워크 코스에서 데이터가 달리는 <strong>길</strong>을 배웠다면, 이번엔 그 길 위를
|
||||
달리는 <strong>내용물</strong>을 볼 차례예요. 여러분이 체크리스트에서 체크박스를
|
||||
딸깍하는 순간을 슬로모션으로 재생해 봅시다.
|
||||
</p>
|
||||
<Code>{CODE_FULL_FLOW}</Code>
|
||||
<p>
|
||||
②의 <strong>Authorization 헤더</strong>가 핵심이에요. HTTP는 원래 기억력이 없어서
|
||||
(매 요청이 처음 보는 손님!) 요청마다 <strong>토큰이라는 신분증</strong>을 들고 가야
|
||||
"아, 아까 로그인한 그 사람" 하고 알아봅니다. 로그인할 때 받은 토큰을 브라우저가
|
||||
보관했다가 매 요청에 붙여 주는 거예요.
|
||||
</p>
|
||||
<p>
|
||||
그리고 ③을 다시 보세요 — 검사는 전부 <strong>서버</strong>가 해요. 화면에서 버튼을
|
||||
숨기는 건 예의일 뿐, 진짜 문지기는 Spring Boot입니다. "프론트는 믿지 않는다"는
|
||||
백엔드의 철칙, 다음 섹션의 에러 코드들이 바로 그 문지기의 대답이에요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="에러 응답 — 401, 403, 404의 언어" sub="숫자 세 자리를 읽으면 디버깅이 절반">
|
||||
<p>
|
||||
주문이 항상 성공하는 건 아니에요. 서버는 거절할 때도 <strong>정해진 형식</strong>으로
|
||||
거절합니다 — 그게 상태코드예요. 특히 이 세 형제를 구분할 수 있으면, 에러가 났을 때
|
||||
어디부터 봐야 할지 바로 알 수 있어요.
|
||||
</p>
|
||||
<Code>{CODE_ERRORS}</Code>
|
||||
<div className="warn">
|
||||
<b>401과 403, 진짜 자주 헷갈려요</b> — <strong>401은 "인증" 실패</strong>(너 누구야?),{' '}
|
||||
<strong>403은 "인가" 실패</strong>(누군진 아는데 자격이 없어). 학교로 치면
|
||||
401은 학생증 없이 교문 통과 시도, 403은 학생증은 있지만 교무실 캐비닛을 열려는 것.
|
||||
면접 단골 질문이기도 하니 지금 확실히 잡아 두세요.
|
||||
</div>
|
||||
<p>
|
||||
좋은 API는 상태코드와 함께 <strong>본문에 이유도</strong> 담아 줘요.
|
||||
예를 들어 <span className="icode">{'{"error": "토큰이 만료되었습니다"}'}</span> 처럼요.
|
||||
숫자는 기계(프론트 코드의 분기 처리)를 위해, 메시지는 사람(디버깅하는 개발자)을 위해.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="직접 호출 실습 — curl과 fetch" sub="화면 없이 API만 콕 집어 불러 보기">
|
||||
<p>
|
||||
지금까지는 화면(버튼)을 통해 간접적으로 API를 불렀지만, 개발자는
|
||||
<strong> API만 따로 직접</strong> 호출할 줄 알아야 해요. 도구는 이미 여러분 PC에
|
||||
다 있습니다 — 터미널의 <strong>curl</strong>과 브라우저의 <strong>fetch</strong>.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 터미널(PowerShell)을 열고 아래를 순서대로 실행해 보세요.
|
||||
맨 마지막 요청에서 <strong>일부러 401을 맞아 보는 것</strong>까지가 실습이에요 —
|
||||
에러 코드를 책이 아니라 내 터미널에서 처음 만나면 절대 안 잊어버립니다.
|
||||
</div>
|
||||
<Code>{CODE_CURL}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 이번엔 브라우저 차례. 우리 플랫폼에 <strong>로그인된 탭</strong>에서{' '}
|
||||
<span className="kbd">F12</span> → <span className="kbd">Console</span>을 열고
|
||||
아래 코드를 한 줄씩 실행해 보세요. 200, 404, (시크릿 창에서) 401 — 세 가지 응답을
|
||||
전부 직접 받아 보는 게 목표예요.
|
||||
</div>
|
||||
<Code>{CODE_FETCH}</Code>
|
||||
<p>
|
||||
보너스: <span className="kbd">F12</span> → <span className="kbd">Network</span> 탭을
|
||||
열어 둔 채 체크리스트 화면에서 체크박스를 딸깍해 보세요. 섹션 4 표에서 본{' '}
|
||||
<span className="icode">PUT /api/checklists/...</span> 요청이 진짜로 날아가는 걸
|
||||
목격할 수 있어요. 클릭해서 요청 헤더의 <strong>Authorization</strong>, 응답 탭의{' '}
|
||||
<strong>JSON 본문</strong>까지 확인하면 이 코스의 모든 조각이 한 화면에 모입니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="부록 — API 테스트 도구로 한 단계 더" sub="curl 다음은 GUI, 실무자의 작업대">
|
||||
<p>
|
||||
curl은 훌륭하지만 헤더·토큰·본문이 길어지면 명령어가 금방 지저분해져요.
|
||||
그래서 실무에서는 Postman 같은 <strong>API 테스트 도구</strong>를 작업대로 씁니다.
|
||||
개념은 이미 다 배웠어요 — 같은 요청을 폼에 채워 넣는 것뿐이에요.
|
||||
</p>
|
||||
<Code>{CODE_POSTMAN}</Code>
|
||||
<div className="tip">
|
||||
<b>부록 과제 제출</b> 위 4단계 중 <strong>3번(로그인 → 토큰 받기)</strong> 성공 화면을
|
||||
캡처해서 과제로 제출하세요. 토큰 값은 <strong>신분증</strong>이니까 가운데를 가리고
|
||||
캡처하는 것 잊지 말고요 — 시크릿을 다루는 습관도 채점 포인트입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🍽️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 API는 <strong>메뉴판</strong>(정해진 요청·정해진 응답), REST는{' '}
|
||||
<strong>메뉴판 작성 문법</strong>(자원=URL, 행위=메서드), JSON은{' '}
|
||||
<strong>주문서의 언어</strong>, 401·403·404는 <strong>주방의 거절 멘트</strong>라는 걸
|
||||
알아요. 무엇보다 curl과 fetch로 <strong>직접 주문</strong>까지 넣어 봤죠.
|
||||
다음은 그 주문을 받는 주방 자체를 만들어 볼 차례 —{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link>를 아직 안 봤다면
|
||||
먼저 복습하고, 준비됐다면 백엔드(Spring Boot) 코스에서 여러분만의 API를
|
||||
직접 열어 보세요. 부록 과제 캡처 제출도 잊지 말고요!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
362
frontend/src/pages/courses/AwsIntroPage.jsx
Normal file
362
frontend/src/pages/courses/AwsIntroPage.jsx
Normal file
@ -0,0 +1,362 @@
|
||||
// 이 파일이 하는 일: "클라우드와 AWS 입문" 코스 — "남의 컴퓨터를 빌린다"는 발상이
|
||||
// 왜 혁명이었는지에서 출발해, IaaS/PaaS/SaaS 구분, EC2 인스턴스 읽는 법, 요금 개념,
|
||||
// S3·RDS 맛보기, 그리고 어썸데브 실제 인프라 구성까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (클라우드의 탄생 → 서비스 3계층 → EC2 해부 → 요금 → S3·RDS → 우리 인프라 → 콘솔 견학)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_BEFORE_AFTER = `서버가 필요할 때 — 옛날 방식 vs 클라우드 방식
|
||||
|
||||
옛날(온프레미스) 클라우드
|
||||
───────────────────────── ─────────────────────────
|
||||
서버 컴퓨터를 직접 구매 웹 화면에서 클릭 몇 번
|
||||
(수백만 원, 배송 몇 주) (몇 분 뒤 서버 켜짐)
|
||||
둘 곳 마련: 서버실·에어컨·전기 아마존 데이터센터가 대신 관리
|
||||
고장 나면 내가 고침 고장 난 하드웨어는 AWS가 교체
|
||||
사용자가 줄어도 서버는 그대로 필요 없으면 끄고, 끈 만큼 안 냄
|
||||
|
||||
핵심 발상의 전환:
|
||||
"컴퓨터를 소유"하는 시대 → "컴퓨팅 파워를 구독"하는 시대`;
|
||||
|
||||
const CODE_IAAS_PAAS_SAAS = `피자로 이해하는 IaaS / PaaS / SaaS — "어디까지 내가 하나?"
|
||||
|
||||
집에서 만들기 IaaS PaaS SaaS
|
||||
(온프레미스) (재료 배달) (배달 피자) (피자집 가서 먹기)
|
||||
─────────────────────────────────────────────────────────────
|
||||
도우·토핑 내가 내가 가게가 가게가
|
||||
오븐·굽기 내가 가게가 가게가 가게가
|
||||
접시·테이블 내가 내가 내가 가게가
|
||||
|
||||
IT로 번역하면:
|
||||
IaaS = 가상 서버(컴퓨터)만 빌림. OS 설치부터 앱 배포까지 내가. 예) EC2
|
||||
PaaS = 코드만 올리면 실행 환경은 알아서. 예) Elastic Beanstalk
|
||||
SaaS = 완성된 소프트웨어를 그냥 씀. 예) Gmail, 노션
|
||||
|
||||
우리 어썸데브는? → IaaS(EC2)를 빌려서 그 위에 Docker로 직접 다 올려요.
|
||||
배우는 입장에선 가장 좋은 선택 — 전 과정이 손에 잡히니까!`;
|
||||
|
||||
const CODE_T3_MEDIUM = `t3.medium — 인스턴스 이름 해부하기 (자동차 모델명 읽기와 같아요)
|
||||
|
||||
t 3 . medium
|
||||
│ │ │
|
||||
패밀리 세대 크기
|
||||
(차종) (연식) (배기량)
|
||||
|
||||
t = 범용·버스트 가능(가끔 바쁜 워크로드용, 가성비형)
|
||||
3 = 3세대 (숫자가 클수록 최신 — 보통 더 빠르고 저렴)
|
||||
medium = 크기. nano < micro < small < medium < large < xlarge ...
|
||||
|
||||
t3.medium의 실제 사양: vCPU 2개, 메모리 4GB
|
||||
|
||||
다른 패밀리 맛보기:
|
||||
t = 범용 가성비 m = 범용 균형 c = 컴퓨팅(CPU) 강화
|
||||
r = 메모리 강화 g = GPU 탑재(AI 학습 등)
|
||||
|
||||
→ "t3.medium"이 이제 암호가 아니라
|
||||
"가성비형 3세대, 중간 크기(2코어 4GB)"로 읽히면 성공!`;
|
||||
|
||||
const CODE_PRICING = `요금의 대원칙: "쓴 시간만큼" — 그런데 뭐가 '쓴 것'일까?
|
||||
|
||||
EC2 인스턴스를 껐을 때(중지, Stop):
|
||||
─────────────────────────────────────────
|
||||
멈추는 요금 ✋ 인스턴스 사용료 (시간당 과금이 멈춤)
|
||||
계속 나가는 요금 💸
|
||||
· EBS(붙어 있는 디스크) — 서버가 꺼져도 데이터는 보관 중이니까
|
||||
· 탄력적 IP — 서버에 '연결 안 된' 고정 IP는 오히려 과금!
|
||||
(주차장 자리를 잡아두고 차를 안 대면 자릿세를 내는 셈)
|
||||
|
||||
프리 티어(Free Tier) 맛보기:
|
||||
새 계정 가입 후 일정 기간, 작은 인스턴스(t계열 micro 등)와
|
||||
일정량의 S3·RDS를 무료로 체험 가능. 단, 한도를 넘으면 바로 과금!
|
||||
|
||||
신입이 기억할 한 줄:
|
||||
"실험 끝나면 끄기 + 안 쓰는 디스크·IP는 정리하기"
|
||||
— 클라우드 요금 사고의 90%는 '켜 놓고 잊어버림'에서 나옵니다.`;
|
||||
|
||||
const CODE_S3_RDS = `EC2 말고도 자주 만나는 두 친구 — 한 줄씩만
|
||||
|
||||
S3 (Simple Storage Service)
|
||||
= 용량 무제한 파일 창고. 이미지·백업·로그를 던져 넣는 곳.
|
||||
서버 디스크와 달리 "파일 단위"로 넣고 꺼내는 웹 창고예요.
|
||||
|
||||
RDS (Relational Database Service)
|
||||
= DB(예: PostgreSQL)를 AWS가 대신 설치·백업·관리해 주는 서비스.
|
||||
"DB 서버 돌보기"라는 집안일을 외주 주는 것.
|
||||
|
||||
우리는 어떻게 쓰나?
|
||||
파일 → 아직은 EC2 디스크에 직접 (규모가 커지면 S3 후보)
|
||||
DB → EC2 위 Docker 컨테이너로 PostgreSQL 직접 운영 (RDS 아님)
|
||||
→ 직접 운영은 공부가 되고, 관리형(RDS)은 손이 덜 가요. 트레이드오프!`;
|
||||
|
||||
const CODE_OUR_INFRA = `어썸데브 실제 인프라 — 여러분이 지금 보는 이 페이지가 사는 곳
|
||||
|
||||
인터넷 (여러분의 브라우저)
|
||||
│
|
||||
┌─────────▼─────────┐
|
||||
│ 탄력적 IP (고정 주소) │ ← 서버를 재시작해도 주소가 안 바뀌게
|
||||
├───────────────────┤
|
||||
│ 보안 그룹 (방화벽) │ ← 443(HTTPS)·22(SSH) 등
|
||||
│ │ 허락된 문만 열어 둠
|
||||
├───────────────────┤
|
||||
│ EC2 인스턴스 (서울 리전) │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ Caddy (HTTPS 현관) │ │
|
||||
│ │ ├→ React 프론트 │ │ ← 지금 이 화면!
|
||||
│ │ └→ Spring Boot 백엔드│ │
|
||||
│ │ PostgreSQL (Docker) │ │
|
||||
│ │ Gitea (Docker) │ │ ← edu.awesomedevapp.com 코드 저장소
|
||||
│ └─────────────────┘ │
|
||||
└───────────────────────┘
|
||||
|
||||
세 가지 부품의 역할:
|
||||
· EC2 = 건물 (컴퓨터 본체)
|
||||
· 탄력적 IP = 건물의 영구 도로명 주소
|
||||
· 보안 그룹 = 경비실 — "443호 손님만 통과, 나머지는 돌아가세요"`;
|
||||
|
||||
const CODE_CONSOLE_TOUR = `멘토와 함께하는 AWS 콘솔 견학 코스 (읽기 전용으로 구경!)
|
||||
|
||||
준비: 멘토가 화면을 공유하거나, 읽기 전용(ReadOnly) 권한 계정으로 접속
|
||||
— 수습 계정으로 생성·삭제 버튼은 누르지 않아요. 보는 것만!
|
||||
|
||||
견학 순서:
|
||||
1) 우측 상단 리전 확인 → "아시아 태평양(서울)"인지 보기
|
||||
(리전이 다르면 우리 서버가 '안 보여요' — 다른 도시 지도를 보는 셈)
|
||||
2) EC2 → 인스턴스 목록 → 우리 서버 찾기
|
||||
· 인스턴스 유형 읽어 보기 (섹션 3에서 배운 해부법으로!)
|
||||
· 상태가 '실행 중(running)'인지
|
||||
3) 같은 화면에서 퍼블릭 IP 확인 → 탄력적 IP 메뉴와 대조
|
||||
4) 보안 그룹 → 인바운드 규칙 → 열려 있는 포트 확인
|
||||
(443은 전체 공개, 22(SSH)는 왜 특정 IP만 허용일까? 멘토에게 질문!)
|
||||
5) 결제 대시보드는 멘토 화면으로만 — 이번 달 요금 항목 구경
|
||||
|
||||
견학 후 미션: 오늘 본 것을 섹션 6의 그림에 손으로 다시 그려 보기.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '클라우드의 탄생' },
|
||||
{ n: 2, label: 'IaaS·PaaS·SaaS' },
|
||||
{ n: 3, label: 'EC2 해부' },
|
||||
{ n: 4, label: '요금의 원리' },
|
||||
{ n: 5, label: 'S3와 RDS' },
|
||||
{ n: 6, label: '우리 인프라' },
|
||||
{ n: 7, label: '콘솔 견학' },
|
||||
];
|
||||
|
||||
export default function AwsIntroPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>클라우드와 AWS 입문</h1>
|
||||
<p>
|
||||
"남의 컴퓨터를 시간 단위로 빌린다"는 단순한 발상이 어떻게 IT 세상을
|
||||
뒤집었는지부터, 여러분이 지금 보고 있는 이 페이지가 실제로 살고 있는
|
||||
어썸데브의 AWS 서버 구성까지 따라가 봅니다. 개념마다 우리 인프라가
|
||||
그대로 교재예요 — 배운 것을 바로 우리 서버에서 확인합니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">직접 확인 실습 3개</span>
|
||||
<span className="chip">준비물: 내 PC + (섹션 7은 멘토)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title={'"남의 컴퓨터를 빌린다"는 혁명'} sub="발전기를 버리고 전기를 사 쓰게 된 날">
|
||||
<p>
|
||||
100년 전 공장들은 <strong>자기 발전기</strong>를 돌려 전기를 만들었어요. 발전기를
|
||||
사고, 기술자를 고용하고, 고장 나면 공장이 멈췄죠. 그러다 <strong>발전소</strong>가
|
||||
생기자 모두 발전기를 버리고 <strong>전기를 쓴 만큼만 사서 쓰기</strong> 시작했습니다.
|
||||
아무도 "우리 전기는 직접 만들어야 안심"이라고 하지 않게 됐어요.
|
||||
</p>
|
||||
<p>
|
||||
서버에서 똑같은 일이 벌어졌습니다. 예전엔 서비스를 만들려면 <strong>서버 컴퓨터를
|
||||
직접 사서</strong> 서버실에 두고, 에어컨을 틀고, 새벽에 고장 나면 달려가야 했어요.
|
||||
AWS(Amazon Web Services) 같은 <strong>클라우드</strong>는 거대한 데이터센터의
|
||||
컴퓨팅 파워를 <strong>수도·전기처럼 쓴 만큼 내고 빌려 쓰게</strong> 해 줬습니다.
|
||||
</p>
|
||||
<Code>{CODE_BEFORE_AFTER}</Code>
|
||||
<div className="tip">
|
||||
<b>왜 혁명일까?</b> 학생 몇 명도 카드 한 장으로 대기업급 인프라를 몇 분 만에
|
||||
빌릴 수 있게 됐다는 뜻이에요. 어썸데브 같은 회사가 서버실 없이 서비스를 운영하는
|
||||
것도, 여러분이 이 플랫폼에 접속하고 있는 것도 전부 이 혁명 덕분입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="IaaS · PaaS · SaaS" sub="어디까지 내가 하고, 어디부터 맡길까?">
|
||||
<p>
|
||||
클라우드라고 다 같은 게 아니에요. <strong>"어디까지 내가 직접 하느냐"</strong>에
|
||||
따라 3단계로 나눕니다. 피자로 비유하면 딱 감이 와요.
|
||||
</p>
|
||||
<Code>{CODE_IAAS_PAAS_SAAS}</Code>
|
||||
<p>
|
||||
위로 갈수록(IaaS 쪽) <strong>자유롭지만 손이 많이 가고</strong>, 아래로 갈수록(SaaS 쪽)
|
||||
<strong> 편하지만 정해진 대로만</strong> 써야 해요. 어썸데브가 IaaS(EC2)를 고른 건
|
||||
비용 때문이기도 하지만, 여러분이 <strong>OS부터 배포까지 전 과정을 눈으로 볼 수
|
||||
있는 교재</strong>가 되기 때문이기도 합니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 오늘 하루 내가 쓴 서비스를 세 칸에 분류해 보세요.
|
||||
학교 구글 메일은? (SaaS) 우리 Gitea(<span className="icode">edu.awesomedevapp.com</span>)는?
|
||||
(IaaS 위에 우리가 직접 올린 것!) 헷갈리는 게 나오면 그게 좋은 질문거리예요 —
|
||||
멘토 채널에 올려 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="EC2 — 클릭으로 만드는 가상 서버" sub="t3.medium이라는 암호 해독하기">
|
||||
<p>
|
||||
<strong>EC2</strong>(Elastic Compute Cloud)는 AWS의 <strong>가상 서버</strong> 서비스예요.
|
||||
"가상"인 이유는, 진짜 물리 컴퓨터 한 대를 통째로 받는 게 아니라 아마존 데이터센터의
|
||||
초대형 컴퓨터를 <strong>여러 칸으로 나눈 것 중 한 칸</strong>을 빌리기 때문이에요.
|
||||
한 층을 여러 사무실로 쪼개 임대하는 것과 같죠. 빌린 서버 한 대를
|
||||
<strong> 인스턴스</strong>라고 부릅니다.
|
||||
</p>
|
||||
<p>
|
||||
인스턴스는 사양별로 종류가 수백 가지인데, 이름만 읽을 줄 알면 사양이 보여요.
|
||||
자동차 모델명(아반떼 1.6이면 차종과 배기량이 보이듯)과 똑같습니다.
|
||||
</p>
|
||||
<Code>{CODE_T3_MEDIUM}</Code>
|
||||
<div className="warn">
|
||||
<b>t 패밀리의 "버스트"란?</b> 평소엔 CPU를 조금만 쓰다가, 바쁠 때 잠깐
|
||||
<strong> 전력 질주</strong>할 수 있는 방식이에요. 대신 질주할 수 있는 "크레딧"이
|
||||
쌓여 있어야 해요 — 계속 전력 질주만 시키면 크레딧이 바닥나서 느려집니다.
|
||||
평소 한산하고 가끔 바쁜 우리 학습 플랫폼 같은 서비스에 딱 맞는 가성비형이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="요금 — 끄면 안 나가는 것, 꺼도 나가는 것" sub="시간당 과금과 프리 티어, 그리고 함정 하나">
|
||||
<p>
|
||||
EC2 요금의 기본은 <strong>시간당 과금</strong>이에요. 인스턴스가 켜져 있는 시간만큼
|
||||
냅니다. 그래서 "실험이 끝나면 끈다"가 클라우드의 절약 제1원칙이죠. 그런데
|
||||
여기에 초보가 꼭 밟는 함정이 하나 있어요 — <strong>꺼도 나가는 요금</strong>이
|
||||
있다는 것.
|
||||
</p>
|
||||
<Code>{CODE_PRICING}</Code>
|
||||
<p>
|
||||
서버(EC2)는 <strong>전기</strong>처럼 쓴 시간만큼, 디스크(EBS)는 <strong>창고</strong>처럼
|
||||
물건을 보관하는 동안 계속 요금이 나간다고 기억하면 헷갈리지 않아요. 전등은 끄면
|
||||
전기세가 멈추지만, 창고에 짐을 둔 채로는 보관료가 계속 나가잖아요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>회사 계정에서는 절대 혼자 실험 금지!</b> 프리 티어 체험은 <strong>본인 개인
|
||||
계정</strong>에서, 그것도 멘토와 한도를 확인한 뒤에 하세요. 회사 AWS 계정의
|
||||
리소스를 만들거나 끄는 건 수습 권한 밖의 일이에요 — 섹션 7의 견학은 그래서
|
||||
"읽기 전용"입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="S3와 RDS — 한 줄씩만 미리 인사" sub="파일 창고와 관리형 DB, 이름만 알아 두기">
|
||||
<p>
|
||||
AWS에는 서비스가 200개가 넘지만, 신입이 이름을 알아 둘 것은 몇 개 안 돼요.
|
||||
EC2 다음으로 자주 듣게 될 두 가지만 한 줄씩 인사해 둡시다.
|
||||
</p>
|
||||
<Code>{CODE_S3_RDS}</Code>
|
||||
<p>
|
||||
눈여겨볼 점은 마지막 줄이에요. 어썸데브는 PostgreSQL을 RDS로 맡기지 않고
|
||||
<strong> EC2 위 Docker 컨테이너</strong>로 직접 돌립니다. 관리형 서비스는 편하지만
|
||||
비용이 더 들고 내부가 안 보여요. 지금 우리 규모에서는 직접 운영이 더 싸고,
|
||||
여러분에게는 <strong>DB가 어떻게 뜨고 백업되는지 눈으로 보는 교재</strong>가 되죠.
|
||||
규모가 커지면 그때 RDS를 검토하는 것 — 이런 판단이 바로 인프라 설계입니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="어썸데브 인프라 실례" sub="EC2 + 탄력적 IP + 보안 그룹 — 이 페이지가 사는 집">
|
||||
<p>
|
||||
이제 배운 조각을 전부 모아 <strong>실물</strong>을 봅시다. 여러분이 지금 보고 있는
|
||||
이 페이지는 <strong>서울 리전의 EC2 인스턴스</strong> 안에서 살고 있어요.
|
||||
리전(region)은 AWS 데이터센터가 모여 있는 지역 — 사용자와 가까울수록 빠르기
|
||||
때문에 한국 서비스는 서울 리전을 씁니다.
|
||||
</p>
|
||||
<Code>{CODE_OUR_INFRA}</Code>
|
||||
<p>
|
||||
<strong>탄력적 IP</strong>가 왜 필요할까요? EC2는 재시작하면 IP가 <strong>바뀔 수
|
||||
있어요</strong>. 이사 갈 때마다 주소가 바뀌면 아무도 못 찾아오죠. 탄력적 IP는
|
||||
"영구 도로명 주소"를 하나 받아 서버에 붙여 두는 것 — 그래서 도메인(DNS)이
|
||||
늘 같은 곳을 가리킬 수 있습니다. <strong>보안 그룹</strong>은 경비실이에요.
|
||||
허락한 포트로 오는 손님만 통과시키고 나머지는 전부 돌려보냅니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 네트워크 코스에서 배운 명령으로 우리 서버의 "도로명
|
||||
주소"를 알아내 보세요. 터미널에서{' '}
|
||||
<span className="icode">nslookup edu.awesomedevapp.com</span> — 나온 IP가 바로
|
||||
그림 맨 위의 <strong>탄력적 IP</strong>예요. 그리고 브라우저 주소창에서 이 플랫폼
|
||||
주소의 자물쇠 아이콘을 눌러 보세요. 그 HTTPS를 처리하는 게 그림 속{' '}
|
||||
<strong>Caddy</strong>입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="콘솔 구경은 멘토와 함께" sub="읽기 전용 견학 — 보는 건 자유, 누르는 건 금지">
|
||||
<p>
|
||||
AWS <strong>콘솔</strong>은 인스턴스를 만들고 끄고 지우는 관제탑이에요. 그만큼
|
||||
클릭 한 번이 <strong>서비스 중단이나 과금</strong>으로 이어질 수 있어서, 수습 기간의
|
||||
콘솔 접근은 <strong>멘토 동행 + 읽기 전용</strong>이 원칙입니다. 운전면허 따기 전에
|
||||
조수석에서 도로를 익히는 단계라고 생각하세요.
|
||||
</p>
|
||||
<Code>{CODE_CONSOLE_TOUR}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기(멘토와)</b> 위 견학 코스를 멘토와 함께 돌면서, 섹션 3에서 배운
|
||||
해부법으로 <strong>우리 인스턴스의 유형을 소리 내어 읽어 보세요.</strong> 패밀리는
|
||||
뭐고, 몇 세대고, 크기는? 그리고 보안 그룹 인바운드 규칙에서{' '}
|
||||
<span className="icode">22</span>번 포트가 왜 특정 IP에만 열려 있는지 멘토에게
|
||||
설명을 들어 보세요 — 보안 코스의 예고편입니다.
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>권한 안내</b> 수습 계정으로는 리소스 생성·수정·삭제가 막혀 있어요(막혀 있어야
|
||||
정상이에요!). "권한이 없습니다" 메시지는 오류가 아니라 <strong>안전벨트</strong>입니다.
|
||||
더 보고 싶은 화면이 있으면 멘토에게 요청하세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>☁️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 "클라우드"가 마법의 단어가 아니라 <strong>시간 단위로 빌리는 남의
|
||||
컴퓨터</strong>로, <span className="icode">t3.medium</span>이 암호가 아니라
|
||||
<strong> 읽을 수 있는 사양표</strong>로 보일 거예요. 우리 서비스가 EC2 + 탄력적 IP +
|
||||
보안 그룹 위에서 어떻게 사는지도 그릴 수 있고요. 다음은 그 서버 안에서 앱을
|
||||
포장해 나르는 기술 — <Link to="/learn/docker"><strong>Docker 입문</strong></Link>{' '}
|
||||
코스로 이어 가세요. 데이터가 서버까지 오는 길이 궁금하다면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link>를 먼저 복습해도
|
||||
좋습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
417
frontend/src/pages/courses/BinaryPage.jsx
Normal file
417
frontend/src/pages/courses/BinaryPage.jsx
Normal file
@ -0,0 +1,417 @@
|
||||
// 이 파일이 하는 일: "2진수와 데이터 표현" 코스 — 전기 스위치의 켜짐/꺼짐에서 출발해
|
||||
// 숫자·문자·색·이미지·소리가 전부 0과 1로 표현되는 원리까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (왜 0과 1인가 → 진법 변환 → 비트·바이트 단위 → 문자 인코딩 → 색 → 이미지·소리 → 계산기 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_SWITCH = `컴퓨터의 가장 작은 부품 = 전기 스위치(트랜지스터)
|
||||
|
||||
전기가 흐른다 → 1 (켜짐, ON)
|
||||
전기가 안 흐른다 → 0 (꺼짐, OFF)
|
||||
|
||||
"왜 하필 2개?" — 상태가 딱 2개면 헷갈릴 수가 없어서예요.
|
||||
전압이 조금 흔들려도 "켜짐인지 꺼짐인지"는 확실히 구분됩니다.
|
||||
|
||||
만약 0~9를 전압 10단계로 표현했다면?
|
||||
전압이 살짝만 흔들려도 6이 7로 읽히는 사고가 나요.
|
||||
→ 그래서 컴퓨터는 '확실한 둘'만 씁니다.
|
||||
|
||||
여러분 PC의 CPU 안에는 이런 스위치가 수십억 개 들어 있어요.
|
||||
컴퓨터가 하는 모든 일은 결국 이 스위치들을 켜고 끄는 것!`;
|
||||
|
||||
const CODE_BIN_TO_DEC = `2진수 → 10진수: 각 자리의 '값'을 더하면 끝!
|
||||
|
||||
10진수는 자리마다 1, 10, 100, 1000... (×10씩)
|
||||
2진수는 자리마다 1, 2, 4, 8, 16, 32, 64, 128... (×2씩)
|
||||
|
||||
예) 2진수 1011 은?
|
||||
|
||||
자리값: 8 4 2 1
|
||||
숫자 : 1 0 1 1
|
||||
─────────────────
|
||||
계산 : 8 + 0 + 2 + 1 = 11
|
||||
|
||||
→ 1이 켜진 자리의 값만 골라 더하면 됩니다.
|
||||
(스위치가 켜진 전구의 값만 더한다고 생각해 보세요)
|
||||
|
||||
연습) 1101 = ? 11111111 = ?
|
||||
답: 13 255 ← 이 255, 곧 또 만나요!`;
|
||||
|
||||
const CODE_DEC_TO_BIN = `10진수 → 2진수: 2로 계속 나누고, 나머지를 거꾸로 읽기
|
||||
|
||||
예) 13을 2진수로:
|
||||
|
||||
13 ÷ 2 = 6 나머지 1 ↑
|
||||
6 ÷ 2 = 3 나머지 0 │ 아래에서 위로
|
||||
3 ÷ 2 = 1 나머지 1 │ 거꾸로 읽는다
|
||||
1 ÷ 2 = 0 나머지 1 │
|
||||
│
|
||||
→ 1101 끝!
|
||||
|
||||
검산: 8 + 4 + 0 + 1 = 13 ✓
|
||||
|
||||
지름길 — '큰 자리값부터 빼기'도 있어요:
|
||||
13에서 8 빼기 가능? → 1, 남은 5에서 4 빼기 가능? → 1,
|
||||
남은 1에서 2 빼기 가능? → 0, 1 빼기 가능? → 1 ⇒ 1101`;
|
||||
|
||||
const CODE_UNITS = `비트에서 테라바이트까지 — 데이터의 단위
|
||||
|
||||
1 bit 0 아니면 1. 스위치 한 개.
|
||||
1 Byte = 8bit 스위치 8개 묶음. 영어 한 글자 크기.
|
||||
8칸이면 2⁸ = 256가지(0~255)를 표현!
|
||||
|
||||
1 KB = 1,024 Byte ← 1,000이 아니라 1,024?!
|
||||
1 MB = 1,024 KB 비밀: 2¹⁰ = 1,024
|
||||
1 GB = 1,024 MB 컴퓨터는 2의 거듭제곱으로 세는 게 편해서,
|
||||
1 TB = 1,024 GB 1,000에 가장 가까운 2¹⁰을 한 묶음으로 씁니다.
|
||||
|
||||
대략적인 감:
|
||||
카톡 메시지 한 줄 ≈ 수십 Byte
|
||||
사진 한 장 ≈ 2~5 MB
|
||||
영화 한 편 ≈ 1~4 GB
|
||||
요즘 노트북 SSD ≈ 256 GB ~ 1 TB
|
||||
|
||||
참고: 하드디스크 제조사는 1TB = 1,000⁴Byte로 계산해서 팔아요.
|
||||
그래서 1TB 디스크를 꽂으면 윈도우엔 약 931GB로 보입니다. (사기 아님!)`;
|
||||
|
||||
const CODE_ASCII = `문자는 어떻게 숫자가 되나 — "글자마다 번호표를 붙이자"
|
||||
|
||||
ASCII (1963년~) : 영어권에서 만든 128개짜리 번호표
|
||||
|
||||
'A' = 65 'B' = 66 'C' = 67 ...
|
||||
'a' = 97 'b' = 98 ...
|
||||
'0' = 48 (문자로서의 0! 숫자 0과 달라요)
|
||||
스페이스 = 32, 줄바꿈 = 10
|
||||
|
||||
즉 컴퓨터에 "AB"를 저장하면 실제로는:
|
||||
65, 66 → 01000001, 01000010 ← 이렇게 스위치가 켜집니다
|
||||
|
||||
문제: 128칸으로는 한글·한자·이모지를 못 담아요.
|
||||
→ 전 세계 모든 문자에 번호를 붙인 '유니코드'가 등장!
|
||||
'가' = 44032(U+AC00), '힣' = 55203, '😀' = 128512`;
|
||||
|
||||
const CODE_UTF8 = `UTF-8 — 유니코드 번호를 실제 바이트로 담는 포장법
|
||||
|
||||
글자마다 필요한 만큼만 바이트를 씁니다 (가변 길이):
|
||||
|
||||
영어 'A' → 1바이트 01000001
|
||||
한글 '가' → 3바이트 11101010 10110000 10000000
|
||||
이모지 😀 → 4바이트
|
||||
|
||||
그래서 같은 10글자라도 영어보다 한글 문장이 용량이 커요.
|
||||
|
||||
'글자 깨짐(뷁·)'의 정체:
|
||||
UTF-8로 포장한 데이터를 다른 규칙(EUC-KR 등)으로 풀면
|
||||
번호표가 엉뚱한 글자에 매칭 → 외계어가 됩니다.
|
||||
포장 규칙과 개봉 규칙이 같아야 해요!
|
||||
|
||||
우리 스택에서도: PostgreSQL 데이터베이스를 만들 때
|
||||
인코딩을 UTF8로 지정하는 이유가 바로 이것입니다.`;
|
||||
|
||||
const CODE_COLOR = `색은 숫자다 — #2159C5 해부하기
|
||||
|
||||
모니터의 픽셀 하나 = 빨강(R)·초록(G)·파랑(B) 꼬마전구 3개.
|
||||
각 전구의 밝기를 0~255(= 1바이트!)로 조절해 색을 섞어요.
|
||||
|
||||
#2159C5 를 두 글자씩 자르면:
|
||||
|
||||
# 21 59 C5
|
||||
│ │ └─ B(파랑) = C5(16진수) = 197 ← 많이!
|
||||
│ └──── G(초록) = 59 = 89 ← 조금
|
||||
└─────── R(빨강) = 21 = 33 ← 아주 조금
|
||||
|
||||
→ 파랑이 압도적 = 파란색 계열!
|
||||
|
||||
잠깐, 16진수(0~9, A~F)는 왜 쓰나요?
|
||||
2진수 4자리 = 딱 16진수 1글자. (1100 0101 = C5)
|
||||
긴 2진수를 사람이 읽기 좋게 접어 쓴 것뿐이에요.
|
||||
|
||||
극단값으로 감 잡기:
|
||||
#000000 = R0 G0 B0 → 전구 다 끔 = 검정
|
||||
#FFFFFF = 255 255 255 → 다 최대 = 하양
|
||||
#FF0000 = 빨강만 최대 = 빨강`;
|
||||
|
||||
const CODE_MEDIA = `이미지와 소리도 결국 숫자입니다
|
||||
|
||||
이미지 = 픽셀(색 숫자)의 격자
|
||||
1920×1080 사진 = 픽셀 2,073,600개
|
||||
픽셀당 RGB 3바이트 → 약 6MB
|
||||
"어? 사진은 2~3MB던데?" → JPEG 같은 압축이
|
||||
눈이 눈치 못 챌 정보를 줄여 주기 때문이에요.
|
||||
|
||||
소리 = 공기의 떨림(파형)을 초당 수만 번 '숫자로 채집'
|
||||
CD 음질: 1초에 44,100번, 매번 2바이트로 기록
|
||||
= 파형 곡선을 아주 촘촘한 점으로 받아 적기 (샘플링)
|
||||
점이 촘촘할수록 원래 곡선에 가깝고, 용량도 커집니다.
|
||||
|
||||
영상 = 이미지(프레임)를 초당 30~60장 + 소리
|
||||
→ 그래서 영상이 GB급으로 큰 거예요.
|
||||
|
||||
핵심: 사진·노래·영상·이 페이지의 글자까지, 저장되는 순간
|
||||
전부 0과 1의 긴 행렬입니다. 다르게 '해석'될 뿐!`;
|
||||
|
||||
const CODE_CALC = `Windows 계산기 = 공짜 진법 변환기
|
||||
|
||||
1) 시작 메뉴 → "계산기" 실행
|
||||
2) 왼쪽 위 ☰ 메뉴 → "프로그래머" 모드 선택
|
||||
3) 화면에 HEX / DEC / OCT / BIN 네 줄이 동시에 보여요
|
||||
|
||||
미션 A: DEC를 누르고 13 입력
|
||||
→ BIN 줄에 1101 이 뜨는지 확인 (섹션 2에서 손으로 푼 값!)
|
||||
|
||||
미션 B: DEC 255 입력
|
||||
→ BIN 1111 1111 (1바이트 꽉 참!), HEX FF 확인
|
||||
|
||||
미션 C: HEX를 누르고 21 입력 → DEC 33
|
||||
이어서 59 → 89, C5 → 197
|
||||
(섹션 5의 #2159C5 해부를 계산기로 검산한 것!)
|
||||
|
||||
미션 D: 아무 수나 넣고 BIN 표시의 자리별 켜짐/꺼짐을
|
||||
토글해 보세요 — DEC 값이 자리값만큼 변하는 게 보입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '왜 0과 1인가' },
|
||||
{ n: 2, label: '진법 변환 연습' },
|
||||
{ n: 3, label: '비트·바이트·단위' },
|
||||
{ n: 4, label: '문자 인코딩' },
|
||||
{ n: 5, label: '색은 숫자다' },
|
||||
{ n: 6, label: '이미지와 소리' },
|
||||
{ n: 7, label: '계산기 실습' },
|
||||
];
|
||||
|
||||
export default function BinaryPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>2진수와 데이터 표현<br />— 스위치 하나에서 사진 한 장까지</h1>
|
||||
<p>
|
||||
컴퓨터는 왜 0과 1밖에 모를까요? 그 둘만으로 어떻게 문자·색·사진·노래를 전부
|
||||
담아낼까요? 전기 스위치 하나에서 출발해, 지금 보고 있는 이 화면이 통째로
|
||||
숫자라는 걸 <strong>직접 계산하고 확인</strong>하면서 배웁니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">준비물: 계산기 앱 + 손가락</span>
|
||||
<span className="chip">실습 4개</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="왜 0과 1인가" sub="컴퓨터의 알파벳은 딱 두 글자">
|
||||
<p>
|
||||
컴퓨터를 아무리 뜯어 봐도 그 안에 숫자나 글자는 없어요. 있는 건
|
||||
<strong> 전기가 흐르거나(1), 안 흐르거나(0)</strong> 하는 아주 작은 스위치
|
||||
(트랜지스터) 수십억 개뿐입니다. 형광등 스위치를 상상해 보세요 — 켜짐과 꺼짐,
|
||||
중간은 없죠. 이 확실함이 핵심이에요.
|
||||
</p>
|
||||
<Code>{CODE_SWITCH}</Code>
|
||||
<p>
|
||||
글자 두 개뿐인 알파벳이라니 너무 가난해 보이지만, <strong>자리수를 늘리면</strong>
|
||||
{' '}표현력은 폭발합니다. 스위치 1개는 2가지, 2개는 4가지, 3개는 8가지...
|
||||
10개면 벌써 1,024가지예요. 모스 부호가 '삐'와 '삐—' 두 소리만으로 모든 문장을
|
||||
전보로 보냈던 것과 똑같은 원리입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 한 손을 펴 보세요. 손가락 5개를 스위치라 치고
|
||||
접힘=0, 펴짐=1이라면 몇 가지 상태를 만들 수 있을까요?
|
||||
2×2×2×2×2 = <strong>32가지</strong> — 손가락 다섯 개로 0부터 31까지 셀 수 있어요.
|
||||
양손이면 1,023까지! (섹션 2를 배우면 진짜로 세는 법을 알게 됩니다.)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="2진수 ↔ 10진수 변환 연습" sub="자리값만 알면 5분 만에 끝나는 기술">
|
||||
<p>
|
||||
우리가 쓰는 10진수는 자리가 하나 올라갈 때마다 값이 <strong>10배</strong>
|
||||
(1, 10, 100...)가 돼요. 2진수는 똑같은 규칙에서 숫자만 바꾼 것 —
|
||||
자리마다 <strong>2배</strong>(1, 2, 4, 8, 16...)입니다. 이 '자리값'만 기억하면
|
||||
변환은 덧셈 문제가 돼요.
|
||||
</p>
|
||||
<Code>{CODE_BIN_TO_DEC}</Code>
|
||||
<p>
|
||||
반대 방향(10진수 → 2진수)은 <strong>2로 나눈 나머지를 거꾸로 읽는</strong>
|
||||
{' '}기계적인 방법이 제일 안전합니다. 동전 교환기에 지폐를 넣으면 큰 동전부터
|
||||
차례로 바꿔 주듯, 수를 2의 덩어리로 분해하는 과정이에요.
|
||||
</p>
|
||||
<Code>{CODE_DEC_TO_BIN}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 종이에 자기 <strong>생일의 '일'</strong>(1~31)을 2진수로
|
||||
바꿔 보세요. 5자리 안에 반드시 들어갑니다(섹션 1의 손가락 5개 = 31까지!).
|
||||
다 풀었으면 손가락을 접고 펴서 그 2진수를 만들어 옆 사람에게 맞혀 보게 하세요.
|
||||
검산은 섹션 7의 계산기 실습에서 합니다.
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>흔한 실수</b> 자리값을 왼쪽부터 1, 2, 4... 로 매기면 틀려요.
|
||||
자리값은 항상 <strong>오른쪽 끝이 1</strong>이고 왼쪽으로 갈수록 2배씩 커집니다.
|
||||
10진수에서 일의 자리가 오른쪽 끝인 것과 같아요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="비트·바이트, 그리고 1024의 비밀" sub="KB, MB, GB — 매일 보던 단위의 정체">
|
||||
<p>
|
||||
스위치 하나가 <strong>비트(bit)</strong>, 비트 8개 묶음이 <strong>바이트(Byte)</strong>예요.
|
||||
왜 하필 8개냐면, 8비트면 2⁸ = <strong>256가지</strong>를 표현할 수 있어서 영어 알파벳·숫자·기호를
|
||||
담기에 딱 알맞았거든요. 계란 한 판처럼, 바이트는 데이터를 세는 기본 묶음이 됐습니다.
|
||||
</p>
|
||||
<Code>{CODE_UNITS}</Code>
|
||||
<p>
|
||||
핵심은 <strong>1KB = 1,024Byte</strong>라는 것. 1,000이 아니라 1,024인 이유는
|
||||
컴퓨터가 2진수 세상에 살기 때문이에요. 2를 10번 곱하면 1,024 — 1,000에 아주 가까운
|
||||
'2진수식 천 단위'라서 이걸 한 묶음으로 삼았습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 아무 폴더에서 파일 하나를 골라
|
||||
<span className="kbd">마우스 오른쪽 클릭</span> → <span className="kbd">속성</span>을 열어 보세요.
|
||||
"크기: 2.31MB (2,428,517 바이트)"처럼 <strong>두 숫자가 같이</strong> 나와요.
|
||||
2,428,517 ÷ 1,024 ÷ 1,024 를 계산기로 눌러 보면 정말 2.31이 나옵니다 —
|
||||
1,000으로 나누면 안 맞아요!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="문자는 어떻게 숫자가 되나" sub="ASCII에서 유니코드, 그리고 한글 UTF-8까지">
|
||||
<p>
|
||||
스위치로 숫자를 만드는 건 알겠는데, 글자는요? 방법은 의외로 단순합니다 —
|
||||
<strong> 글자마다 번호표를 붙이는 것</strong>. 사물함에 이름 대신 번호를 매기듯,
|
||||
'A'는 65번, 'B'는 66번... 이렇게 전 세계가 같은 번호표를 쓰기로 약속한 게
|
||||
문자 인코딩이에요.
|
||||
</p>
|
||||
<Code>{CODE_ASCII}</Code>
|
||||
<p>
|
||||
한글은 어떨까요? '가'부터 '힣'까지 조합 가능한 글자가 <strong>11,172자</strong>나
|
||||
돼서 1바이트(256칸)로는 어림도 없어요. 그래서 유니코드 번호를
|
||||
<strong> 여러 바이트에 나눠 담는 포장법, UTF-8</strong>을 씁니다.
|
||||
</p>
|
||||
<Code>{CODE_UTF8}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 학습 플랫폼 아무 페이지에서
|
||||
<span className="kbd">F12</span> 개발자 도구 → <span className="kbd">Console</span> 탭을 열고
|
||||
<span className="icode">'가'.charCodeAt(0)</span> 을 입력해 보세요 —
|
||||
<strong>44032</strong>가 나오면 방금 유니코드 번호표를 직접 읽은 거예요.
|
||||
<span className="icode">'A'.charCodeAt(0)</span>(65),
|
||||
<span className="icode">String.fromCharCode(44032 + 1)</span>('각'!)도 해 보세요.
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>실무 연결</b> 백엔드(Spring Boot)와 데이터베이스(PostgreSQL)의 인코딩이 어긋나면
|
||||
한글이 <strong>???나 외계어로 깨져 저장</strong>됩니다. 신입 개발자가 가장 자주 만나는
|
||||
사고 중 하나라, "인코딩은 처음부터 끝까지 UTF-8로 통일"이 우리 회사의 기본 규칙이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="색은 숫자다 — #2159C5 해부" sub="주소창 옆 그 파란색의 정체">
|
||||
<p>
|
||||
모니터를 돋보기로 확대하면 픽셀 하나하나가 <strong>빨강·초록·파랑 꼬마전구 3개</strong>로
|
||||
돼 있어요. 물감과 반대로, 빛은 셋을 다 켜면 하양이 됩니다. 각 전구의 밝기를
|
||||
0~255로 정하면 — 어라, 0~255? 섹션 3에서 본 <strong>딱 1바이트</strong>네요!
|
||||
색 하나 = 3바이트라는 뜻입니다.
|
||||
</p>
|
||||
<Code>{CODE_COLOR}</Code>
|
||||
<p>
|
||||
<span className="icode">#2159C5</span> 같은 색 코드는 결국
|
||||
<strong> "빨강 33, 초록 89, 파랑 197만큼 켜라"</strong>는 전구 주문서예요.
|
||||
16진수는 새 숫자 체계가 아니라, 길어서 읽기 힘든 2진수를 4자리씩 접어 쓴
|
||||
속기법일 뿐이고요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> <span className="kbd">F12</span> 개발자 도구 →
|
||||
<span className="kbd">Elements</span> 탭에서 이 페이지의 아무 요소나 클릭하고,
|
||||
Styles 영역의 색상 네모를 눌러 보세요. 컬러 피커가 뜨면서
|
||||
<span className="icode">#RRGGBB</span>와 <span className="icode">rgb(r, g, b)</span> 값이
|
||||
같이 보여요. 슬라이더로 R만 쭉 올려 보세요 — 16진수 앞 두 글자만 변합니다!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="이미지와 소리가 숫자인 원리" sub="사진 = 색 숫자의 격자, 소리 = 파형 받아쓰기">
|
||||
<p>
|
||||
섹션 5에서 "색 하나 = 숫자 3개"를 배웠으니 이미지는 이제 쉬워요.
|
||||
사진은 그 색 숫자를 <strong>모눈종이처럼 가로세로로 빽빽하게 채운 격자</strong>일
|
||||
뿐입니다. 픽셀 아트를 아주아주 곱게 만든 게 사진인 셈이죠.
|
||||
</p>
|
||||
<Code>{CODE_MEDIA}</Code>
|
||||
<p>
|
||||
소리는 눈에 안 보여서 신기하게 느껴지지만, 원리는 <strong>속기사</strong>와 같아요.
|
||||
마이크로 들어온 공기의 떨림(파형)의 높이를 1초에 수만 번 재서 숫자로 받아 적는
|
||||
것 — 이게 <strong>샘플링</strong>입니다. 재생은 그 숫자대로 스피커를 다시 떨게
|
||||
하는 것이고요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 아무 사진이나 그림판(또는 이미지 뷰어)에서 열고
|
||||
<strong>확대를 끝까지</strong> 해 보세요. 매끈하던 사진이 네모난 색 칸(픽셀)들로
|
||||
깨져 보이기 시작합니다 — 그 네모 하나하나가 섹션 5의 3바이트짜리 숫자예요.
|
||||
사진 파일 속성에서 "사진 크기: 4032×3024"를 확인하고, 두 수를 곱해
|
||||
픽셀이 몇 개인지도 계산해 보세요. (천만 개가 넘어요!)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="계산기(프로그래머 모드) 실습" sub="지금까지 손으로 푼 걸 전부 기계로 검산">
|
||||
<p>
|
||||
Windows 계산기에는 개발자를 위한 <strong>프로그래머 모드</strong>가 숨어 있어요.
|
||||
10진수(DEC)·2진수(BIN)·16진수(HEX)를 <strong>동시에 보여주는 진법 변환기</strong>라서,
|
||||
이 코스에서 손으로 푼 모든 계산을 1초 만에 검산할 수 있습니다.
|
||||
</p>
|
||||
<Code>{CODE_CALC}</Code>
|
||||
<p>
|
||||
미션 D가 특히 재미있어요. BIN 표시줄의 0/1을 직접 클릭해 뒤집을 수 있는데,
|
||||
어떤 자리를 켜면 DEC가 정확히 그 자리값(1, 2, 4, 8...)만큼 늘어나는 게
|
||||
실시간으로 보입니다. 섹션 2에서 배운 자리값이 눈앞에서 움직이는 순간이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 섹션 2에서 종이로 변환했던 <strong>내 생일의 '일'</strong>을
|
||||
DEC에 넣고, BIN 결과가 내 답과 같은지 확인하세요. 그다음 보너스 —
|
||||
자기 <strong>태어난 연도</strong>(예: 2008)를 넣고 BIN이 몇 자리나 나오는지 세어 보세요.
|
||||
11자리! 2008이 2¹⁰(1,024)보다 크고 2¹¹(2,048)보다 작기 때문입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>💡 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 스위치의 켜짐/꺼짐이 어떻게 숫자가 되고, 그 숫자가 글자·색·사진·소리로
|
||||
<strong> 해석</strong>되는지 전부 알게 됐어요. 컴퓨터의 세계에서 데이터의 정체는 하나 —
|
||||
0과 1의 행렬이고, 다른 건 '읽는 규칙'뿐입니다. 다음은 이 데이터가 어떻게
|
||||
<strong> 이동</strong>하는지 배울 차례 — {' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서 방금 배운
|
||||
비트들이 랜선과 해저 케이블을 달리는 여행을 따라가 보세요. 배운 내용은
|
||||
멘토와의 주간 리뷰에서 "생일 2진수 변환"을 말로 설명하는 것으로 확인합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
445
frontend/src/pages/courses/CleanCodePage.jsx
Normal file
445
frontend/src/pages/courses/CleanCodePage.jsx
Normal file
@ -0,0 +1,445 @@
|
||||
// 이 파일이 하는 일: "클린 코드와 코드리뷰" 코스 — 왜 코드는 '읽기'가 먼저인지에서 출발해
|
||||
// 이름 짓기 → 함수 쪼개기 → 좋은 주석 → 중복 제거 → 매직넘버 추출 → 코드리뷰 예절 →
|
||||
// 직접 리팩터링 실습까지 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 코드 예제는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 코드 예제 · 표 상수들 ──
|
||||
|
||||
const CODE_READ_TIME = `개발자의 하루를 몰래 들여다보면...
|
||||
|
||||
코드를 "쓰는" 시간 ██ 약 1
|
||||
코드를 "읽는" 시간 ████████████████████ 약 10
|
||||
|
||||
새 기능 한 줄을 쓰기 위해:
|
||||
- 이 함수가 어디서 불리는지 읽고
|
||||
- 옆 파일의 비슷한 코드를 읽고
|
||||
- 어제의 내가 짠 코드를 읽고 (그리고 후회하고)
|
||||
- 동료의 PR을 읽는다
|
||||
|
||||
→ 그래서 "빨리 쓴 코드"보다 "빨리 읽히는 코드"가
|
||||
팀 전체의 속도를 결정합니다.`;
|
||||
|
||||
const CODE_NAMING_BAD = `// ❌ 3주 뒤의 나도 못 알아보는 코드
|
||||
const a = 86400;
|
||||
const tmp = list.filter((x) => x.s === 1);
|
||||
function data(d) { ... }
|
||||
|
||||
// 읽는 사람의 머릿속:
|
||||
// "a가 뭐지? tmp는 임시로 뭘 담은 거지?
|
||||
// s가 1이면 뭐가 어떻다는 거지? data는 데이터를 뭐 어쩌라는 거지?"`;
|
||||
|
||||
const CODE_NAMING_GOOD = `// ✅ 이름이 곧 설명인 코드
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
const activeMembers = members.filter((member) => member.status === ACTIVE);
|
||||
function formatEnrollmentDate(enrolledAt) { ... }
|
||||
|
||||
// 규칙 3줄 요약:
|
||||
// 1) 변수·상수 = 명사 activeMembers, courseList, maxRetryCount
|
||||
// 2) 함수 = 동사로 시작 fetchCourses(), validateEmail(), isCompleted()
|
||||
// 3) a, b, tmp, data, info 같은 '아무 이름' 금지
|
||||
// (반복문의 i, 좌표의 x·y처럼 관례가 굳은 것만 예외)`;
|
||||
|
||||
const CODE_ONE_JOB_BAD = `// ❌ 한 함수가 네 가지 일을 하는 중 — 이름도 못 짓겠죠?
|
||||
function handleSubmit() {
|
||||
// 1) 입력값 검사
|
||||
if (!name || name.length < 2) { setError('이름을 확인하세요'); return; }
|
||||
if (!email.includes('@')) { setError('이메일을 확인하세요'); return; }
|
||||
// 2) 서버에 보낼 모양으로 가공
|
||||
const payload = { name: name.trim(), email: email.toLowerCase() };
|
||||
// 3) 전송
|
||||
fetch('/api/members', { method: 'POST', body: JSON.stringify(payload) });
|
||||
// 4) 화면 뒷정리
|
||||
setName(''); setEmail(''); setError(null);
|
||||
}`;
|
||||
|
||||
const CODE_ONE_JOB_GOOD = `// ✅ 일 하나 = 함수 하나. 각자 이름표를 달아 주면
|
||||
// handleSubmit은 '목차'처럼 읽힙니다.
|
||||
function validateForm({ name, email }) { ... } // 검사만
|
||||
function buildMemberPayload({ name, email }) { ... } // 가공만
|
||||
function resetForm() { ... } // 뒷정리만
|
||||
|
||||
function handleSubmit() {
|
||||
const error = validateForm({ name, email });
|
||||
if (error) { setError(error); return; }
|
||||
const payload = buildMemberPayload({ name, email });
|
||||
fetch('/api/members', { method: 'POST', body: JSON.stringify(payload) });
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 판별법: 함수가 하는 일을 한 문장으로 말할 때
|
||||
// "~하고, ~하고, 그리고 ~한다"처럼 '그리고'가 나오면 쪼갤 신호!`;
|
||||
|
||||
const CODE_COMMENT = `// ❌ '무엇'을 반복하는 주석 — 코드만 봐도 아는 얘기
|
||||
// i를 1 증가시킨다
|
||||
i += 1;
|
||||
|
||||
// ✅ '왜'를 남기는 주석 — 코드만 봐서는 절대 모르는 얘기
|
||||
// 서버가 페이지 번호를 0이 아니라 1부터 세기 때문에 보정한다
|
||||
i += 1;
|
||||
|
||||
// ✅ 우리 플랫폼 실전 예 (지금 이 파일 맨 위를 보세요!)
|
||||
// "이 파일이 하는 일" 주석: 파일을 열자마자 지도부터 보여준다
|
||||
// "학습 포인트" 주석: 왜 이렇게 짰는지 의도를 남긴다
|
||||
|
||||
// 순서 정리:
|
||||
// 1순위 — 주석 없이도 읽히게 이름·구조를 고친다
|
||||
// 2순위 — 그래도 남는 '왜'(사정·제약·의도)만 주석으로 쓴다`;
|
||||
|
||||
const CODE_DRY = `// ❌ 복붙 3형제 — 버튼 스타일을 바꾸려면 세 군데를 고쳐야 해요
|
||||
// CourseCard.jsx 에도, NoticeCard.jsx 에도, QuizCard.jsx 에도
|
||||
// 거의 똑같은 카드 마크업이 통째로 들어 있는 상황
|
||||
|
||||
// ✅ 한 곳으로 모으기 — 이 페이지의 Section 컴포넌트가 실례!
|
||||
function Card({ title, badge, children }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>{title}</h3>
|
||||
{badge && <span className="chip">{badge}</span>}
|
||||
</div>
|
||||
<div className="card-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 이제 고칠 곳은 딱 한 곳. 이것이 DRY(Don't Repeat Yourself)!
|
||||
|
||||
// 주의: '우연히 닮은' 코드까지 억지로 합치면 오히려 독.
|
||||
// 기준 — 한쪽을 고칠 때 다른 쪽도 "반드시 같이" 고쳐야 하는가?`;
|
||||
|
||||
const CODE_MAGIC = `// ❌ 매직넘버 — 숫자가 마법처럼 뿅 나타나서 아무 설명이 없다
|
||||
if (progress >= 80) { unlockCertificate(); }
|
||||
setTimeout(retry, 3000);
|
||||
if (title.length > 50) { ... }
|
||||
|
||||
// 읽는 사람: "80이 뭔데? 왜 3000이지? 50은 누가 정했지?"
|
||||
// 더 무서운 건: 기준이 80→70으로 바뀔 때 '80'을 전부 찾아
|
||||
// 바꿔야 하는데, 관련 없는 80까지 섞여서 하나를 놓친다는 것.
|
||||
|
||||
// ✅ 이름 있는 상수로 추출 — 숫자에 이름표 달기
|
||||
const CERTIFICATE_UNLOCK_PERCENT = 80; // 수료 기준: 진도 80% (교육팀 확정)
|
||||
const RETRY_DELAY_MS = 3000;
|
||||
const MAX_TITLE_LENGTH = 50;
|
||||
|
||||
if (progress >= CERTIFICATE_UNLOCK_PERCENT) { unlockCertificate(); }
|
||||
|
||||
// 백엔드(Spring Boot)도 똑같아요:
|
||||
// application.yml 설정값이나 상수 클래스로 뽑아 두면
|
||||
// "정책이 코드 어딘가의 숫자"가 아니라 "이름 붙은 한 곳"이 됩니다.`;
|
||||
|
||||
const CODE_REVIEW_MANNER = `코드리뷰 대화 번역기 — 같은 지적, 다른 온도
|
||||
|
||||
❌ "이 코드 왜 이렇게 짰어요?"
|
||||
✅ "이 부분을 이렇게 바꾸면 중복이 줄 것 같은데 어떻게 생각하세요?"
|
||||
|
||||
❌ "이거 틀렸는데요."
|
||||
✅ "여기 email이 빈 문자열이면 통과되는 것 같아요.
|
||||
검증 조건을 하나 추가하면 어떨까요?"
|
||||
|
||||
❌ (리뷰 받고) "제 코드가 뭐가 어때서요?"
|
||||
✅ (리뷰 받고) "오, 그 경우는 생각 못 했네요. 고쳐서 다시 올릴게요.
|
||||
혹시 비슷한 패턴이 다른 파일에도 있으면 알려 주세요!"
|
||||
|
||||
기억할 것:
|
||||
- 리뷰의 과녁은 '코드'지 '사람'이 아니에요. (코드 지적 ≠ 사람 지적)
|
||||
- 칭찬도 리뷰다: 좋은 코드엔 "이 함수 분리 깔끔하네요" 한 줄을.
|
||||
- 작성자가 최종 결정권을 가진 게 아니라, '더 나은 코드'가 이깁니다.
|
||||
누가 맞느냐가 아니라 무엇이 맞느냐의 게임이에요.`;
|
||||
|
||||
const CODE_PRACTICE_BAD = `// ── 리팩터링 실습 재료 ──
|
||||
// 아래 코드는 '돌아는 가는' 코드예요. 하지만 이 코스에서 배운
|
||||
// 문제를 최소 5개 품고 있습니다. 직접 고쳐 보세요!
|
||||
|
||||
function calc(list) {
|
||||
let tmp = 0;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
// 진도율을 더한다
|
||||
if (list[i].p >= 80) {
|
||||
tmp = tmp + 1;
|
||||
}
|
||||
}
|
||||
let a = (tmp / list.length) * 100;
|
||||
if (a >= 60) {
|
||||
console.log('통과');
|
||||
document.title = '축하합니다!';
|
||||
return true;
|
||||
} else {
|
||||
console.log('미달');
|
||||
return false;
|
||||
}
|
||||
}`;
|
||||
|
||||
const CODE_PRACTICE_HINT = `찾아야 할 문제 (스스로 먼저 찾고 나서 펼쳐 보기!)
|
||||
|
||||
1) 이름: calc, tmp, a, p — 전부 '아무 이름' (섹션 2)
|
||||
2) 매직넘버: 80과 60이 맨몸으로 등장 (섹션 6)
|
||||
3) 한 가지 일 위반: 계산 + 콘솔 출력 + 화면(title) 변경을
|
||||
한 함수가 다 함 (섹션 3)
|
||||
4) 주석: "진도율을 더한다"는 코드 반복일 뿐, 심지어
|
||||
실제로 더하는 건 '기준을 넘긴 사람 수' — 거짓말 주석! (섹션 4)
|
||||
5) 덤: filter를 쓰면 for문 자체가 사라져요
|
||||
|
||||
모범 답안의 뼈대 (세부는 자유):
|
||||
const CERTIFICATE_PERCENT = 80;
|
||||
const CLASS_PASS_RATE = 60;
|
||||
|
||||
function getPassRate(students) { ... } // 계산만 해서 숫자 반환
|
||||
function isClassPassed(students) { ... } // 기준 비교만
|
||||
// 콘솔 출력·화면 변경은 함수 밖(호출하는 쪽)의 일!`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
// (섹션 5 '중복 제거'에서 이 컴포넌트 자체가 교재로 다시 등장해요!)
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '읽는 시간이 더 길다' },
|
||||
{ n: 2, label: '이름 짓기' },
|
||||
{ n: 3, label: '함수는 한 가지 일만' },
|
||||
{ n: 4, label: '주석은 "왜"를' },
|
||||
{ n: 5, label: '중복 제거' },
|
||||
{ n: 6, label: '매직넘버 추출' },
|
||||
{ n: 7, label: '코드리뷰 예절' },
|
||||
{ n: 8, label: '리팩터링 실습' },
|
||||
];
|
||||
|
||||
export default function CleanCodePage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 다루는 것 — 잘 '돌아가는' 코드에서 잘 '읽히는' 코드로 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 소프트웨어</div>
|
||||
<h1>클린 코드와 코드리뷰<br />— 동료가 읽을 코드를 쓰는 법</h1>
|
||||
<p>
|
||||
돌아가는 코드를 만들었다면 절반은 온 거예요. 나머지 절반은 <strong>3주 뒤의 나와
|
||||
옆자리 동료가 한 번에 읽을 수 있는 코드</strong>로 만드는 일이고, 이 코스가 그 방법과
|
||||
코드리뷰에서 주고받는 예절까지 다룹니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 리팩터링 과제</span>
|
||||
<span className="chip">연결 과제: FE-07 · BE-05</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="코드는 쓰는 시간보다 읽는 시간이 길다" sub="클린 코드가 필요한 진짜 이유">
|
||||
<p>
|
||||
코딩을 처음 배울 땐 코드를 <strong>쓰는</strong> 일이라고 생각하기 쉬워요. 그런데
|
||||
실제 개발자의 시간을 재 보면 정반대입니다 — 한 줄을 쓰기 위해 열 줄을 읽어요.
|
||||
내 코드, 동료 코드, 어제의 내 코드까지.
|
||||
</p>
|
||||
<Code>{CODE_READ_TIME}</Code>
|
||||
<p>
|
||||
그래서 코드는 <strong>일기</strong>가 아니라 <strong>편지</strong>예요. 일기는 나만
|
||||
알아보면 되지만, 편지는 받는 사람이 읽고 이해해야 완성이죠. 여러분의 코드를 받는
|
||||
사람은 팀 동료, 리뷰어, 그리고 이 코드를 다 잊어버린 <strong>3주 뒤의 여러분
|
||||
자신</strong>입니다. AWESOMEDEV의 코드도 여러 사람이 몇 년을 이어 다듬는 편지예요 —
|
||||
지금 여러분이 보는 이 학습 플랫폼 코드도 마찬가지고요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>미리 알려드리면</b> 이 코스의 규칙들은 전부 한 문장으로 요약돼요 —
|
||||
<strong> "읽는 사람의 물음표를 줄여라."</strong> 이름 짓기부터 코드리뷰 매너까지
|
||||
모두 이 문장의 각론입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="이름 짓기 — 변수는 명사, 함수는 동사" sub="a, tmp, data는 이름이 아니라 물음표">
|
||||
<p>
|
||||
클린 코드의 절반은 <strong>이름</strong>이에요. 이름이 좋으면 주석 없이도 읽히고,
|
||||
이름이 나쁘면 그 코드를 읽는 모든 사람이 매번 추리를 해야 합니다. 물건마다
|
||||
이름표 대신 "그거", "저거"라고 써 붙인 창고를 상상해 보세요 — 물건을 찾을 때마다
|
||||
상자를 전부 열어 봐야겠죠.
|
||||
</p>
|
||||
<Code>{CODE_NAMING_BAD}</Code>
|
||||
<Code>{CODE_NAMING_GOOD}</Code>
|
||||
<p>
|
||||
이름이 길어지는 걸 겁내지 마세요. <span className="icode">activeMembers</span>가
|
||||
<span className="icode">am</span>보다 타자는 열두 배 느려도, 읽기는 백 배 빨라요.
|
||||
그리고 요즘 에디터는 자동완성이 다 해 줍니다. 이름 짓다 5분 고민하는 건 시간 낭비가
|
||||
아니라 <strong>설계</strong>예요 — 이름이 안 지어지는 코드는 대개 역할이 뒤죽박죽인
|
||||
코드거든요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 내가 최근에 작성한 코드(과제·개인 프로젝트 아무거나)를 열고
|
||||
<span className="icode"> a</span>, <span className="icode">tmp</span>,
|
||||
<span className="icode"> data</span>, <span className="icode">list</span> 같은 이름을
|
||||
찾아 보세요. 에디터에서 <span className="kbd">F2</span>(이름 바꾸기)를 누르면 그 변수를
|
||||
쓰는 모든 곳이 한 번에 바뀝니다 — 세 개만 골라 진짜 이름을 지어 주세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="함수는 한 가지 일만" sub='"그리고"가 나오면 쪼갤 신호'>
|
||||
<p>
|
||||
함수는 <strong>일의 단위</strong>예요. 한 함수가 검사도 하고, 가공도 하고, 전송도
|
||||
하고, 뒷정리도 하면 — 주방에서 한 사람이 주문받고 요리하고 서빙하고 설거지까지 하는
|
||||
식당과 같아요. 바쁠 때 뭐 하나가 꼭 사고 나고, 새 직원(동료)에게 일을 나눠 줄 수도 없죠.
|
||||
</p>
|
||||
<Code>{CODE_ONE_JOB_BAD}</Code>
|
||||
<Code>{CODE_ONE_JOB_GOOD}</Code>
|
||||
<p>
|
||||
쪼개고 나면 좋은 점이 연쇄로 따라와요. <strong>이름 짓기가 쉬워지고</strong>(일이
|
||||
하나니까), <strong>테스트하기 쉬워지고</strong>(입력→출력만 보면 되니까),
|
||||
<strong> 재사용할 수 있고</strong>(<span className="icode">validateForm</span>은 다른
|
||||
화면에서도 쓰겠죠), 버그가 나도 <strong>범인 후보가 좁혀집니다</strong>.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 오해</b> "함수는 무조건 5줄 이하!" 같은 줄 수 규칙이 핵심이 아니에요.
|
||||
기준은 줄 수가 아니라 <strong>"이 함수가 하는 일을 '그리고' 없이 한 문장으로 말할 수
|
||||
있는가"</strong>입니다. 한 가지 일을 하는 20줄 함수는 괜찮아요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title='주석은 "무엇"이 아니라 "왜"를' sub="코드가 말 못 하는 사정만 적는다">
|
||||
<p>
|
||||
주석을 많이 달수록 좋은 코드라고 생각하기 쉬운데, 절반만 맞아요. 코드를 그대로
|
||||
한국어로 번역한 주석은 소음이고, 심지어 코드를 고친 뒤 주석을 안 고치면
|
||||
<strong> 거짓말하는 주석</strong>이 됩니다. 좋은 주석은 코드가 스스로 말할 수 없는
|
||||
것 — <strong>왜 이렇게 했는지</strong>(의도·사정·제약)만 말해요.
|
||||
</p>
|
||||
<Code>{CODE_COMMENT}</Code>
|
||||
<p>
|
||||
가장 가까운 실례가 <strong>지금 이 파일</strong>이에요. 이 페이지 소스 맨 위엔
|
||||
"이 파일이 하는 일" 주석이, 중간엔 "학습 포인트" 주석이 있어요. 둘 다 코드 자체를
|
||||
번역한 게 아니라 <strong>왜 이렇게 만들었는지</strong>를 남긴 주석입니다. 우리 플랫폼의
|
||||
모든 페이지가 이 규칙을 따르고 있어요 — 여러분이 코드를 열 때마다 지도를 먼저 받도록요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> Gitea(<span className="icode">edu.awesomedevapp.com</span>)에서
|
||||
이 플랫폼 저장소를 열고 <span className="icode">frontend/src/pages</span> 폴더의 파일을
|
||||
아무거나 3개 골라 맨 위 주석만 읽어 보세요. 코드를 한 줄도 안 읽고도 각 파일의 역할을
|
||||
말할 수 있게 되면 — 그게 바로 "왜" 주석의 힘이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="중복 제거 — 같은 코드가 두 번 보이면 의심" sub="고칠 곳이 한 곳이어야 안 틀린다">
|
||||
<p>
|
||||
복사-붙여넣기는 달콤하지만 이자가 붙는 빚이에요. 같은 코드가 세 군데 있으면, 나중에
|
||||
그 로직을 고칠 때 <strong>세 군데를 전부 기억해서</strong> 고쳐야 하고 — 사람은 꼭
|
||||
한 곳을 잊습니다. 그 잊은 한 곳이 버그가 되죠. 학교 게시판 세 곳에 같은 공지를 붙였는데
|
||||
시간이 바뀌어 두 곳만 고친 상황, 상상되시죠?
|
||||
</p>
|
||||
<Code>{CODE_DRY}</Code>
|
||||
<p>
|
||||
이 페이지의 <span className="icode">Section</span> 컴포넌트가 살아 있는 예예요.
|
||||
8개 섹션의 겉모양(번호·제목·부제 틀)이 전부 똑같아서 컴포넌트 하나로 뽑았고,
|
||||
덕분에 섹션 디자인을 바꿀 일이 생기면 <strong>딱 한 곳</strong>만 고치면 됩니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>과제로 연결</b> 지금 우리 플랫폼 프론트엔드에 카드 마크업이 여러 페이지에 복붙된
|
||||
곳이 실제로 있어요. <strong>FE-07 티켓(중복 카드 컴포넌트 추출)</strong>이 바로 그걸
|
||||
공용 컴포넌트로 뽑는 과제입니다 — 이 섹션을 이해했다면 이미 절반은 푼 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="매직넘버 추출 — 숫자에 이름표 달기" sub="80이 뭔데? 를 없애는 법">
|
||||
<p>
|
||||
코드 한복판에 맨몸으로 등장하는 숫자를 <strong>매직넘버</strong>라고 불러요.
|
||||
작성자에겐 당연한 숫자지만 읽는 사람에겐 마술처럼 뿅 나타난 수수께끼거든요.
|
||||
그 숫자가 <strong>정책</strong>(수료 기준, 제한 길이, 재시도 간격...)이라면
|
||||
반드시 이름 있는 상수로 뽑아야 합니다.
|
||||
</p>
|
||||
<Code>{CODE_MAGIC}</Code>
|
||||
<p>
|
||||
상수로 뽑으면 세 가지가 공짜로 따라와요. <strong>읽기</strong> — 이름이 곧 설명.
|
||||
<strong> 고치기</strong> — 정책이 바뀌면 한 곳만 수정. <strong>찾기</strong> —
|
||||
"수료 기준이 어디 있지?" 할 때 상수 이름으로 검색하면 끝.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>과제로 연결</b> 우리 백엔드(Spring Boot)에도 맨몸 숫자들이 숨어 있어요.
|
||||
<strong> BE-05 티켓(매직넘버 상수 추출)</strong>이 그걸 찾아 이름을 지어 주는
|
||||
과제입니다. 힌트: 페이지 크기, 토큰 만료 시간 근처를 뒤져 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="코드리뷰 — 받는 자세, 하는 매너" sub="과녁은 코드지 사람이 아니다">
|
||||
<p>
|
||||
AWESOMEDEV에서 여러분의 코드는 <strong>코드리뷰</strong>를 거쳐 합쳐져요. 처음 리뷰를
|
||||
받으면 내 코드에 달린 빨간 댓글이 꼭 나에 대한 지적처럼 느껴지는데 — 여기서 제일 중요한
|
||||
문장을 드릴게요. <strong>코드 지적은 사람 지적이 아닙니다.</strong> 리뷰는 시험 채점이
|
||||
아니라, 두 사람이 한 코드를 같이 다듬는 <strong>합주 연습</strong>이에요. 틀린 음을
|
||||
짚어 주는 건 연주자를 미워해서가 아니라 무대를 잘 만들고 싶어서죠.
|
||||
</p>
|
||||
<Code>{CODE_REVIEW_MANNER}</Code>
|
||||
<ol className="olist">
|
||||
<li><strong>받을 때</strong> — 방어하지 말고 먼저 이해해요. "왜 이렇게 봤을까?"가 첫 질문. 이해가 안 되면 되묻는 게 예의예요(추측으로 고치는 게 무례).</li>
|
||||
<li><strong>받을 때</strong> — 리뷰어가 놓친 맥락이 있으면 감정 빼고 설명해요. "이렇게 한 이유는 ~때문인데, 그래도 바꾸는 게 나을까요?"</li>
|
||||
<li><strong>할 때</strong> — 명령 대신 질문·제안으로. "바꾸세요" 대신 "이렇게 하면 어떨까요?" 근거(왜)를 꼭 붙이고요.</li>
|
||||
<li><strong>할 때</strong> — 좋은 부분엔 칭찬 댓글을. 리뷰가 '혼나는 시간'이 아니라 '배우는 시간'이 되는 비결이에요.</li>
|
||||
<li><strong>둘 다</strong> — 취향과 기준을 구분해요. 동작이 틀린 건 고쳐야 하지만, 스타일 취향이 갈리면 팀 규칙을 따르거나 규칙을 새로 정하면 됩니다.</li>
|
||||
</ol>
|
||||
<div className="warn">
|
||||
<b>이것만은 금지</b> 리뷰 코멘트에 사람을 넣지 마세요. "이걸 왜 몰라요?",
|
||||
"기본인데요" 같은 말은 코드를 한 글자도 낫게 만들지 못하고 팀만 다치게 해요.
|
||||
반대로, 리뷰를 받았는데 아무 반응 없이 잠수하는 것도 매너 위반 — 모든 코멘트엔
|
||||
수정이든 답변이든 반응을 남기는 게 원칙입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="실습 — 나쁜 코드를 직접 리팩터링" sub="배운 것 전부를 한 코드에 적용하기">
|
||||
<p>
|
||||
이제 배운 걸 손으로 확인할 시간이에요. 아래 코드는 멀쩡히 돌아가지만, 이 코스에서
|
||||
배운 문제를 <strong>최소 5개</strong> 품고 있습니다. 에디터에 붙여 넣고 직접
|
||||
고쳐 보세요 — 힌트를 먼저 펼치면 반칙!
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE_BAD}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> ① 위 코드를 내 에디터에 붙여 넣고 문제 5개를 주석으로 표시한 뒤
|
||||
직접 리팩터링해 보세요. ② 다 고쳤다면 Gitea에 브랜치를 만들어 올리고 <strong>옆자리
|
||||
동료와 서로 코드리뷰를 주고받아 보세요</strong> — 섹션 7의 매너를 지켜서 코멘트를
|
||||
최소 2개씩! 같은 코드를 고쳤는데 서로 다른 답이 나오는 것까지가 이 실습의 목적이에요.
|
||||
</div>
|
||||
<Code>{CODE_PRACTICE_HINT}</Code>
|
||||
<div className="warn">
|
||||
<b>리팩터링의 철칙</b> 고치기 전과 후의 <strong>동작이 같아야</strong> 리팩터링이에요.
|
||||
동작을 바꾸고 싶은 유혹이 들면 참으세요 — 그건 리팩터링이 아니라 기능 변경이고,
|
||||
따로 나눠서(별도 커밋으로) 해야 리뷰어가 안 헷갈립니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🧹 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 <strong>읽는 사람의 물음표를 줄이는 법</strong>을 알아요 — 이름은
|
||||
명사와 동사로, 함수는 한 가지 일만, 주석은 "왜"만, 중복과 매직넘버는 한 곳으로,
|
||||
그리고 리뷰의 과녁은 언제나 코드라는 것까지. 배운 걸 바로 써먹을 곳도 준비돼
|
||||
있습니다 — <strong>FE-07(중복 카드 컴포넌트 추출)</strong>과
|
||||
<strong> BE-05(매직넘버 상수 추출)</strong> 티켓이 여러분을 기다려요. 코드를
|
||||
올리기 전에 <Link to="/learn/git"><strong>Git과 협업 워크플로</strong></Link> 코스에서
|
||||
브랜치·PR 만드는 법을 먼저 익히면 리뷰 실습까지 한 번에 이어집니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
389
frontend/src/pages/courses/CloudNetworkPage.jsx
Normal file
389
frontend/src/pages/courses/CloudNetworkPage.jsx
Normal file
@ -0,0 +1,389 @@
|
||||
// 이 파일이 하는 일: "클라우드 네트워크 입문" 코스 — 데이터센터·리전에서 출발해
|
||||
// VPC·서브넷·탄력적 IP·로드밸런서를 거쳐, 우리 플랫폼의 실제 네트워크 구성도와
|
||||
// CDN까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (리전/가용영역 → VPC → 서브넷 → 탄력적 IP → 로드밸런서 → 우리 구성도 총정리 → CDN)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_REGION_AZ = `AWS의 세계 지도 — 리전(Region)과 가용영역(AZ)
|
||||
|
||||
리전 = 도시 단위의 '데이터센터 캠퍼스' 묶음
|
||||
가용영역(AZ) = 그 안의 독립된 건물(데이터센터) 그룹
|
||||
|
||||
ap-northeast-2 (서울 리전) ← 우리 서버가 사는 곳!
|
||||
├── ap-northeast-2a ┐
|
||||
├── ap-northeast-2b │ 서로 떨어진 별개 건물들.
|
||||
├── ap-northeast-2c │ 한 곳이 정전·화재가 나도
|
||||
└── ap-northeast-2d ┘ 나머지는 멀쩡하도록 설계
|
||||
|
||||
다른 리전 예) us-east-1(미국 버지니아), ap-northeast-1(도쿄)
|
||||
|
||||
→ 서울 리전을 고른 이유: 사용자가 한국에 있으니
|
||||
물리적으로 가까울수록 응답이 빨라요 (빛도 이동 시간이 걸립니다!)`;
|
||||
|
||||
const CODE_VPC = `VPC(Virtual Private Cloud) = 클라우드 속에 그린 '내 네트워크'
|
||||
|
||||
AWS 데이터센터라는 거대한 아파트 단지 안에,
|
||||
울타리를 치고 "여기부터는 어썸데브 땅"이라고 선언한 구역이에요.
|
||||
|
||||
┌─ AWS 서울 리전 ──────────────────────────────┐
|
||||
│ │
|
||||
│ ┌─ 우리 회사 VPC (10.0.0.0/16) ──────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 울타리 안에서는 우리가 정한 규칙대로 │ │
|
||||
│ │ IP를 나눠 쓰고, 문(게이트웨이)을 통해 │ │
|
||||
│ │ 서만 바깥과 통해요 │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ (다른 회사들의 VPC — 서로 절대 안 보임) │
|
||||
└──────────────────────────────────────────────┘
|
||||
|
||||
10.0.0.0/16 → "10.0.으로 시작하는 주소 65,536개를 내 땅으로 쓸게"
|
||||
지난 코스의 공유기(192.168.x.x)가 만든 집 네트워크의 회사 버전이라
|
||||
생각하면 딱 맞아요.`;
|
||||
|
||||
const CODE_SUBNET = `서브넷 = VPC라는 땅을 용도별로 나눈 '구역'
|
||||
|
||||
┌─ VPC (10.0.0.0/16) ─────────────────────────────┐
|
||||
│ │
|
||||
│ ┌─ 퍼블릭 서브넷 (10.0.1.0/24) ─────────────┐ │
|
||||
│ │ 가게가 늘어선 '대로변' │ │
|
||||
│ │ 인터넷 게이트웨이(정문)와 연결됨 │ │
|
||||
│ │ 예) 웹 서버, Caddy — 손님을 맞아야 하는 것 │ │
|
||||
│ └───────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ 프라이빗 서브넷 (10.0.2.0/24) ───────────┐ │
|
||||
│ │ 외부인 출입 금지 '직원 전용 구역' │ │
|
||||
│ │ 인터넷에서 직접 못 들어옴 │ │
|
||||
│ │ 예) 데이터베이스 — 절대 노출되면 안 되는 것 │ │
|
||||
│ └───────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
핵심 원칙: 손님(인터넷)을 맞을 것만 대로변에,
|
||||
금고(DB)는 안쪽 방에. 은행 창구와 금고실의 관계예요.`;
|
||||
|
||||
const CODE_EIP = `탄력적 IP(Elastic IP) = 서버에 붙이는 '평생 전화번호'
|
||||
|
||||
문제 상황:
|
||||
EC2 서버는 껐다 켜면 공인 IP가 바뀔 수 있어요.
|
||||
→ 도메인이 가리키던 주소가 어긋남 → 사이트 먹통!
|
||||
|
||||
해결:
|
||||
탄력적 IP를 하나 빌려서 서버에 '고정'으로 붙여요.
|
||||
서버를 재부팅해도, 심지어 새 서버로 갈아타도
|
||||
IP를 떼서 새 서버에 다시 붙이면 끝.
|
||||
|
||||
우리 플랫폼의 탄력적 IP: 3.36.160.246
|
||||
|
||||
edu.awesomedevapp.com ──(DNS)──> 3.36.160.246 ──> 우리 EC2
|
||||
|
||||
비유: 휴대폰(서버)을 바꿔도 번호이동으로
|
||||
전화번호(탄력적 IP)는 그대로 가져가는 것과 같아요.`;
|
||||
|
||||
const CODE_NSLOOKUP_EIP = `# 우리 도메인이 정말 그 탄력적 IP를 가리키는지 직접 확인:
|
||||
nslookup edu.awesomedevapp.com
|
||||
|
||||
Name: edu.awesomedevapp.com
|
||||
Address: 3.36.160.246 ← 위에서 배운 그 주소가 나오나요?
|
||||
|
||||
# 한 발 더: 그 IP까지 몇 개의 환승역을 거치는지도 보세요.
|
||||
tracert 3.36.160.246
|
||||
# 마지막 홉이 AWS 서울 리전의 우리 서버입니다.`;
|
||||
|
||||
const CODE_LB = `로드밸런서(Load Balancer) = 서버 여러 대 앞의 '번호표 기계'
|
||||
|
||||
서버 1대일 때: 손님이 몰리면?
|
||||
[사용자들] → [서버] 줄이 길어지고, 서버가 지쳐 쓰러짐
|
||||
|
||||
로드밸런서를 두면:
|
||||
┌→ [서버 A]
|
||||
[사용자들] → [LB] ──┼→ [서버 B] 요청을 골고루 분배!
|
||||
└→ [서버 C]
|
||||
|
||||
로드밸런서가 하는 일:
|
||||
1) 분배 — 요청을 여러 서버에 나눠 보냄 (은행 번호표 기계)
|
||||
2) 헬스체크 — "살아 있니?" 주기적으로 확인,
|
||||
쓰러진 서버는 명단에서 빼고 나머지로 배분
|
||||
3) 무중단 배포의 열쇠 — 서버를 한 대씩 빼서 업데이트해도
|
||||
나머지가 손님을 받으니 서비스가 안 끊겨요
|
||||
|
||||
우리 플랫폼은 아직 서버 1대라 LB가 없어요.
|
||||
대신 Caddy가 '건물 안의 작은 안내데스크' 역할로
|
||||
컨테이너들에게 요청을 나눠 주고 있죠. 사용자가 늘면
|
||||
LB를 앞에 세우는 게 자연스러운 다음 단계입니다.`;
|
||||
|
||||
const CODE_OUR_STACK = `우리 학습 플랫폼의 실제 네트워크 구성 — 총정리!
|
||||
|
||||
[여러분의 브라우저]
|
||||
│ ① "edu.awesomedevapp.com 주세요"
|
||||
▼
|
||||
[DNS] 이름 → 3.36.160.246 (탄력적 IP) 로 번역
|
||||
│ ② HTTPS 요청 출발
|
||||
▼
|
||||
━━━━━ AWS 서울 리전 (ap-northeast-2) ━━━━━
|
||||
┌─ VPC (어썸데브의 울타리) ──────────────────┐
|
||||
│ ┌─ 퍼블릭 서브넷 ────────────────────────┐ │
|
||||
│ │ [EC2 서버] ← 탄력적 IP 3.36.160.246 │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ ③ 443 포트로 도착 │ │
|
||||
│ │ [Caddy] 현관 안내데스크 │ │
|
||||
│ │ │ HTTPS 암호 해제 + 길 안내 │ │
|
||||
│ │ ├──> [React 프론트] 화면 담당 │ │
|
||||
│ │ ├──> [Spring Boot] API·로직 담당 │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ ▼ (Docker 내부 네트워크) │ │
|
||||
│ │ │ [PostgreSQL] 데이터 금고 │ │
|
||||
│ │ └──> [Gitea] 우리 코드 저장소 │ │
|
||||
│ └───────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────┘
|
||||
|
||||
컨테이너들은 Docker의 내부 네트워크로만 서로 대화해요.
|
||||
PostgreSQL은 바깥(인터넷)에서 직접 두드릴 수 없습니다 —
|
||||
프라이빗 서브넷 개념을 Docker 네트워크로 실현한 셈이에요.`;
|
||||
|
||||
const CODE_CDN = `CDN(Content Delivery Network) = 전국(전 세계) 편의점 체인
|
||||
|
||||
문제: 브라질 사용자가 서울 서버의 이미지를 받으면?
|
||||
지구 반대편 → 왕복 수백 ms, 매번 느림
|
||||
|
||||
해결: 세계 곳곳의 '중간 창고(엣지 서버)'에 복사본을 미리 배치
|
||||
|
||||
[서울 원본 서버] ──복사──> [도쿄 엣지] [미국 엣지] [유럽 엣지]...
|
||||
|
||||
브라질 사용자 → 가장 가까운 엣지에서 받음 → 훨씬 빠름!
|
||||
|
||||
편의점 비유: 본사 공장(원본 서버)이 서울에 있어도,
|
||||
동네 편의점(엣지)에 물건을 미리 깔아 두면
|
||||
어디서든 걸어가서 바로 사죠.
|
||||
|
||||
무엇을 CDN에 두나? 자주 바뀌지 않는 것들 —
|
||||
이미지, 동영상, JS/CSS 파일. (개인 학습 기록처럼
|
||||
매번 달라지는 데이터는 원본 서버가 만들어야 해요.)
|
||||
|
||||
우리 플랫폼은 사용자가 한국에 몰려 있어 아직 CDN이 없지만,
|
||||
유튜브·넷플릭스가 전 세계에서 끊김 없이 도는 비결이 바로 이거예요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '리전과 가용영역' },
|
||||
{ n: 2, label: 'VPC' },
|
||||
{ n: 3, label: '퍼블릭/프라이빗 서브넷' },
|
||||
{ n: 4, label: '탄력적 IP' },
|
||||
{ n: 5, label: '로드밸런서 한 입' },
|
||||
{ n: 6, label: '우리 플랫폼 구성도' },
|
||||
{ n: 7, label: 'CDN 맛보기' },
|
||||
];
|
||||
|
||||
export default function CloudNetworkPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>클라우드 네트워크 입문<br />— 우리 서버는 어디에, 어떻게 살고 있을까</h1>
|
||||
<p>
|
||||
"서버가 클라우드에 있다"는 말은 사실 <strong>지구 어딘가의 진짜 데이터센터</strong>에
|
||||
있다는 뜻이에요. 우리 플랫폼이 사는 AWS 서울 리전에서 출발해, VPC라는 울타리와
|
||||
서브넷이라는 구역을 지나 여러분의 브라우저까지 — 실제 우리 서비스의 네트워크를
|
||||
지도를 그리듯 따라가 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="데이터센터, 리전, 가용영역" sub="구름이 아니라 건물입니다">
|
||||
<p>
|
||||
<strong>클라우드</strong>라고 하면 하늘에 붕 떠 있는 무언가 같지만, 실체는
|
||||
<strong> 데이터센터</strong> — 서버 수만 대가 랙에 꽂혀 웅웅 돌아가는 거대한
|
||||
건물이에요. AWS 같은 클라우드 회사는 이런 건물을 전 세계에 지어 두고
|
||||
"서버 필요하면 우리 건물 것 빌려 쓰세요" 하고 임대업을 하는 겁니다.
|
||||
PC방에 가면 내 컴퓨터 없이도 게임을 할 수 있는 것처럼, 우리는 서버를
|
||||
사지 않고 빌려 써요.
|
||||
</p>
|
||||
<p>
|
||||
이 건물들은 <strong>리전</strong>(도시 단위 묶음)과 <strong>가용영역</strong>(그 안의
|
||||
독립 건물 그룹)으로 조직돼요. 우리 플랫폼은 <strong>서울 리전(ap-northeast-2)</strong>의
|
||||
EC2 서버에서 돌아갑니다.
|
||||
</p>
|
||||
<Code>{CODE_REGION_AZ}</Code>
|
||||
<p>
|
||||
가용영역이 여러 개인 이유는 <strong>계란을 한 바구니에 담지 않기</strong> 위해서예요.
|
||||
한 건물에 불이 나도 몇 km 떨어진 다른 건물은 멀쩡하니까, 중요한 서비스는
|
||||
여러 가용영역에 복사본을 두고 운영합니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="VPC — 클라우드 속 내 네트워크" sub="거대한 단지 안에 울타리 치기">
|
||||
<p>
|
||||
데이터센터에는 수천 개 회사의 서버가 함께 살아요. 그런데 우리 서버와 옆 회사
|
||||
서버가 아무렇게나 통신할 수 있다면 큰일이겠죠? 그래서 클라우드에서는 회사마다
|
||||
<strong> VPC</strong>(Virtual Private Cloud)라는 <strong>가상의 울타리</strong>를
|
||||
쳐 줍니다. 같은 건물을 쓰지만 서로의 존재가 전혀 보이지 않는, 완전히 분리된
|
||||
'내 네트워크'예요.
|
||||
</p>
|
||||
<Code>{CODE_VPC}</Code>
|
||||
<p>
|
||||
지난 <Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서
|
||||
공유기가 집 안에 <span className="icode">192.168.x.x</span> 사설 네트워크를
|
||||
만들어 주는 걸 봤죠? VPC는 그것의 <strong>회사·클라우드 버전</strong>입니다.
|
||||
울타리 안 IP 대역을 우리가 정하고, 바깥과는 <strong>게이트웨이</strong>라는
|
||||
정문을 통해서만 통해요. 개념이 정확히 평행이라, 하나를 이해하면 둘 다 이해한 거예요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="퍼블릭 서브넷 vs 프라이빗 서브넷" sub="대로변 가게와 직원 전용 구역">
|
||||
<p>
|
||||
VPC라는 땅이 생겼으면, 이제 <strong>구역 정리</strong>를 해요. 땅을 몇 개의
|
||||
<strong> 서브넷</strong>(부분 네트워크)으로 나누고, 각 구역에 어울리는 서버를
|
||||
배치합니다. 핵심 구분은 딱 하나 — <strong>인터넷에서 직접 들어올 수 있느냐</strong>예요.
|
||||
</p>
|
||||
<Code>{CODE_SUBNET}</Code>
|
||||
<div className="olist">
|
||||
<ol>
|
||||
<li>
|
||||
<strong>퍼블릭 서브넷</strong> — 인터넷 게이트웨이(정문)와 연결된 대로변.
|
||||
웹 서버처럼 <strong>손님(사용자)을 직접 맞아야 하는 것</strong>만 여기에 둬요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>프라이빗 서브넷</strong> — 정문과 이어지지 않은 안쪽 구역. 데이터베이스처럼
|
||||
<strong> 절대 외부에 노출되면 안 되는 것</strong>을 숨겨 둡니다. 해커가 인터넷에서
|
||||
아무리 두드려도 애초에 길이 없어요.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>실무에서 가장 흔한 보안 사고 중 하나</b>가 DB를 퍼블릭에 두는 거예요.
|
||||
"은행 금고를 길가에 내놓은 것"과 같습니다. <strong>손님 맞을 것만 대로변에,
|
||||
귀중품은 안쪽에</strong> — 이 원칙 하나만 기억해도 사고의 절반은 막아요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="탄력적 IP — 서버의 평생 전화번호" sub="우리 서버의 주소, 3.36.160.246">
|
||||
<p>
|
||||
클라우드 서버(EC2)는 껐다 켜면 <strong>공인 IP가 바뀔 수 있어요</strong>.
|
||||
빌린 자취방을 옮길 때마다 주소가 바뀌는 것과 같죠. 그런데 도메인은 특정 IP를
|
||||
가리키고 있으니, IP가 바뀌면 <strong>도메인이 허공을 가리키는</strong> 사고가 납니다.
|
||||
</p>
|
||||
<Code>{CODE_EIP}</Code>
|
||||
<p>
|
||||
그래서 우리 플랫폼은 <strong>탄력적 IP(Elastic IP)</strong>를 하나 빌려서 EC2에
|
||||
붙여 뒀어요. 그게 바로 <span className="icode">3.36.160.246</span> — 서버를
|
||||
재부팅해도, 언젠가 더 큰 서버로 이사해도 이 번호는 그대로 가져갑니다.
|
||||
휴대폰을 바꿔도 번호이동으로 전화번호를 유지하는 것과 같아요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 터미널에서 우리 도메인이 정말{' '}
|
||||
<span className="icode">3.36.160.246</span>을 가리키는지 DNS에 직접 물어보세요.
|
||||
지난 코스에서 배운 <span className="icode">nslookup</span>과{' '}
|
||||
<span className="icode">tracert</span>가 여기서 다시 등장합니다!
|
||||
</div>
|
||||
<Code>{CODE_NSLOOKUP_EIP}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="로드밸런서 한 입" sub="손님이 몰리면 번호표 기계가 필요해요">
|
||||
<p>
|
||||
서버 한 대가 감당 못 할 만큼 사용자가 늘면 어떻게 할까요? 더 비싼 서버로
|
||||
바꾸는 방법(수직 확장)도 있지만, 한계가 있어요. 그래서 실무에서는
|
||||
<strong> 같은 서버를 여러 대 두고 요청을 나눠 받게</strong> 합니다(수평 확장).
|
||||
이때 맨 앞에서 교통정리를 하는 장치가 <strong>로드밸런서</strong>예요.
|
||||
</p>
|
||||
<Code>{CODE_LB}</Code>
|
||||
<p>
|
||||
은행에 창구가 하나뿐이면 줄이 끝없이 길어지지만, 번호표 기계가 손님을
|
||||
여러 창구로 나눠 주면 전체가 빨라지죠. 게다가 한 창구 직원이 자리를 비워도
|
||||
(서버 장애·업데이트) 나머지 창구가 손님을 받으니 <strong>서비스가 멈추지 않아요</strong> —
|
||||
이게 여러분이 쓰는 큰 서비스들이 "점검 중" 없이 업데이트되는 비결입니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="우리 플랫폼의 실제 구성도 — 총정리" sub="1~5섹션을 한 장의 지도로">
|
||||
<p>
|
||||
이제 배운 조각을 전부 이어 붙일 시간이에요. 여러분이 지금 이 페이지를 보기까지,
|
||||
요청이 실제로 지나온 길 전체입니다. <strong>이 다이어그램 한 장을 그릴 수 있으면
|
||||
이 코스는 통과</strong>예요.
|
||||
</p>
|
||||
<Code>{CODE_OUR_STACK}</Code>
|
||||
<p>
|
||||
눈여겨볼 포인트: <strong>Caddy</strong>는 밖을 향해 열린 유일한 현관이고,
|
||||
<strong> PostgreSQL</strong>은 Docker 내부 네트워크에만 있어서 인터넷에서 직접
|
||||
접근할 수 없어요. 섹션 3에서 배운 "손님 맞을 것만 대로변에, 금고는 안쪽에"
|
||||
원칙이 우리 서버 안에서 실제로 작동하고 있는 겁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 이 페이지에서 <span className="kbd">F12</span> →{' '}
|
||||
<span className="kbd">Network</span> 탭 → <span className="kbd">F5</span> 새로고침.
|
||||
아무 요청이나 클릭해 <strong>Remote Address</strong>를 찾아보세요.{' '}
|
||||
<span className="icode">3.36.160.246:443</span> — 방금 다이어그램에서 본
|
||||
탄력적 IP와 HTTPS 포트가 진짜로 찍혀 있을 거예요. 지도와 실물을 맞춰 보는 순간입니다!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="CDN 맛보기" sub="전 세계에 미리 깔아 두는 복사본 창고">
|
||||
<p>
|
||||
마지막으로, 큰 서비스들이 쓰는 기술 하나만 맛봅시다. 서울 서버가 아무리 빨라도
|
||||
지구 반대편 사용자에게는 <strong>물리적 거리</strong> 때문에 느릴 수밖에 없어요.
|
||||
빛조차 왕복에 시간이 걸리니까요. 그래서 등장한 것이 <strong>CDN</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_CDN}</Code>
|
||||
<div className="tip">
|
||||
<b>생각해 보기</b> 우리 플랫폼에 브라질 지사가 생긴다면, 무엇을 CDN에 올리고
|
||||
무엇을 서울 원본 서버에 남겨야 할까요? 힌트: "모두에게 똑같은 것"과
|
||||
"사람마다 다른 것"을 나눠 보세요. 답을 멘토에게 말로 설명해 보면 완벽!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>☁️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 "클라우드"라는 말을 들으면 구름 대신 <strong>서울 리전의 데이터센터,
|
||||
VPC 울타리, 퍼블릭/프라이빗 구역, 그리고 3.36.160.246</strong>이 떠오를 거예요.
|
||||
우리 플랫폼의 네트워크 지도를 통째로 그릴 수 있게 됐습니다. 아직{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스를 안 들었다면
|
||||
먼저 다녀오고, 다음은 이 지도 위에 서비스를 올리는 방법 —{' '}
|
||||
<Link to="/learn/deploy"><strong>배포 기초</strong></Link> 코스에서 Docker 컨테이너가
|
||||
EC2 위에서 어떻게 굴러가는지 이어서 배워 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
387
frontend/src/pages/courses/ColorPage.jsx
Normal file
387
frontend/src/pages/courses/ColorPage.jsx
Normal file
@ -0,0 +1,387 @@
|
||||
// 이 파일이 하는 일: "색 이론 기초" 코스 — 색의 3속성에서 출발해
|
||||
// 배색 원리, 우리 브랜드 블루(#2159C5), 접근성, 다크모드까지 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (3속성 → 색상환·배색 → 60-30-10 → 브랜드 컬러 → 톤앤매너 → 명도 대비·접근성 → 다크모드 → 배색 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_HSL = `색 하나를 말로 설명하는 세 가지 좌표 — HSL
|
||||
|
||||
H (Hue, 색상) "무슨 색이야?" 0~360° 각도
|
||||
S (Saturation, 채도) "얼마나 쨍해?" 0%(회색)~100%(쨍함)
|
||||
L (Lightness, 명도) "얼마나 밝아?" 0%(검정)~100%(흰색)
|
||||
|
||||
예) 우리 브랜드 블루 #2159C5 를 HSL로 번역하면:
|
||||
hsl(219, 71%, 45%)
|
||||
→ 색상 219° (파랑 근처) · 채도 71% (꽤 쨍함) · 명도 45% (중간보다 살짝 어두움)
|
||||
|
||||
CSS에서 그대로 쓸 수 있어요:
|
||||
color: hsl(219, 71%, 45%);
|
||||
/* 명도만 올리면 같은 파랑의 '밝은 버전'이 나옵니다 */
|
||||
background: hsl(219, 71%, 92%);`;
|
||||
|
||||
const CODE_WHEEL = `색상환(Color Wheel) — 색상(H) 0~360°를 시계처럼 둥글게 배열한 것
|
||||
|
||||
0° 빨강
|
||||
330° 30° 주황
|
||||
270° 보라 60° 노랑
|
||||
240° 120° 초록
|
||||
180° 청록
|
||||
(219° ← 우리 블루는 이 근처!)
|
||||
|
||||
배색의 두 기본기:
|
||||
1) 보색(Complementary) — 정반대(180° 차이)의 색.
|
||||
파랑(219°) ↔ 주황(39°) → 대비가 강렬, 강조·경고에 딱
|
||||
2) 유사색(Analogous) — 이웃(±30° 안팎)의 색.
|
||||
파랑 · 청록 · 남보라 → 차분하고 조화로움, 화면 전체 톤에 딱
|
||||
|
||||
암기법: "싸울 땐 맞은편(보색), 친할 땐 옆자리(유사색)"`;
|
||||
|
||||
const CODE_603010 = `60-30-10 법칙 — 화면 색 비율의 황금 레시피
|
||||
|
||||
┌────────────────────────────────────┐
|
||||
│ 60% 바탕색 (배경·카드) │ ← 무대
|
||||
│ 예) 흰색 / 아주 연한 회색 │
|
||||
├─────────────────────┐ │
|
||||
│ 30% 보조색 (텍스트·경계선·아이콘) │ ← 조연
|
||||
│ 예) 진회색 계열 │
|
||||
├───────┐ │ │
|
||||
│ 10% │ 포인트색 │ │ ← 주인공
|
||||
│ #2159C5 (버튼·링크·강조) │
|
||||
└───────┴─────────────┴──────────────┘
|
||||
|
||||
포인트색이 10%뿐이라서 눈에 띄는 거예요.
|
||||
파란 버튼이 화면의 60%를 차지하면? 아무것도 강조되지 않습니다.`;
|
||||
|
||||
const CODE_BRAND_BLUE = `우리 블루 #2159C5 해부
|
||||
|
||||
#2159C5 = R 33 · G 89 · B 197 (16진수 21=33, 59=89, C5=197)
|
||||
= hsl(219, 71%, 45%)
|
||||
|
||||
왜 이 파랑인가?
|
||||
- 색상 219° : 순수 파랑(240°)보다 살짝 청록 쪽 → 차갑되 딱딱하지 않음
|
||||
- 채도 71% : 쨍하지만 형광은 아님 → "일 잘하는" 인상
|
||||
- 명도 45% : 흰 배경 위에서 글자·버튼으로 써도 또렷함
|
||||
|
||||
파랑이 '신뢰의 색'인 이유 — 하늘·바다처럼 변하지 않는 것의 색이라
|
||||
심리적으로 안정·전문성을 떠올리게 해요. 은행·병원·개발사 로고에
|
||||
파랑이 유난히 많은 건 우연이 아닙니다. 어썸데브도 그 계보!`;
|
||||
|
||||
const CODE_TONE = `톤앤매너(Tone & Manner) — 색이 만드는 '말투'
|
||||
|
||||
같은 파랑 계열이라도 톤이 다르면 인상이 달라요:
|
||||
|
||||
진하고 채도 높게 → 또렷·전문적 (우리 메인 버튼)
|
||||
연하고 채도 낮게 → 부드러움·여백 (배경·비활성 상태)
|
||||
어둡고 탁하게 → 진지·무게감 (다크모드 표면)
|
||||
|
||||
어썸데브 학습 플랫폼의 톤앤매너 한 줄 요약:
|
||||
"흰 바탕 + 회색 글 + 파랑 한 방울" — 차분한 교실에
|
||||
선생님(파랑)이 필요한 순간에만 등장하는 구성이에요.
|
||||
|
||||
톤앤매너가 무너지는 흔한 실수:
|
||||
- 페이지마다 다른 파랑을 쓰는 것 (#2159C5 하나로 통일!)
|
||||
- 강조하고 싶다고 빨강·초록·보라를 한 화면에 다 쓰는 것`;
|
||||
|
||||
const CODE_CONTRAST = `명도 대비(Contrast)와 WCAG — "읽히는가?"의 기준
|
||||
|
||||
대비비(contrast ratio) = 밝은 색 밝기 : 어두운 색 밝기 (1:1 ~ 21:1)
|
||||
|
||||
WCAG(웹 접근성 표준)의 최소 기준:
|
||||
본문 텍스트 4.5 : 1 이상 (AA 등급)
|
||||
큰 텍스트(18pt+) 3 : 1 이상
|
||||
|
||||
우리 팔레트로 확인해 보면:
|
||||
흰 배경(#FFFFFF) 위 #2159C5 글자 → 약 6.3 : 1 ✅ 본문 합격
|
||||
흰 배경 위 연한 하늘색 글자 → 약 1.8 : 1 ❌ 눈 아픔
|
||||
|
||||
기억할 것: "예쁜데 안 읽히는 색"은 디자인이 아니라 장식입니다.
|
||||
저시력 사용자, 햇빛 아래 폰 화면 — 대비는 모두를 위한 배려예요.`;
|
||||
|
||||
const CODE_DARKMODE = `다크모드에서 색이 달라지는 이유 — 그냥 반전이 아니다!
|
||||
|
||||
라이트 모드 다크 모드
|
||||
───────────────────── ─────────────────────
|
||||
배경: 흰색 배경: 검정에 가까운 짙은 회색
|
||||
(순수 검정 #000은 눈이 피로 → 살짝 회색)
|
||||
글자: 진회색 글자: 연회색
|
||||
(순수 흰색은 어둠 속에서 너무 번쩍임)
|
||||
포인트: #2159C5 포인트: 더 밝은 파랑!
|
||||
|
||||
왜 포인트색을 밝게 바꿀까?
|
||||
어두운 배경 위에서 #2159C5(명도 45%)는 배경에 묻혀서
|
||||
대비비가 뚝 떨어져요. 그래서 다크모드에선 같은 색상(H)을
|
||||
유지한 채 명도(L)만 올린 파랑을 씁니다.
|
||||
"같은 사람인데 무대 조명에 맞춰 옷만 갈아입는" 거예요.
|
||||
|
||||
우리 앱도 CSS 변수로 이걸 해요:
|
||||
:root { --accent: #2159C5; }
|
||||
[data-theme="dark"] { --accent: 더 밝은 파랑; }
|
||||
컴포넌트는 var(--accent)만 쓰니까 테마가 바뀌면 색이 따라 바뀝니다.
|
||||
(이 코스 페이지에 색을 하드코딩하지 않는 이유이기도 해요!)`;
|
||||
|
||||
const CODE_PRACTICE = `배색 연습 — 우리 팔레트로 '가입 완료' 화면을 칠해 보세요
|
||||
|
||||
재료 (우리 플랫폼 팔레트):
|
||||
A. 흰색/연회색 (바탕) B. 진회색 (본문 글자)
|
||||
C. #2159C5 (브랜드 블루) D. 연한 블루 (C의 명도 올린 버전)
|
||||
|
||||
미션: 아래 요소에 A~D를 배정하고, 이유를 한 줄씩 써 보기
|
||||
1) 페이지 배경 → ?
|
||||
2) "가입을 축하해요" 제목 → ?
|
||||
3) 안내 본문 3줄 → ?
|
||||
4) [학습 시작하기] 버튼 → ?
|
||||
5) 버튼 뒤 강조 배경 띠 → ?
|
||||
|
||||
체크리스트 (60-30-10 감사):
|
||||
□ 파랑(C)이 화면의 10%쯤인가? 버튼·링크에만 썼는가?
|
||||
□ 글자와 배경의 대비가 충분한가? (섹션 6 기준으로!)
|
||||
□ 파랑을 두 가지 이상 섞지 않았는가? (D는 C의 명도 변형만 OK)
|
||||
|
||||
모범답안은 없어요 — 다만 "왜 여기에 이 색?"을
|
||||
말로 설명할 수 있으면 통과입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '색의 3속성' },
|
||||
{ n: 2, label: '색상환과 배색' },
|
||||
{ n: 3, label: '60-30-10 법칙' },
|
||||
{ n: 4, label: '우리 블루 해부' },
|
||||
{ n: 5, label: '톤앤매너' },
|
||||
{ n: 6, label: '대비와 접근성' },
|
||||
{ n: 7, label: '다크모드의 색' },
|
||||
{ n: 8, label: '배색 실습' },
|
||||
];
|
||||
|
||||
export default function ColorPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>색 이론 기초<br />— 감이 아니라 문법으로 색 고르기</h1>
|
||||
<p>
|
||||
"예쁜 색"은 취향이지만, "잘 읽히고 신뢰가 가는 색"은 배울 수 있는 문법이에요.
|
||||
색의 3속성에서 출발해 우리 브랜드 블루 <strong>#2159C5</strong>가 왜 그 자리에
|
||||
있는지, 다크모드에선 왜 옷을 갈아입는지까지 따라가 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">직접 확인 실습 4개</span>
|
||||
<span className="chip">준비물: 우리 플랫폼 화면</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="색의 3속성 — 색상·명도·채도" sub="모든 색은 세 개의 손잡이로 만들어진다">
|
||||
<p>
|
||||
세상의 모든 색은 딱 <strong>세 개의 손잡이(다이얼)</strong>를 돌려서 만들 수 있어요.
|
||||
<strong> 색상(Hue)</strong>은 "무슨 색이냐"(빨강·파랑·초록...),
|
||||
<strong> 명도(Lightness)</strong>는 "얼마나 밝냐",
|
||||
<strong> 채도(Saturation)</strong>는 "얼마나 쨍하냐"입니다.
|
||||
</p>
|
||||
<p>
|
||||
음악에 비유하면 색상은 <strong>음정</strong>(도·레·미), 명도는 <strong>볼륨</strong>,
|
||||
채도는 <strong>이펙트의 세기</strong> 같은 거예요. 같은 '도'라도 크게 치거나 작게 칠 수
|
||||
있듯이, 같은 파랑도 밝은 파랑·어두운 파랑·탁한 파랑이 될 수 있죠.
|
||||
</p>
|
||||
<Code>{CODE_HSL}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 이 페이지에서 <span className="kbd">F12</span>로 개발자
|
||||
도구를 열고, 아무 요소나 선택해 CSS의 색상값을 클릭해 보세요. 색상 피커가 뜨면
|
||||
<span className="icode">HEX</span> 표기를 <span className="icode">HSL</span>로
|
||||
전환한 뒤 <strong>H·S·L 슬라이더를 하나씩만</strong> 움직여 보세요 — 세 손잡이가
|
||||
각각 뭘 바꾸는지 눈으로 확인할 수 있어요. (새로고침하면 원래대로 돌아오니 마음껏!)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="색상환과 배색 — 보색·유사색" sub="색들의 자리 배치도를 알면 조합이 보인다">
|
||||
<p>
|
||||
색상(H) 0~360°를 시계처럼 둥글게 배열한 것이 <strong>색상환</strong>이에요.
|
||||
색상환은 배색의 <strong>지도</strong>입니다 — 어떤 색과 어떤 색을 붙이면 싸우고,
|
||||
어떤 색끼리는 잘 지내는지가 자리 배치로 보이거든요.
|
||||
</p>
|
||||
<Code>{CODE_WHEEL}</Code>
|
||||
<p>
|
||||
<strong>보색</strong>은 반에서 정반대 성격의 두 친구 같아요 — 붙여 놓으면 서로가
|
||||
서로를 돋보이게 하지만, 너무 많이 쓰면 화면이 시끄러워져요. 그래서 보색은
|
||||
"여기 좀 봐 주세요" 하는 <strong>강조 한 곳</strong>에만 씁니다.
|
||||
<strong> 유사색</strong>은 단짝 친구들 — 나란히 있어도 튀지 않아서 배경·카드·그래프처럼
|
||||
<strong> 넓은 면적</strong>을 채울 때 좋아요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>주의</b> 보색을 글자와 배경으로 쓰는 건 금물이에요. 예를 들어 파란 배경에
|
||||
주황 글자는 경계가 아른아른 떨리는 <strong>진동 현상</strong>이 생겨 읽기 괴롭습니다.
|
||||
보색은 "면 대 면"이 아니라 "넓은 면 위의 작은 포인트"로!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="60-30-10 법칙" sub="색 비율의 황금 레시피">
|
||||
<p>
|
||||
어떤 색을 고를지 알았다면, 이제 <strong>얼마나 쓸지</strong>가 문제예요.
|
||||
인테리어에서 온 유명한 법칙이 <strong>60-30-10</strong>입니다 —
|
||||
바탕색 60%, 보조색 30%, 포인트색 10%. 요리로 치면 밥(60) + 반찬(30) + 고추장
|
||||
한 스푼(10)이에요. 고추장이 맛있다고 밥 대신 고추장을 60% 담으면... 그건 요리가 아니죠.
|
||||
</p>
|
||||
<Code>{CODE_603010}</Code>
|
||||
<p>
|
||||
핵심 원리는 <strong>희소성</strong>이에요. 포인트색이 귀하니까(10%) 눈이 거기로
|
||||
가는 거예요. 우리 플랫폼에서 파란 버튼이 한눈에 들어오는 이유는 파랑이 예뻐서가
|
||||
아니라, 화면의 나머지 90%가 <strong>파랑에게 양보</strong>하고 있기 때문입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 우리 학습 플랫폼 아무 페이지나 열고 눈을 살짝 찡그린 채
|
||||
바라보세요(디테일이 뭉개지면 색 덩어리만 보여요). 흰 바탕이 대략 60, 회색 글·선이 30,
|
||||
파랑이 10 근처인지 어림해 보세요. 그리고 자주 쓰는 다른 서비스 하나를 열어 같은 방식으로
|
||||
비율을 세어 보면 — 잘 만든 화면일수록 이 비율에 가깝다는 걸 발견하게 될 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="브랜드 컬러의 힘 — 우리 블루 #2159C5 해부" sub="왜 하필 '신뢰의 파랑'인가">
|
||||
<p>
|
||||
브랜드 컬러는 회사의 <strong>교복</strong> 같은 거예요. 로고, 버튼, 발표 자료,
|
||||
명함 — 어디서든 같은 색이 반복되면 사람들은 색만 보고도 "아, 어썸데브"라고
|
||||
알아봅니다. 반복이 만드는 기억, 그게 브랜드 컬러의 힘이에요.
|
||||
</p>
|
||||
<Code>{CODE_BRAND_BLUE}</Code>
|
||||
<p>
|
||||
섹션 1의 세 손잡이로 다시 보면, #2159C5는 <strong>우연히 예쁜 파랑이 아니라
|
||||
설계된 파랑</strong>이에요. 채도를 71%까지만 올려서 형광펜처럼 가볍지 않고,
|
||||
명도를 45%로 잡아서 흰 배경 위 글자·버튼 어느 쪽으로도 쓸 수 있죠.
|
||||
B2B 개발사에게 필요한 "차분한데 유능해 보이는" 인상을 세 손잡이로 조율한 결과입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>기억할 것</b> 브랜드 컬러는 <strong>딱 하나의 값</strong>으로 관리해요.
|
||||
"대충 비슷한 파랑"이 문서마다 다르게 쓰이는 순간 교복 효과는 사라집니다.
|
||||
우리 코드에서는 CSS 변수 한 곳에만 정의하고 모두가 그 변수를 가져다 쓰는 이유예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="톤앤매너" sub="색이 만드는 서비스의 '말투'">
|
||||
<p>
|
||||
<strong>톤앤매너</strong>는 서비스 전체가 색·서체·말씨로 풍기는 일관된 분위기예요.
|
||||
사람으로 치면 <strong>말투</strong>죠. 어제는 존댓말, 오늘은 반말, 내일은 사투리인
|
||||
사람은 신뢰가 안 가듯이, 페이지마다 색의 성격이 바뀌는 서비스도 어딘가 불안해 보여요.
|
||||
</p>
|
||||
<Code>{CODE_TONE}</Code>
|
||||
<p>
|
||||
중요한 건 톤앤매너가 <strong>새 색을 고르는 일이 아니라, 쓸 수 있는 색을 좁히는
|
||||
일</strong>이라는 점이에요. "우리는 이 파랑과 이 회색만 쓴다"는 제약이 있어야
|
||||
누가 어떤 페이지를 만들어도 같은 회사 제품처럼 보입니다. 제약이 곧 정체성이에요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="명도 대비와 접근성" sub="예쁜 색보다 먼저, 읽히는 색 — WCAG 감각 익히기">
|
||||
<p>
|
||||
색 조합의 첫 번째 관문은 미적 감각이 아니라 <strong>"읽히는가?"</strong>예요.
|
||||
글자색과 배경색의 <strong>명도 차이(대비)</strong>가 작으면, 시력이 좋은 사람도
|
||||
햇빛 아래선 못 읽고, 저시력 사용자에겐 아예 보이지 않는 화면이 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_CONTRAST}</Code>
|
||||
<p>
|
||||
<strong>WCAG</strong>는 웹 접근성의 국제 기준인데, 숫자를 다 외울 필요는 없어요.
|
||||
<strong> "본문은 4.5:1"</strong> 하나만 기억하고, 애매하면 대비 검사 도구로
|
||||
확인하는 습관이면 충분합니다. 좋은 자가 테스트: 만든 화면을 <strong>흑백으로
|
||||
상상</strong>했을 때도 글자가 또렷하면 대체로 합격이에요 — 대비는 결국 색상이 아니라
|
||||
<strong> 명도</strong>의 문제거든요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 개발자 도구(<span className="kbd">F12</span>)로 우리 플랫폼의
|
||||
아무 텍스트나 검사한 뒤, Styles 패널에서 <span className="icode">color</span> 값 옆의
|
||||
색 견본을 클릭해 보세요. 색상 피커 안에 <strong>Contrast ratio(대비비)</strong> 항목이
|
||||
나타나고, AA 기준 통과 여부를 체크 표시로 알려 줍니다. 본문 글자가 4.5:1을 넘는지
|
||||
직접 확인해 보고, 슬라이더로 명도를 올려 "몇부터 불합격이 되는지" 경계선도 찾아보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="다크모드에서 색이 달라지는 이유" sub="우리 앱의 테마 토글이 하는 일">
|
||||
<p>
|
||||
다크모드는 색을 단순히 <strong>반전</strong>하는 게 아니에요. 무대 조명이 바뀌면
|
||||
같은 옷도 다르게 보이듯, 배경이 어두워지면 모든 색의 <strong>대비 관계</strong>가
|
||||
다시 계산돼야 합니다. 그래서 잘 만든 다크모드는 색마다 "어두운 무대용 버전"을
|
||||
따로 준비해요.
|
||||
</p>
|
||||
<Code>{CODE_DARKMODE}</Code>
|
||||
<p>
|
||||
여기서 섹션 1의 세 손잡이가 또 등장해요. 다크모드용 파랑은 <strong>색상(H)은
|
||||
그대로 두고 명도(L)만 올린</strong> 색이에요. 색상까지 바꾸면 "다른 브랜드"가
|
||||
되어 버리니까, 정체성(색상)은 지키고 조명 대응(명도)만 하는 거죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 학습 플랫폼의 <strong>테마 토글</strong>을 눌러
|
||||
라이트 ↔ 다크를 오가며 파란 버튼과 링크를 관찰해 보세요. "같은 파랑인데 다크모드에서
|
||||
조금 더 밝다"가 보이면 성공! 그다음 <span className="kbd">F12</span> →
|
||||
Elements에서 <span className="icode">html</span> 요소의 속성이 토글에 따라 바뀌는 것과,
|
||||
CSS 변수 값이 함께 바뀌는 것까지 찾아내면 오늘 배운 이론과 코드가 한 줄로 이어집니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="실습 — 우리 팔레트로 배색 연습" sub="이론을 손으로: 화면 하나를 직접 칠해 보기">
|
||||
<p>
|
||||
이제 1~7섹션의 도구를 전부 꺼내 쓸 시간이에요. 종이(또는 그림 도구)에
|
||||
간단한 <strong>'가입 완료' 화면</strong>을 스케치하고, 우리 팔레트 네 가지만으로
|
||||
칠해 봅니다. 규칙은 하나 — <strong>모든 색 선택에 이유를 달 것.</strong>
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE}</Code>
|
||||
<ol className="olist">
|
||||
<li>요소 다섯 개에 A~D를 배정하고 이유를 한 줄씩 적어요.</li>
|
||||
<li>60-30-10 체크리스트로 스스로 감사(audit)해요 — 파랑이 넘치면 덜어내기.</li>
|
||||
<li>흑백으로 상상했을 때도 위계(제목 > 본문 > 배경)가 보이는지 확인해요.</li>
|
||||
<li>완성본을 멘토나 동기에게 보여 주고 "왜 이 색?" 질문에 말로 답해 보세요.</li>
|
||||
</ol>
|
||||
<div className="warn">
|
||||
<b>흔한 함정</b> "허전해서" 색을 추가하고 싶어질 거예요. 참으세요!
|
||||
허전함은 대부분 색이 아니라 <strong>여백과 크기 조절</strong>로 해결됩니다.
|
||||
색을 추가하는 건 언제나 마지막 수단이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🎨 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 색을 <strong>감</strong>이 아니라 <strong>세 개의 손잡이(색상·명도·채도)와
|
||||
비율(60-30-10), 대비(4.5:1)</strong>라는 문법으로 다룰 수 있게 됐어요.
|
||||
우리 블루 #2159C5가 버튼 위에 있는 이유도 설명할 수 있고요. 다음은 색이 올라가는
|
||||
뼈대를 배울 차례 — <Link to="/learn/layout"><strong>레이아웃과 타이포그래피
|
||||
기초</strong></Link> 코스에서 여백·정렬·글자 위계를 이어서 배워 보세요.
|
||||
배색 실습 결과물은 멘토 리뷰 과제로 꼭 제출하는 것, 잊지 마세요!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
345
frontend/src/pages/courses/CpuDeepPage.jsx
Normal file
345
frontend/src/pages/courses/CpuDeepPage.jsx
Normal file
@ -0,0 +1,345 @@
|
||||
// 이 파일이 하는 일: "CPU 깊이 보기" 코스 — CPU가 명령어 한 줄을 처리하는 원리에서 출발해
|
||||
// 코어·스레드·클럭·캐시, 발열과 스로틀링, 제품 이름 읽는 법, 작업 관리자 관찰 실습,
|
||||
// 그리고 "개발자에게 CPU가 중요한 순간(빌드 시간)"까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_CYCLE = `명령어 사이클 — CPU는 이 세 박자를 1초에 수십억 번 반복해요
|
||||
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ ① 가져오기 │ → │ ② 해석 │ → │ ③ 실행 │ ─┐
|
||||
│ (Fetch) │ │ (Decode) │ │ (Execute)│ │
|
||||
└──────────┘ └──────────┘ └──────────┘ │
|
||||
↑ │
|
||||
└────────── 다음 명령어로! ←──────────────┘
|
||||
|
||||
① 가져오기: 메모리(RAM)에서 다음 명령어를 읽어 온다
|
||||
② 해석: "아, 이건 두 숫자를 더하라는 뜻이구나" 알아듣는다
|
||||
③ 실행: 실제로 더하고, 결과를 저장한다
|
||||
|
||||
우리가 쓰는 자바(Java)·자바스크립트 코드도 결국
|
||||
이런 기계 명령어 수백만 개로 번역돼서 이 사이클을 탑니다.`;
|
||||
|
||||
const CODE_KITCHEN = `CPU = 주방, 이렇게 비유해 보세요
|
||||
|
||||
코어(Core) = 요리사 수 (8코어 = 요리사 8명)
|
||||
스레드(Thread) = 요리사의 손 놀림 (한 명이 두 요리를 번갈아 = 2스레드)
|
||||
클럭(Clock) = 손 놀리는 속도 (4.5GHz = 1초에 45억 박자)
|
||||
캐시(Cache) = 도마 옆 재료 선반 (냉장고=RAM까지 안 가도 되게)
|
||||
|
||||
예) "8코어 16스레드 4.5GHz" =
|
||||
요리사 8명이 각자 두 요리를 번갈아 하며,
|
||||
1초에 45억 박자로 손을 놀리는 주방`;
|
||||
|
||||
const CODE_CACHE = `캐시 3층 구조 — 가까울수록 작고 빠르다
|
||||
|
||||
CPU 코어 ── L1 캐시 손 안의 재료 (수십 KB, 가장 빠름)
|
||||
── L2 캐시 도마 옆 선반 (수 MB)
|
||||
── L3 캐시 주방 공용 선반 (수십 MB, 코어들이 공유)
|
||||
│
|
||||
RAM 복도 끝 냉장고 (수십 GB, 캐시보다 훨씬 느림)
|
||||
│
|
||||
SSD 지하 대형 창고 (TB급, RAM보다 또 훨씬 느림)
|
||||
|
||||
핵심: CPU 입장에서 RAM은 '멀어요'. 자주 쓰는 재료를
|
||||
가까운 선반(캐시)에 미리 올려 두는 게 속도의 비밀입니다.`;
|
||||
|
||||
const CODE_THROTTLE = `발열 → 스로틀링(Throttling) 시나리오
|
||||
|
||||
평상시 : 4.5GHz로 신나게 달림 (온도 50~70℃)
|
||||
무거운 작업: 온도 상승... 90℃ 돌파!
|
||||
스로틀링 : CPU가 스스로 클럭을 낮춤 (4.5 → 3.0GHz)
|
||||
"이대로 달리면 타 버리니까 감속할게"
|
||||
쿨링 회복 : 온도가 내려가면 다시 제 속도로
|
||||
|
||||
→ "게임(또는 빌드)이 처음엔 빠른데 몇 분 지나면 느려져요"의
|
||||
단골 범인이 바로 이 스로틀링이에요. 고장이 아니라 자기 보호!`;
|
||||
|
||||
const CODE_NAMING = `인텔 vs 라이젠 — 이름만 봐도 급이 보인다
|
||||
|
||||
인텔: Core i7-14700K
|
||||
│ │ │└─ 뒤 숫자+접미사: 세부 모델 (K=오버클럭 가능, F=내장그래픽 없음)
|
||||
│ │ └── 앞 두 자리 14 = 14세대 (클수록 최신)
|
||||
│ └───── i3 < i5 < i7 < i9 (등급, 클수록 상위)
|
||||
└────────── 브랜드
|
||||
|
||||
라이젠: Ryzen 7 7700X
|
||||
│ │ │└─ 접미사 (X=고성능, G=내장그래픽 강화)
|
||||
│ │ └── 첫 자리 7 = 7000번대 세대
|
||||
│ └───── 3 < 5 < 7 < 9 (등급, 인텔 i3~i9와 대응)
|
||||
└────────── 브랜드
|
||||
|
||||
함정 주의!
|
||||
"i7이 i5보다 무조건 빠르다" ← 세대가 다르면 틀릴 수 있어요.
|
||||
최신 세대 i5가 몇 년 전 i7을 이기는 일은 아주 흔합니다.
|
||||
등급(체급)과 세대(연식)를 반드시 같이 보세요.`;
|
||||
|
||||
const CODE_TASKMGR = `작업 관리자로 내 CPU 관찰하기 (Windows)
|
||||
|
||||
1) Ctrl + Shift + Esc → 작업 관리자 열기
|
||||
2) [성능] 탭 → 왼쪽에서 [CPU] 선택
|
||||
|
||||
여기서 찾아보세요:
|
||||
· 이름 예) Intel Core i5-1240P ← 섹션 5로 해독!
|
||||
· 코어 / 논리 프로세서 예) 12 / 16 ← 요리사 수 / 손 수
|
||||
· 기본 속도 vs 현재 속도 (GHz) ← 지금 몇 박자로 일하나
|
||||
· L1/L2/L3 캐시 크기 ← 선반 3층의 크기
|
||||
· 이용률(%) 그래프
|
||||
|
||||
3) 그래프 위에서 우클릭 → [그래프 변경] → [논리 프로세서]
|
||||
→ 코어(스레드)별 그래프가 바둑판처럼 쫙!
|
||||
유튜브를 틀거나 npm run build를 돌리면서
|
||||
어떤 칸이 바빠지는지 지켜보세요.`;
|
||||
|
||||
const CODE_BUILD = `개발자의 하루와 CPU — 빌드 시간으로 체감하기
|
||||
|
||||
우리 스택에서 CPU를 갈아 넣는 순간들:
|
||||
· npm run build React 프론트 번들링 (수십 초~수 분)
|
||||
· ./mvnw package Spring Boot 컴파일+패키징
|
||||
· docker build 이미지 빌드 (레이어마다 컴파일·압축)
|
||||
· 테스트 전체 실행 케이스 수백 개 병렬 처리
|
||||
|
||||
간단 계산: 빌드 3분짜리를 하루 20번 돌리면?
|
||||
3분 × 20회 = 하루 60분이 '기다림'
|
||||
CPU 업그레이드로 빌드가 1분이 되면 → 하루 40분 회수!
|
||||
|
||||
그래서 회사가 개발 장비에 투자하는 건 사치가 아니라 계산이에요.
|
||||
(참고: 우리 EC2 서울 서버도 vCPU 수에 따라 빌드·배포 속도가 달라집니다.)`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '연산의 원리' },
|
||||
{ n: 2, label: '코어·스레드·클럭' },
|
||||
{ n: 3, label: '캐시 L1~L3' },
|
||||
{ n: 4, label: '발열과 스로틀링' },
|
||||
{ n: 5, label: '이름 해독하기' },
|
||||
{ n: 6, label: '작업 관리자 실습' },
|
||||
{ n: 7, label: '개발자와 CPU' },
|
||||
];
|
||||
|
||||
export default function CpuDeepPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>CPU 깊이 보기<br />— 명령어 한 줄에서 빌드 시간까지</h1>
|
||||
<p>
|
||||
컴퓨터의 두뇌라 불리는 CPU가 실제로는 어떤 박자로 일하는지, 코어·스레드·캐시 같은
|
||||
스펙 용어가 무슨 뜻인지 주방 비유 하나로 꿰어 봅니다. 마지막엔{' '}
|
||||
<strong>내 PC의 작업 관리자</strong>로 오늘 배운 걸 전부 눈으로 확인해요 —
|
||||
스펙표를 읽을 줄 아는 개발자가 되는 게 이 코스의 목표입니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 (내 PC로 바로)</span>
|
||||
<span className="chip">준비물: Windows PC 한 대</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="연산의 원리 — 명령어 사이클" sub="가져오고, 알아듣고, 실행한다. 그게 전부">
|
||||
<p>
|
||||
CPU는 마법의 상자가 아니에요. 하는 일은 딱 하나 —{' '}
|
||||
<strong>메모리에서 명령어를 가져와서(Fetch), 무슨 뜻인지 해석하고(Decode),
|
||||
실행한다(Execute)</strong>. 이 세 박자를 무한 반복할 뿐입니다.
|
||||
</p>
|
||||
<Code>{CODE_CYCLE}</Code>
|
||||
<p>
|
||||
요리사가 레시피를 <strong>한 줄 읽고 → 무슨 뜻인지 파악하고 → 칼질하는</strong> 것과
|
||||
똑같아요. 다른 점은 속도뿐 — CPU는 이 반복을 1초에 수십억 번 합니다.
|
||||
그래서 명령어 하나하나는 "두 숫자를 더해라" 수준으로 단순해도, 쌓이면
|
||||
게임이 되고, 우리 학습 플랫폼이 되고, AI가 되는 거예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>미리 연결해 두기</b> 여러분이 React나 Spring Boot로 쓰는 코드도 결국 이 단순한
|
||||
명령어 수백만 개로 번역돼요. "내 코드 한 줄 = 기계 명령어 여러 개"라는 감각을
|
||||
지금 심어 두면, 나중에 성능 이야기가 나올 때 훨씬 쉽게 들립니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="코어 · 스레드 · 클럭" sub="요리사 몇 명이, 몇 박자로 일하는가">
|
||||
<p>
|
||||
CPU 스펙표의 용어들은 <strong>주방</strong>으로 바꿔 읽으면 한 번에 이해돼요.
|
||||
CPU라는 주방 안에 요리사(<strong>코어</strong>)가 여러 명 있고, 각자 일정한
|
||||
박자(<strong>클럭</strong>)로 손을 놀립니다.
|
||||
</p>
|
||||
<Code>{CODE_KITCHEN}</Code>
|
||||
<p>
|
||||
<strong>스레드</strong>가 헷갈리기 쉬운데, "요리사 한 명이 파스타 면을 삶는{' '}
|
||||
<strong>대기 시간</strong>에 옆 화구의 소스를 젓는" 기술이에요. 몸(코어)은 하나지만
|
||||
일감 줄(스레드)을 두 개 받아서 빈틈을 메우는 거죠. 그래서 8코어 16스레드는
|
||||
"요리사 8명인데 주문 창구는 16개"인 셈 — 진짜 16명만큼은 아니어도 8명보다는
|
||||
확실히 많은 일을 해냅니다.
|
||||
</p>
|
||||
<p>
|
||||
<strong>클럭(GHz)</strong>은 손 놀리는 속도예요. 4.5GHz면 1초에 45억 박자.
|
||||
다만 <strong>"클럭 높음 = 무조건 빠름"은 아닙니다</strong>. 박자당 해내는 일의 양(같은
|
||||
한 박자에 칼질을 한 번 하느냐 두 번 하느냐)이 세대마다 다르거든요 — 이 함정은
|
||||
섹션 5에서 다시 만나요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="캐시 — 도마 옆 재료 선반" sub="L1, L2, L3… 가까울수록 작고 빠르다">
|
||||
<p>
|
||||
요리사가 재료 하나 꺼내러 매번 <strong>복도 끝 냉장고(RAM)</strong>까지 달려가면
|
||||
요리가 한없이 느려지겠죠. 그래서 자주 쓰는 재료를 <strong>도마 옆 선반</strong>에
|
||||
미리 올려 둡니다 — 이게 <strong>캐시</strong>예요. 그리고 이 선반이 3층으로 나뉘어 있죠.
|
||||
</p>
|
||||
<Code>{CODE_CACHE}</Code>
|
||||
<p>
|
||||
층수 규칙은 하나예요. <strong>코어에서 가까울수록 작고 빠르고, 멀수록 크고 느리다.</strong>{' '}
|
||||
L1은 손 안이라 순식간이지만 몇 줌밖에 못 담고, L3는 주방 전체가 나눠 쓰는 큰 선반이라
|
||||
넉넉하지만 몇 걸음 걸어가야 해요. CPU는 필요한 데이터를 L1 → L2 → L3 → RAM 순서로
|
||||
찾아보는데, 가까운 선반에서 바로 찾으면(<strong>캐시 히트</strong>) 빠르고, 없어서
|
||||
냉장고까지 가면(<strong>캐시 미스</strong>) 수십~수백 배 느려집니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>스펙표의 숨은 실력자</b> — 같은 코어 수·클럭이라도 L3 캐시가 큰 CPU가 게임이나
|
||||
빌드에서 눈에 띄게 빠른 경우가 많아요. 코어 수와 클럭만 보고 CPU를 고르면
|
||||
절반만 보는 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="발열과 쿨링, 그리고 스로틀링" sub="빨리 달릴수록 뜨거워지고, 뜨거우면 스스로 감속한다">
|
||||
<p>
|
||||
1초에 수십억 번 스위치를 켰다 끄면 당연히 <strong>열</strong>이 나요. 문제는 CPU가
|
||||
열에 약하다는 것 — 그래서 모든 PC에는 <strong>쿨링</strong>(방열판+팬, 고성능 PC는
|
||||
수랭)이 붙어 있고, CPU 자신도 비장의 자기 보호 기능을 갖고 있습니다.
|
||||
바로 <strong>스로틀링</strong>이에요.
|
||||
</p>
|
||||
<Code>{CODE_THROTTLE}</Code>
|
||||
<p>
|
||||
마라톤 선수가 전력 질주로 시작했다가 몸에 무리가 오면 <strong>스스로 페이스를
|
||||
낮추는</strong> 것과 같아요. 고장이 아니라 타 버리는 걸 막는 정상 동작입니다.
|
||||
노트북이 얇을수록 쿨링 공간이 좁아 스로틀링이 빨리 오고, 그래서{' '}
|
||||
<strong>같은 CPU라도 데스크톱과 얇은 노트북의 실제 성능이 다른</strong> 거예요.
|
||||
여름철 게임이 유독 버벅이는 것도, 노트북을 이불 위에 올려 두면(통풍구 막힘) 느려지는
|
||||
것도 전부 이 친구 소행이고요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>개발자의 습관</b> 빌드가 평소보다 이상하게 느리면 코드 탓만 하지 말고
|
||||
팬 소리·기기 온도부터 체크해 보세요. 통풍구를 확보하는 것만으로 빌드 시간이
|
||||
돌아오는 경우가 실제로 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="인텔 vs 라이젠 — 이름 해독하기" sub="i7-14700K, Ryzen 7 7700X… 암호가 아니라 문법이에요">
|
||||
<p>
|
||||
CPU 시장은 크게 <strong>인텔(Core 시리즈)</strong>과{' '}
|
||||
<strong>AMD(Ryzen 시리즈)</strong> 두 진영이에요. 제품 이름이 암호처럼 보이지만,
|
||||
사실 <strong>등급(체급) + 세대(연식) + 접미사(옵션)</strong>라는 똑같은 문법으로
|
||||
만들어져 있어서 한 번 배우면 평생 읽습니다.
|
||||
</p>
|
||||
<Code>{CODE_NAMING}</Code>
|
||||
<p>
|
||||
중고차 고르기와 똑같아요. "그랜저(체급)냐 아반떼냐"만큼{' '}
|
||||
<strong>"2018년식이냐 2024년식이냐(세대)"</strong>가 중요하죠. 최신 연식의 중형차가
|
||||
10년 된 대형차보다 잘 달리는 것처럼, <strong>최신 세대 i5가 구형 i7을 이기는 건
|
||||
아주 흔한 일</strong>입니다. 친구가 "i7인데 왜 느리지?"라고 하면 이제 여러분이
|
||||
세대를 물어봐 줄 수 있어요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="직접 확인해 보기 ① — 작업 관리자로 내 CPU 관찰" sub="지금까지 배운 용어를 전부 내 PC에서 찾아보기">
|
||||
<p>
|
||||
이론은 끝! 이제 <strong>내 PC의 CPU</strong>를 직접 들여다볼 차례예요.
|
||||
Windows 작업 관리자에는 오늘 배운 코어·스레드·클럭·캐시가 전부 표시됩니다.
|
||||
</p>
|
||||
<Code>{CODE_TASKMGR}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> <span className="kbd">Ctrl</span> +{' '}
|
||||
<span className="kbd">Shift</span> + <span className="kbd">Esc</span>로 작업 관리자를
|
||||
열고, 위 순서대로 따라가며 다음 세 가지를 노트에 적어 보세요.
|
||||
</div>
|
||||
<ol className="olist">
|
||||
<li>내 CPU의 정확한 이름 — 그리고 섹션 5의 문법으로 <strong>체급과 세대를 해독</strong>해 보기</li>
|
||||
<li>코어 수와 논리 프로세서(스레드) 수 — 우리 주방의 요리사는 몇 명?</li>
|
||||
<li>L1/L2/L3 캐시 크기 — 정말 층수가 올라갈수록 커지는지 확인</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>한 걸음 더</b> 그래프를 <strong>논리 프로세서</strong> 보기로 바꾼 뒤, 유튜브 영상을
|
||||
하나 틀어 보세요. 어떤 칸(코어)이 바빠지나요? 모든 코어가 조금씩 나눠 일하나요,
|
||||
한두 코어만 집중적으로 일하나요? 이 관찰이 다음 섹션의 복선입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="개발자에게 CPU가 중요한 순간" sub="직접 확인해 보기 ② — 빌드 시간으로 체감하기">
|
||||
<p>
|
||||
"나는 게임 안 하는데 CPU가 왜 중요해요?" — 개발자에겐{' '}
|
||||
<strong>빌드</strong>가 게임이에요. 우리 스택(React·Spring Boot·Docker)의 빌드는
|
||||
코드 수천 개 파일을 컴파일하고 압축하는 <strong>CPU 총력전</strong>이거든요.
|
||||
</p>
|
||||
<Code>{CODE_BUILD}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 플랫폼 프론트엔드 저장소(
|
||||
<span className="icode">mirim-app/frontend</span>)를 받아 둔 PC에서, 작업 관리자의
|
||||
CPU 그래프(논리 프로세서 보기)를 <strong>옆에 띄워 놓고</strong> 터미널에{' '}
|
||||
<span className="icode">npm run build</span>를 실행해 보세요. 조용하던 코어들이
|
||||
일제히 100%로 치솟는 장관을 볼 수 있어요. 걸린 시간도 기록해 두세요 — 나중에
|
||||
장비가 바뀌면 비교해 보는 재미가 있습니다. (Gitea{' '}
|
||||
<span className="icode">edu.awesomedevapp.com</span>에서 클론할 수 있어요.)
|
||||
</div>
|
||||
<p>
|
||||
하나 더 — 빌드 도중 그래프를 보면 <strong>모든 코어가 다 바쁜 구간</strong>과{' '}
|
||||
<strong>한 코어만 일하는 구간</strong>이 번갈아 나타나요. 병렬로 쪼갤 수 있는 일과
|
||||
순서대로만 해야 하는 일이 섞여 있기 때문이죠. "코어가 많다고 모든 게 코어 수만큼
|
||||
빨라지진 않는다"는 사실을 그래프 하나로 체감하게 됩니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>서버도 마찬가지</b> — 우리 서비스가 도는 AWS EC2(서울)도 인스턴스 등급에 따라
|
||||
vCPU 수가 달라요. 배포 빌드가 느리다면 코드 문제일 수도 있지만, 서버의 요리사 수가
|
||||
부족한 걸 수도 있습니다. 스펙을 읽을 줄 알면 원인을 나눠서 볼 수 있어요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🧠 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 CPU 스펙표가 암호가 아니라 문장으로 읽혀요 — 명령어 사이클이라는 심장 박동,
|
||||
코어·스레드·클럭·캐시라는 주방의 구조, 스로틀링이라는 자기 보호, 그리고 이름
|
||||
해독법까지. 다음은 CPU가 재료를 가져오는 <strong>냉장고(메모리)와 창고(저장장치)</strong>
|
||||
차례예요 — <Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스와
|
||||
함께 들으면 "내 코드가 실행되는 무대" 전체가 그려집니다. 실습 ①·②에서 기록한
|
||||
숫자들은 멘토와의 주간 리뷰 때 가져와서 같이 해석해 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
431
frontend/src/pages/courses/DataModelingPage.jsx
Normal file
431
frontend/src/pages/courses/DataModelingPage.jsx
Normal file
@ -0,0 +1,431 @@
|
||||
// 이 파일이 하는 일: "데이터 모델링 기초" 코스 — 테이블 설계를 '정리정돈'에 비유해
|
||||
// ERD 읽는 법, 기본키/외래키, 1:N·N:M 관계, 정규화, 나쁜 설계의 냄새까지
|
||||
// 7개 섹션으로 안내하는 정적 학습 페이지. 마지막은 "쪽지 기능" 설계 실습으로 마무리.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_MESSY_DESK = `설계 없이 데이터를 쌓으면 이런 일이 벌어져요:
|
||||
|
||||
메모장.txt
|
||||
─────────────────────────────────────────────
|
||||
김수습, 3반, 과제1 제출함, 점수 85
|
||||
이수습 / 3반 / 과제1 아직 / 멘토가 박멘토님
|
||||
김수습 과제2도 냈음 (점수는 나중에)
|
||||
박수습(2반?) 과제1 90점 — 근데 위에 김수습이랑 같은 반 맞나?
|
||||
|
||||
→ "3반 학생 중 과제1 안 낸 사람?" 같은 간단한 질문에도
|
||||
파일 전체를 눈으로 뒤져야 해요. 데이터가 1만 줄이면?
|
||||
|
||||
잘 정리된 테이블
|
||||
─────────────────────────────────────────────
|
||||
users(사용자) : 누가 있는지 한 번만 기록
|
||||
assignment(과제) : 어떤 과제가 있는지 한 번만 기록
|
||||
submission(제출) : "누가 어떤 과제를 냈다"는 사실만 기록
|
||||
|
||||
→ 같은 질문을 SQL 한 줄로 즉시 답할 수 있어요.`;
|
||||
|
||||
const CODE_ERD_OURS = `우리 학습 플랫폼의 핵심 ERD (관계도)
|
||||
|
||||
┌──────────────┐ ┌──────────────────┐
|
||||
│ users │ │ assignment │
|
||||
├──────────────┤ ├──────────────────┤
|
||||
│ id (PK) │ │ id (PK) │
|
||||
│ name │ │ title │
|
||||
│ email │ │ due_date │
|
||||
│ role │ │ description │
|
||||
└──────┬───────┘ └────────┬─────────┘
|
||||
│ 1 │ 1
|
||||
│ │
|
||||
│ N │ N
|
||||
└───────►┌──────────────────┐◄──────┘
|
||||
│ submission │
|
||||
├──────────────────┤
|
||||
│ id (PK) │
|
||||
│ user_id (FK) │ → users.id 를 가리킴
|
||||
│ assignment_id(FK)│ → assignment.id 를 가리킴
|
||||
│ content │
|
||||
│ submitted_at │
|
||||
│ score │
|
||||
└──────────────────┘
|
||||
|
||||
읽는 법: 선의 양 끝에 붙은 숫자가 관계예요.
|
||||
"users 1명은 submission을 N개 가질 수 있다" (한 학생이 여러 번 제출)
|
||||
"assignment 1개에 submission이 N개 달릴 수 있다" (한 과제에 여러 명이 제출)`;
|
||||
|
||||
const CODE_PK_FK = `기본키(PK)와 외래키(FK) — 학번과 "학번으로 가리키기"
|
||||
|
||||
users 테이블 submission 테이블
|
||||
────────────────────── ─────────────────────────────
|
||||
id │ name │ email id │ user_id │ content
|
||||
───┼───────┼────────── ────┼─────────┼───────────────
|
||||
1 │ 김수습 │ kim@... 10 │ 1 │ "과제1 제출.."
|
||||
2 │ 이수습 │ lee@... 11 │ 2 │ "저도 냈어요"
|
||||
3 │ 박수습 │ park@.. 12 │ 1 │ "과제2 제출.."
|
||||
↑ ↑
|
||||
PK: 이 테이블 안에서 FK: 다른 테이블의 PK를
|
||||
절대 겹치지 않는 번호 "가리키는" 번호
|
||||
|
||||
submission에 이름 대신 user_id를 적는 이유:
|
||||
- 김수습이 개명해도 users 한 곳만 고치면 끝
|
||||
- 동명이인이 와도 번호가 다르니 절대 안 섞임
|
||||
- 이름 오타("김수습" vs "김 수습")로 데이터가 갈라질 일이 없음`;
|
||||
|
||||
const CODE_PSQL_PEEK = `# 우리 개발 DB에 직접 들어가서 진짜 PK/FK를 눈으로 확인하기
|
||||
# (로컬에서 Docker로 PostgreSQL을 띄워 뒀다면)
|
||||
|
||||
docker exec -it <postgres-컨테이너이름> psql -U <사용자명> -d <DB이름>
|
||||
|
||||
\\dt -- 테이블 목록 보기
|
||||
\\d submission -- submission 테이블의 구조 보기
|
||||
|
||||
# 출력에서 이런 줄을 찾아보세요:
|
||||
Indexes:
|
||||
"submission_pkey" PRIMARY KEY (id) ← 기본키!
|
||||
Foreign-key constraints:
|
||||
"..._user_id_fkey" FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ← 외래키!
|
||||
|
||||
# 나올 때는 \\q`;
|
||||
|
||||
const CODE_ONE_TO_N = `1:N — "하나가 여럿을 거느린다"
|
||||
|
||||
멘토 1명 ──── 수습 N명 (멘토 쪽엔 아무것도 안 적고,
|
||||
과제 1개 ──── 제출 N개 '여럿' 쪽에 FK 한 칸만 추가!)
|
||||
|
||||
users
|
||||
────────────────────────────
|
||||
id │ name │ role │ mentor_id (FK, 자기 테이블을 가리켜도 OK!)
|
||||
───┼───────┼───────┼──────────
|
||||
1 │ 박멘토 │ MENTOR│ (없음)
|
||||
2 │ 김수습 │ INTERN│ 1 ← 박멘토가 담당
|
||||
3 │ 이수습 │ INTERN│ 1 ← 박멘토가 담당
|
||||
|
||||
규칙: 1:N에서는 항상 N쪽(여럿 쪽)에 FK를 둔다.
|
||||
멘토 테이블에 "담당수습1, 담당수습2, 담당수습3..." 칸을
|
||||
늘려 가는 순간 설계가 무너집니다. (섹션 6에서 다시 만나요)`;
|
||||
|
||||
const CODE_N_TO_M = `N:M — "여럿과 여럿"은 중간 테이블이 필요해요
|
||||
|
||||
학생 여러 명 ↔ 스터디 여러 개
|
||||
(한 학생이 여러 스터디에 들고, 한 스터디에 여러 학생이 든다)
|
||||
|
||||
FK 한 칸으로는 표현 불가! → 사이에 '다리' 테이블을 놓습니다.
|
||||
|
||||
users study_member(중간 테이블) study
|
||||
────────── ────────────────────── ──────────
|
||||
id │ name id │ user_id │ study_id id │ name
|
||||
───┼────── ───┼─────────┼───────── ───┼──────
|
||||
2 │ 김수습 1 │ 2 │ 1 1 │ React반
|
||||
3 │ 이수습 2 │ 2 │ 2 2 │ DB반
|
||||
3 │ 3 │ 1
|
||||
|
||||
읽는 법: study_member의 한 줄 = "가입했다"는 사실 하나.
|
||||
김수습(2)은 React반(1)과 DB반(2) 둘 다,
|
||||
이수습(3)은 React반(1)만.
|
||||
|
||||
사실 우리 submission도 정확히 이 패턴이에요!
|
||||
users ↔ assignment 라는 N:M 관계의 중간 테이블이
|
||||
submission이고, 거기에 content·score 같은
|
||||
'관계 자체의 정보'가 얹혀 있는 것뿐입니다.`;
|
||||
|
||||
const CODE_NORMALIZE = `정규화 = "같은 정보를 두 번 적지 않기" 대작전
|
||||
|
||||
나쁜 예 — 제출 테이블에 전부 때려 넣기:
|
||||
──────────────────────────────────────────────────
|
||||
제출id │ 학생이름 │ 학생이메일 │ 과제제목 │ 과제마감일
|
||||
──────┼─────────┼───────────┼───────────┼──────────
|
||||
10 │ 김수습 │ kim@... │ 자기소개 API│ 07-20
|
||||
11 │ 이수습 │ lee@... │ 자기소개 API│ 07-20
|
||||
12 │ 김수습 │ kim@... │ ERD 그리기 │ 07-27
|
||||
|
||||
문제 1: 김수습의 이메일이 두 줄에 중복 → 한 줄만 고치면 데이터 모순
|
||||
문제 2: 과제 마감일이 바뀌면? 그 과제의 제출 줄 전부를 찾아 고쳐야 함
|
||||
문제 3: 아직 아무도 안 낸 새 과제는... 적을 곳이 없음!
|
||||
|
||||
정규화 후: users / assignment / submission 세 테이블로 분리
|
||||
→ 각 사실은 딱 한 곳에만. 고칠 일이 생기면 한 줄만 고침.
|
||||
|
||||
교과서의 1·2·3차 정규형, 직관 버전:
|
||||
1차: 한 칸에는 값 하나만 (칸 안에 목록 금지)
|
||||
2차: 그 줄의 '주인공(PK)'과 상관없는 정보는 딴 데로
|
||||
3차: 다른 칸을 통해 알 수 있는 정보는 또 적지 말기
|
||||
(과제제목을 알면 마감일은 assignment에서 찾으면 됨)`;
|
||||
|
||||
const CODE_BAD_SMELLS = `나쁜 설계의 냄새 3가지 — 보이면 일단 의심!
|
||||
|
||||
냄새 1: 한 칸에 여러 값
|
||||
tags = "react,db,docker" ← 쉼표로 구겨 넣기
|
||||
→ "docker 태그 단 글 검색"이 지옥이 됨. 별도 테이블로!
|
||||
|
||||
냄새 2: 번호 붙은 칸의 행진
|
||||
phone1, phone2, phone3 ← 4번째 전화번호는?
|
||||
담당수습1, 담당수습2, ... ← 1:N을 억지로 가로로 편 것
|
||||
→ 세로로(줄을 늘려서) 담아야 할 걸 가로로 담은 신호
|
||||
|
||||
냄새 3: 복붙된 정보
|
||||
모든 제출 줄마다 학생 이메일이 통째로 복사돼 있다
|
||||
→ 정규화 필요 신호 (섹션 5의 문제 그대로)
|
||||
|
||||
공통 처방: "이 정보의 주인 테이블은 어디지?"를 묻고,
|
||||
주인 테이블에 한 번만 적은 뒤 FK로 가리키기.`;
|
||||
|
||||
const CODE_PRACTICE = `실습 미션: "쪽지 기능" 테이블 설계하기
|
||||
|
||||
요구사항 (우리 플랫폼에 진짜 있으면 좋을 기능이죠?)
|
||||
- 사용자가 다른 사용자에게 쪽지를 보낼 수 있다
|
||||
- 쪽지에는 제목·내용·보낸 시각이 있다
|
||||
- 받는 사람은 읽음/안읽음을 구분할 수 있다
|
||||
- 나중에 "내가 보낸 쪽지함 / 받은 쪽지함"을 만들 예정
|
||||
|
||||
스스로에게 던질 질문 (설계 순서 그대로):
|
||||
1. 어떤 '사실'들을 저장해야 하지? → 명사를 뽑아 보기
|
||||
2. 테이블은 몇 개 필요할까? 새로 만들 건 뭐고,
|
||||
이미 있는 users를 어떻게 재사용할까?
|
||||
3. 보낸 사람과 받는 사람 — FK가 몇 개 필요하지?
|
||||
둘 다 users를 가리켜도 되나? (힌트: 됩니다!)
|
||||
4. 읽음 여부는 어느 테이블의 어느 칸에?
|
||||
5. 관계는 1:N일까 N:M일까? 한 쪽지를 여러 명에게
|
||||
보내는 '단체 쪽지'까지 지원한다면 뭐가 달라질까?
|
||||
|
||||
제출물: 종이나 그림판에 그린 ERD 한 장
|
||||
(섹션 2의 그림처럼 — 테이블 상자, PK/FK 표시, 관계선과 1/N)`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '설계 = 정리정돈' },
|
||||
{ n: 2, label: 'ERD 읽기' },
|
||||
{ n: 3, label: '기본키·외래키' },
|
||||
{ n: 4, label: '1:N과 N:M' },
|
||||
{ n: 5, label: '정규화' },
|
||||
{ n: 6, label: '나쁜 설계의 냄새' },
|
||||
{ n: 7, label: '실습: 쪽지 기능' },
|
||||
];
|
||||
|
||||
export default function DataModelingPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>데이터 모델링 기초<br />— 테이블을 잘 짜면 코드가 편해진다</h1>
|
||||
<p>
|
||||
데이터베이스 테이블 설계는 이사 갈 집의 <strong>수납장을 미리 짜는 일</strong>이에요.
|
||||
지금 여러분이 쓰는 이 학습 플랫폼의 진짜 테이블(users·assignment·submission)을
|
||||
교재 삼아, 관계도를 읽고 직접 설계까지 해 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 70분</span>
|
||||
<span className="chip">직접 확인해 보기 3개</span>
|
||||
<span className="chip">준비물: 종이와 펜</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="테이블 설계 = 정리정돈" sub="옷장을 안 나누면 매일 아침이 지옥이 되듯이">
|
||||
<p>
|
||||
방 정리를 떠올려 보세요. 양말·티셔츠·교과서를 전부 한 서랍에 던져 넣으면
|
||||
넣을 때는 편하지만, <strong>찾을 때마다</strong> 서랍 전체를 뒤집어야 하죠.
|
||||
데이터도 똑같아요. 설계 없이 쌓은 데이터는 넣기는 쉬워도 꺼내기가 고통입니다.
|
||||
</p>
|
||||
<Code>{CODE_MESSY_DESK}</Code>
|
||||
<p>
|
||||
<strong>데이터 모델링</strong>은 데이터를 넣기 전에 "어떤 서랍(테이블)을 만들고,
|
||||
각 서랍에 어떤 칸(컬럼)을 둘까"를 정하는 일이에요. 우리 플랫폼의 백엔드
|
||||
(<strong>Spring Boot</strong>)가 다루는 모든 데이터도 <strong>PostgreSQL</strong>의
|
||||
테이블 설계 위에 서 있습니다. 설계가 좋으면 코드가 짧아지고, 설계가 나쁘면
|
||||
코드가 그 나쁨을 평생 뒤치다꺼리하게 돼요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>미리 보는 핵심 문장</b> 이 코스 전체를 한 줄로 줄이면 이거예요 —
|
||||
<strong> "같은 사실은 딱 한 곳에만 적고, 필요한 곳에서는 번호(FK)로 가리킨다."</strong>
|
||||
섹션 7까지 가면 이 문장이 완전히 내 것이 됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="ERD 읽고 그리기" sub="우리 플랫폼의 진짜 관계도로 배우기">
|
||||
<p>
|
||||
<strong>ERD</strong>(Entity-Relationship Diagram)는 테이블들의 <strong>지도</strong>예요.
|
||||
상자 하나가 테이블 하나, 상자를 잇는 선이 <strong>관계</strong>입니다.
|
||||
멀리 갈 것 없이, 지금 여러분이 과제를 제출할 때마다 실제로 움직이는
|
||||
우리 플랫폼의 핵심 세 테이블을 볼게요.
|
||||
</p>
|
||||
<Code>{CODE_ERD_OURS}</Code>
|
||||
<p>
|
||||
ERD를 읽는 순서는 이래요. <strong>① 상자 이름</strong>부터 —
|
||||
"아, users랑 assignment랑 submission이 있구나."
|
||||
<strong> ② 선의 방향과 숫자</strong> — "학생 하나에 제출이 여러 개 달리는구나."
|
||||
<strong> ③ FK 칸</strong> — "submission이 user_id로 users를 가리키는구나."
|
||||
이 세 단계면 처음 보는 프로젝트의 DB도 30분 안에 파악할 수 있어요.
|
||||
실제로 실무 개발자가 새 팀에 합류하면 <strong>제일 먼저 찾는 문서가 ERD</strong>입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 종이에 위 ERD를 <strong>안 보고</strong> 다시 그려 보세요.
|
||||
상자 3개, PK/FK 표시, 관계선의 1과 N까지. 그리고 원본과 비교 —
|
||||
빠뜨린 게 있다면 그게 아직 이해가 덜 된 부분이에요. 지도는 그려 봐야 외워집니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="기본키(PK)와 외래키(FK)" sub="학번, 그리고 '학번으로 가리키기'">
|
||||
<p>
|
||||
<strong>기본키(Primary Key)</strong>는 학번이에요. 이름은 동명이인이 있을 수 있지만,
|
||||
학번은 학교 안에서 절대 겹치지 않죠. 테이블의 모든 줄(행)은 PK 하나로
|
||||
유일하게 찍어낼 수 있어야 합니다. 보통 <span className="icode">id</span>라는
|
||||
자동 증가 숫자를 씁니다.
|
||||
</p>
|
||||
<p>
|
||||
<strong>외래키(Foreign Key)</strong>는 "다른 반 친구를 부를 때 학번으로 부르기"예요.
|
||||
submission 테이블이 학생 정보를 통째로 복사해 오는 대신,
|
||||
<span className="icode">user_id</span> 한 칸에 <strong>users의 학번(PK)만</strong> 적어 두는 거죠.
|
||||
</p>
|
||||
<Code>{CODE_PK_FK}</Code>
|
||||
<div className="warn">
|
||||
<b>이메일을 PK로 쓰면 안 될까요?</b> 유일하긴 하죠. 하지만 이메일은
|
||||
<strong> 바뀔 수 있는 값</strong>이에요. PK가 바뀌면 그걸 가리키던 모든 FK가
|
||||
함께 흔들립니다. 그래서 PK는 의미 없는 번호(<span className="icode">id</span>)로 두고,
|
||||
이메일 같은 건 "중복 금지" 제약만 거는 게 정석이에요. 학번을 두고
|
||||
"이름+생일"로 학생을 식별하지 않는 것과 같은 이유입니다.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 로컬에서 Docker로 개발 DB를 띄워 뒀다면,
|
||||
psql로 접속해 우리 테이블의 PK/FK를 눈으로 직접 확인할 수 있어요.
|
||||
컨테이너 이름은 <span className="icode">docker ps</span>로 먼저 찾으세요.
|
||||
</div>
|
||||
<Code>{CODE_PSQL_PEEK}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="1:N과 N:M" sub="관계의 두 가지 얼굴, 그리고 중간 테이블이라는 다리">
|
||||
<p>
|
||||
테이블 사이의 관계는 대부분 두 종류로 정리돼요. 먼저 <strong>1:N</strong> —
|
||||
"하나가 여럿을 거느리는" 관계입니다. 멘토 한 명이 수습 여러 명을 담당하고,
|
||||
과제 하나에 제출이 여러 개 달리죠.
|
||||
</p>
|
||||
<Code>{CODE_ONE_TO_N}</Code>
|
||||
<p>
|
||||
그런데 <strong>양쪽 다 여럿</strong>이면 어떡할까요? 한 학생이 여러 스터디에 들고,
|
||||
한 스터디에도 여러 학생이 드는 <strong>N:M</strong> 관계요. 이때는 FK 한 칸으로는
|
||||
부족해서, 두 테이블 사이에 <strong>다리(중간 테이블)</strong>를 놓습니다.
|
||||
</p>
|
||||
<Code>{CODE_N_TO_M}</Code>
|
||||
<p>
|
||||
마지막 줄이 이 섹션의 반전 포인트예요 — 섹션 2에서 본 <strong>submission이
|
||||
바로 중간 테이블</strong>이었다는 것. "학생과 과제 사이의 N:M 다리"에
|
||||
제출 내용과 점수라는 정보가 얹힌 형태죠. N:M을 발견하면
|
||||
"중간 테이블 + 거기에 얹을 정보가 있나?"를 묻는 습관을 들이세요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="정규화 — 중복 제거 대작전" sub="같은 사실을 두 번 적으면 언젠가 모순이 생긴다">
|
||||
<p>
|
||||
<strong>정규화</strong>(Normalization)라는 무서운 단어의 정체는 사실 단순해요 —
|
||||
<strong> "같은 정보를 여러 곳에 복사해 두지 말자"</strong>입니다.
|
||||
연락처를 폰에도 적고 다이어리에도 적고 포스트잇에도 적으면,
|
||||
친구가 번호를 바꿨을 때 셋 중 하나는 꼭 옛날 번호로 남죠. 그게 데이터 모순이에요.
|
||||
</p>
|
||||
<Code>{CODE_NORMALIZE}</Code>
|
||||
<p>
|
||||
1·2·3차 정규형의 엄밀한 정의는 나중에 필요할 때 찾아봐도 충분해요.
|
||||
지금 단계에서는 위의 <strong>직관 버전 세 줄</strong>과,
|
||||
"중복이 보이면 테이블을 쪼개고 FK로 가리킨다"는 감각이면 실무 설계의 90%를 커버합니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>그럼 무조건 잘게 쪼갤수록 좋은가요?</b> 아니에요. 너무 쪼개면 데이터를
|
||||
읽을 때마다 여러 테이블을 이어 붙여야(JOIN) 해서 느려질 수 있어요.
|
||||
그래서 실무에서는 일부러 약간의 중복을 허용하기도 합니다(비정규화).
|
||||
다만 그건 <strong>정규화를 할 줄 아는 사람이 이유를 갖고 어기는 것</strong> —
|
||||
기본기는 언제나 "중복 없이"입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="나쁜 설계의 냄새" sub="코드 리뷰처럼, 설계에도 '냄새'가 있다">
|
||||
<p>
|
||||
경험 많은 개발자는 테이블 구조만 슥 봐도 "여기 냄새 나는데?" 하고 알아챕니다.
|
||||
대표적인 냄새 세 가지를 익혀 두면, 여러분도 설계 리뷰에서 한마디 얹을 수 있어요.
|
||||
</p>
|
||||
<Code>{CODE_BAD_SMELLS}</Code>
|
||||
<p>
|
||||
특히 <strong>냄새 1(한 칸에 여러 값)</strong>이 제일 유혹적이에요.
|
||||
"태그쯤이야 쉼표로 이어 붙이면 되지" 싶지만, 그 순간부터 검색·수정·삭제가
|
||||
전부 문자열 곡예가 됩니다. 한 칸엔 값 하나 — 목록이 필요하면 줄을 늘리거나
|
||||
테이블을 새로 만드세요. 서랍 한 칸에 양말 뭉치를 테이프로 감아 넣지 않는 것처럼요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>연습 문제</b> 다음 설계의 냄새를 맡아 보세요 —
|
||||
<span className="icode">study(id, name, member_names)</span>에서
|
||||
<span className="icode">member_names = "김수습,이수습"</span>.
|
||||
냄새가 몇 번인지, 어떻게 고칠지 말로 설명해 보세요.
|
||||
(힌트: 섹션 4에서 정답 구조를 이미 봤어요!)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습: '쪽지 기능' 테이블 설계" sub="배운 걸 전부 꺼내 쓰는 진짜 설계 한 판">
|
||||
<p>
|
||||
이제 여러분 차례입니다. 우리 플랫폼에 <strong>쪽지 기능</strong>을 추가한다고
|
||||
가정하고, 필요한 테이블을 직접 설계해 보세요. 정답이 하나로 정해진 문제가
|
||||
아니에요 — <strong>왜 그렇게 설계했는지 설명할 수 있느냐</strong>가 핵심입니다.
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③ — 멘토 리뷰 받기</b> 완성한 ERD를 들고 멘토에게
|
||||
설계 리뷰를 요청하세요. 리뷰 때 이 세 가지를 <strong>내가 먼저</strong> 설명하면
|
||||
훨씬 깊은 피드백을 받을 수 있어요: ① 각 FK가 무엇을 가리키는지
|
||||
② 관계가 1:N인지 N:M인지와 그 근거 ③ 중복 저장한 정보가 없는지 스스로 점검한 결과.
|
||||
실무의 설계 리뷰도 정확히 이 순서로 흘러갑니다.
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>흔한 함정 미리보기</b> "보낸사람이름" 칸을 만들고 싶어지면 섹션 3을,
|
||||
"받는사람1, 받는사람2" 칸이 떠오르면 섹션 6의 냄새 2를 다시 읽어 보세요.
|
||||
단체 쪽지(한 쪽지 → 여러 수신자)는 N:M — 섹션 4의 중간 테이블이 답입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🗂️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 <strong>ERD를 읽고</strong>, PK/FK로 <strong>테이블을 잇고</strong>,
|
||||
중복을 보면 <strong>쪼개서 가리키는</strong> 사람이 됐어요. 설계한 테이블을
|
||||
실제로 만들고 데이터를 꺼내는 언어가 궁금하다면{' '}
|
||||
<Link to="/learn/sql"><strong>SQL 첫걸음</strong></Link> 코스로,
|
||||
그 테이블을 백엔드 코드와 연결하는 법이 궁금하다면{' '}
|
||||
<Link to="/learn/backend"><strong>Spring Boot 백엔드 입문</strong></Link>으로
|
||||
이어 가세요. 쪽지 기능 ERD는 멘토 리뷰 후 과제 게시판에 제출하는 것도 잊지 말고요!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
417
frontend/src/pages/courses/DataStructuresPage.jsx
Normal file
417
frontend/src/pages/courses/DataStructuresPage.jsx
Normal file
@ -0,0 +1,417 @@
|
||||
// 이 파일이 하는 일: "자료구조 맛보기" 코스 — 데이터를 담는 그릇(배열·스택·큐·해시맵·트리)이
|
||||
// 왜 여러 종류인지, 언제 뭘 꺼내 쓰는지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (왜 그릇이 여러 개인가 → 배열 → 스택·큐 → 해시맵 → 트리 → 시간복잡도 감각 → 속도 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 코드 예제는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 코드 예제 · 텍스트 다이어그램 상수들 ──
|
||||
|
||||
const CODE_KITCHEN = `부엌의 그릇들 = 자료구조
|
||||
|
||||
컵 → 하나만 담아서 바로 마심 (변수)
|
||||
쟁반 → 순서대로 나란히 놓음 (배열)
|
||||
접시 탑 → 맨 위에 쌓고, 맨 위부터 꺼냄 (스택)
|
||||
급식 줄 → 먼저 선 사람이 먼저 받음 (큐)
|
||||
사물함 → 번호(열쇠)만 알면 바로 열음 (해시맵)
|
||||
폴더 → 폴더 안에 폴더, 그 안에 파일 (트리)
|
||||
|
||||
→ 국을 쟁반에 담으면 흐르고, 밥을 컵에 담으면 좁죠.
|
||||
데이터도 똑같아요 — "무엇을 자주 하느냐"에 맞는 그릇이 따로 있습니다.`;
|
||||
|
||||
const CODE_ARRAY = `// 배열 = 번호표 붙은 쟁반. 순서가 생명!
|
||||
const interns = ['지민', '서연', '하준', '유나'];
|
||||
|
||||
interns[0]; // '지민' — 0번부터 시작! (첫 칸이 0인 게 국룰)
|
||||
interns.length; // 4
|
||||
interns.push('도윤'); // 맨 뒤에 추가 → ['지민','서연','하준','유나','도윤']
|
||||
|
||||
// 순서대로 전부 돌기 — 배열이 제일 잘하는 일
|
||||
for (const name of interns) {
|
||||
console.log(name + ' 출석!');
|
||||
}
|
||||
|
||||
// 그런데 '서연'이 몇 번째인지 모르면?
|
||||
interns.indexOf('서연'); // 앞에서부터 한 칸씩 다 뒤져봄 → 데이터가 많으면 느려요
|
||||
// 이 약점을 해시맵(섹션 4)이 해결합니다.`;
|
||||
|
||||
const CODE_STACK = `// 스택(Stack) = 접시 탑. LIFO — 나중에 쌓은 게 먼저 나감
|
||||
// 브라우저 '뒤로가기'가 정확히 이 구조예요.
|
||||
const history = []; // JS 배열로 스택 흉내내기
|
||||
|
||||
history.push('/learn'); // 학습 센터 방문
|
||||
history.push('/learn/network'); // 네트워크 코스 방문
|
||||
history.push('/learn/data-structures'); // 지금 이 페이지!
|
||||
|
||||
// [뒤로가기] 버튼 클릭 = pop (맨 위 접시를 꺼냄)
|
||||
history.pop(); // '/learn/data-structures' 가 빠지고
|
||||
history.at(-1); // '/learn/network' ← 지금 보이는 페이지
|
||||
|
||||
// 코드 에디터의 Ctrl+Z(실행 취소)도, 함수가 함수를 부르는
|
||||
// '콜 스택'도 전부 스택 — 최근 것부터 되감을 때 쓰는 그릇입니다.`;
|
||||
|
||||
const CODE_QUEUE = `// 큐(Queue) = 급식 줄. FIFO — 먼저 온 사람이 먼저 받음
|
||||
const lunchLine = [];
|
||||
|
||||
lunchLine.push('지민'); // 줄 맨 뒤에 섬 (enqueue)
|
||||
lunchLine.push('서연');
|
||||
lunchLine.push('하준');
|
||||
|
||||
lunchLine.shift(); // '지민' — 맨 앞사람부터 배식! (dequeue)
|
||||
lunchLine.shift(); // '서연'
|
||||
// 남은 줄: ['하준']
|
||||
|
||||
// 스택과 큐는 push는 같고 꺼내는 쪽만 달라요:
|
||||
// 스택: push + pop (뒤로 넣고 뒤에서 꺼냄) → 되감기
|
||||
// 큐: push + shift (뒤로 넣고 앞에서 꺼냄) → 순서 지키기
|
||||
// 프린터 인쇄 대기열, 서버의 요청 처리 줄이 전부 큐입니다.`;
|
||||
|
||||
const CODE_HASHMAP = `// 해시맵(Map) = 열쇠(key)로 한 번에 여는 사물함
|
||||
const lockers = new Map();
|
||||
|
||||
lockers.set('지민', '17번 사물함');
|
||||
lockers.set('서연', '42번 사물함');
|
||||
lockers.set('하준', '8번 사물함');
|
||||
|
||||
lockers.get('서연'); // '42번 사물함' — 한. 번. 에.
|
||||
lockers.has('유나'); // false — 없는 것도 한 번에 알아요
|
||||
|
||||
// 배열이었다면? 앞에서부터 "지민이야? 아니. 서연이야? 맞다!" 하나씩 확인.
|
||||
// 해시맵은 열쇠('서연')를 해시 함수라는 기계에 넣으면
|
||||
// "42번 칸으로 가!" 하고 위치가 바로 계산돼서 나와요.
|
||||
// 학번 뒷자리로 사물함 번호가 정해지는 규칙이 있다면,
|
||||
// 전교생 명단을 안 뒤져도 내 사물함을 바로 찾는 것과 같죠.
|
||||
|
||||
// 우리 스택에서도 어디에나 있어요:
|
||||
// PostgreSQL의 인덱스, 자바스크립트 객체 {}, HTTP 헤더... 전부 key → value.`;
|
||||
|
||||
const CODE_TREE = `트리 = 폴더 안에 폴더. 여러분이 매일 보는 구조예요.
|
||||
|
||||
우리 프론트엔드 프로젝트 폴더: 이 페이지의 DOM도 트리:
|
||||
frontend/ <div>
|
||||
├── src/ ├── <div class="hero">
|
||||
│ ├── pages/ │ ├── <h1>
|
||||
│ │ ├── NetworkPage.jsx │ └── <p>
|
||||
│ │ └── courses/ ├── <nav class="pill-nav">
|
||||
│ │ └── DataStructuresPage.jsx └── <section class="step-card">
|
||||
│ └── App.jsx └── ...
|
||||
└── package.json
|
||||
|
||||
JSON 응답도 트리예요:
|
||||
{ "course": { ← 부모
|
||||
"title": "자료구조 맛보기", ← 자식
|
||||
"sections": [ ... ] ← 자식 (그 안에 또 자식들)
|
||||
} }
|
||||
|
||||
공통점: 맨 위 뿌리(root) 하나 → 가지가 갈라짐 → 끝에 잎(leaf).
|
||||
"부모-자식 관계가 있는 데이터"는 전부 트리에 담깁니다.
|
||||
React가 화면을 그리는 것도 결국 컴포넌트 트리를 따라 내려가는 일이에요.`;
|
||||
|
||||
const CODE_BIGO = `시간복잡도 = "데이터가 10배 늘면 일도 몇 배 느나?"의 감각
|
||||
|
||||
O(1) — 데이터가 아무리 많아도 한 번에 끝
|
||||
사물함: 전교생이 100명이든 10만 명이든 내 열쇠로 한 번에 염
|
||||
예) map.get(key), arr[3]
|
||||
|
||||
O(n) — 데이터가 10배면 일도 10배
|
||||
출석부: 100명 부르면 100번, 10만 명이면 10만 번
|
||||
예) 배열 전체 순회, arr.indexOf(x)
|
||||
|
||||
O(n²) — 데이터가 10배면 일은 100배!
|
||||
반 전체 악수: 30명이 서로 다 악수하면 약 450번,
|
||||
300명이면 약 45,000번 — 사람 10배에 악수는 100배
|
||||
예) 반복문 안의 반복문 (모든 쌍 비교)
|
||||
|
||||
n = 100,000일 때 대략:
|
||||
O(1) = 1번 ← 눈 깜빡할 새
|
||||
O(n) = 100,000번 ← 그래도 순식간
|
||||
O(n²) = 100억 번 ← 브라우저가 멈춰요
|
||||
|
||||
→ "이 코드, 데이터가 10만 개여도 괜찮을까?"라고
|
||||
스스로 묻는 습관이 시간복잡도 감각의 시작입니다.`;
|
||||
|
||||
const CODE_RACE = `// ── 배열 vs Map 검색 속도 대결 ──
|
||||
// 브라우저 아무 페이지에서 F12 → Console 탭에 통째로 붙여넣고 Enter!
|
||||
|
||||
// 1) 선수 입장: 가짜 수강생 10만 명 등록
|
||||
const N = 100000;
|
||||
const arr = [];
|
||||
const map = new Map();
|
||||
for (let i = 0; i < N; i++) {
|
||||
const id = 'student-' + i;
|
||||
arr.push(id);
|
||||
map.set(id, i);
|
||||
}
|
||||
|
||||
// 2) 각자 1,000명씩 찾아보기 — 일부러 '뒤쪽' 학생을 찾아요 (배열에 최악)
|
||||
const targets = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
targets.push('student-' + (N - 1 - i));
|
||||
}
|
||||
|
||||
// 3) 배열 선수 (O(n) — 앞에서부터 하나씩 확인)
|
||||
console.time('배열 includes x1000');
|
||||
for (const t of targets) arr.includes(t);
|
||||
console.timeEnd('배열 includes x1000');
|
||||
|
||||
// 4) Map 선수 (O(1) — 열쇠로 한 번에)
|
||||
console.time('Map has x1000');
|
||||
for (const t of targets) map.has(t);
|
||||
console.timeEnd('Map has x1000');
|
||||
|
||||
// 결과 예시 (PC마다 다르지만 차이의 '규모'를 보세요):
|
||||
// 배열 includes x1000: 300ms 안팎 ← 1,000번 × 최대 10만 칸 확인
|
||||
// Map has x1000: 0.5ms 안팎 ← 1,000번 × 한 번에
|
||||
// 같은 질문 1,000개인데 수백 배 차이 — 이게 자료구조를 배우는 이유입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '왜 그릇이 여러 개?' },
|
||||
{ n: 2, label: '배열' },
|
||||
{ n: 3, label: '스택과 큐' },
|
||||
{ n: 4, label: '해시맵' },
|
||||
{ n: 5, label: '트리' },
|
||||
{ n: 6, label: '시간복잡도 감각' },
|
||||
{ n: 7, label: '속도 대결 실습' },
|
||||
];
|
||||
|
||||
export default function DataStructuresPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 소프트웨어</div>
|
||||
<h1>자료구조 맛보기<br />— 데이터를 담는 그릇 고르는 법</h1>
|
||||
<p>
|
||||
국은 국그릇에, 밥은 밥공기에 — 데이터에도 쓰임새마다 어울리는 그릇이 따로 있어요.
|
||||
배열·스택·큐·해시맵·트리를 <strong>직접 콘솔에 쳐 보면서</strong> 맛보고,
|
||||
마지막엔 10만 개 데이터로 배열과 Map의 속도 대결까지 시켜 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 60분</span>
|
||||
<span className="chip">콘솔 실습 3개</span>
|
||||
<span className="chip">준비물: 브라우저 F12</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="왜 그릇마다 쓰임이 다를까?" sub="자료구조 = 데이터를 담는 그릇의 종류">
|
||||
<p>
|
||||
<strong>자료구조</strong>는 어렵게 들리지만 정체는 단순해요 —
|
||||
<strong> 데이터를 담아 두는 그릇의 종류</strong>입니다. 부엌에 컵·쟁반·접시·냄비가
|
||||
따로 있는 이유는 담는 내용물과 <strong>"자주 하는 동작"</strong>이 다르기 때문이죠.
|
||||
데이터도 똑같아요. "순서대로 훑는 일"이 많은지, "최근 것부터 되감는 일"이 많은지,
|
||||
"이름으로 바로 찾는 일"이 많은지에 따라 최적의 그릇이 달라집니다.
|
||||
</p>
|
||||
<Code>{CODE_KITCHEN}</Code>
|
||||
<p>
|
||||
이 코스에서는 이 그릇들을 하나씩 손에 들어 봅니다. 외울 필요 없어요 —
|
||||
각 그릇이 <strong>어떤 동작을 제일 잘하는지</strong>(그리고 뭘 못하는지)만
|
||||
몸에 남기면 성공입니다. 그 감각이 있어야 나중에 "여기엔 뭘 쓰지?"를
|
||||
스스로 결정할 수 있거든요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>아침수업과 연결</b> 이번 <strong>아침수업 자료구조 주간</strong>이 바로 이 코스의
|
||||
심화판이에요. 여기서 그릇들 얼굴을 미리 익혀 두면, 아침수업에서 구현 원리를
|
||||
파고들 때 "아, 그 접시 탑!" 하고 바로 따라갈 수 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="배열 — 번호표 붙은 쟁반" sub="순서가 있는 데이터의 기본 그릇">
|
||||
<p>
|
||||
<strong>배열</strong>(Array)은 칸마다 번호표(인덱스)가 붙은 쟁반이에요.
|
||||
가장 큰 특징은 <strong>순서가 유지된다</strong>는 것. 출석부처럼 "1번부터 차례대로"가
|
||||
중요한 데이터는 전부 배열에 담습니다. 우리 플랫폼에서도 코스 목록, 공지 목록처럼
|
||||
<strong> 화면에 순서대로 나열되는 것</strong>은 거의 다 배열이에요.
|
||||
</p>
|
||||
<Code>{CODE_ARRAY}</Code>
|
||||
<p>
|
||||
배열의 강점은 <strong>순서대로 전부 돌기</strong>와 <strong>몇 번째 칸인지 알 때
|
||||
바로 꺼내기</strong>(<span className="icode">interns[0]</span>은 한 번에!)예요.
|
||||
약점은 <strong>"값으로 찾기"</strong> — 번호표가 아니라 내용물로 찾으려면
|
||||
앞에서부터 한 칸씩 다 열어 봐야 합니다. 이 약점은 섹션 4의 해시맵이,
|
||||
그 차이의 크기는 섹션 7의 속도 대결이 보여 줄 거예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 <span className="kbd">F12</span> →
|
||||
<span className="kbd">Console</span> 탭을 열고 위 코드를 그대로 붙여넣어 보세요.
|
||||
그리고 <span className="icode">interns[10]</span>처럼 없는 칸을 꺼내면 뭐가 나오는지도
|
||||
실험해 보세요. (에러가 아니라 <span className="icode">undefined</span>가 나와요 —
|
||||
"빈 칸"이라는 뜻입니다.)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="스택과 큐 — 접시 탑과 급식 줄" sub="넣는 건 같은데 꺼내는 방향이 다르다">
|
||||
<p>
|
||||
이번엔 <strong>꺼내는 순서에 규칙이 있는</strong> 두 그릇이에요.
|
||||
<strong> 스택</strong>(Stack)은 접시 탑 — 맨 위에 쌓고 맨 위부터 꺼냅니다.
|
||||
나중에 넣은 게 먼저 나와서 <strong>LIFO</strong>(Last In, First Out)라고 불러요.
|
||||
여러분이 하루에도 수십 번 누르는 <strong>브라우저 뒤로가기</strong>가 정확히 스택입니다.
|
||||
</p>
|
||||
<Code>{CODE_STACK}</Code>
|
||||
<p>
|
||||
<strong>큐</strong>(Queue)는 급식 줄 — 뒤로 줄 서고 앞에서부터 빠집니다.
|
||||
먼저 온 게 먼저 나가는 <strong>FIFO</strong>(First In, First Out)예요.
|
||||
새치기가 없는 그릇이라, <strong>순서를 공정하게 지켜야 하는 곳</strong>에 씁니다.
|
||||
우리 백엔드(Spring Boot)가 동시에 들어온 요청들을 처리하는 것도 기본은 줄 세우기예요.
|
||||
</p>
|
||||
<Code>{CODE_QUEUE}</Code>
|
||||
<div className="warn">
|
||||
<b>헷갈림 주의</b> 스택과 큐는 별도의 신기한 물건이 아니라
|
||||
<strong> "배열을 쓰는 규칙"</strong>에 가까워요. 위 코드처럼 JS에서는 배열의
|
||||
<span className="icode">push</span>/<span className="icode">pop</span>만 쓰면 스택,
|
||||
<span className="icode">push</span>/<span className="icode">shift</span>만 쓰면 큐가
|
||||
됩니다. 중간에 다른 메서드를 섞는 순간 규칙이 깨져요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 위 <span className="icode">history</span> 코드를 콘솔에
|
||||
붙여넣고, 실제 브라우저에서 학습 센터 → 이 코스 순서로 이동했다가
|
||||
<strong> 뒤로가기</strong>를 눌러 보세요. 내 코드의 <span className="icode">pop()</span>과
|
||||
브라우저의 동작이 똑같이 움직이는 걸 비교하면, LIFO가 머리가 아니라 손에 남아요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="해시맵 — 열쇠로 한 번에 여는 사물함" sub="'찾기'가 O(1)이 되는 마법의 원리">
|
||||
<p>
|
||||
배열에서 값으로 찾으려면 앞에서부터 다 뒤져야 했죠. <strong>해시맵</strong>(JS에서는
|
||||
<span className="icode">Map</span>, 그냥 객체 <span className="icode">{'{}'}</span>도
|
||||
비슷한 역할)은 <strong>열쇠(key)만 있으면 한 번에</strong> 찾아 줍니다.
|
||||
</p>
|
||||
<Code>{CODE_HASHMAP}</Code>
|
||||
<p>
|
||||
왜 한 번에 될까요? 비밀은 <strong>해시 함수</strong>예요. 열쇠(예:
|
||||
<span className="icode">'서연'</span>)를 넣으면 <strong>보관 위치가 계산되어
|
||||
나오는 기계</strong>죠. 저장할 때 그 계산대로 넣어 두고, 찾을 때도 같은 계산을 하면
|
||||
같은 위치가 나오니까 — 뒤질 필요 없이 <strong>바로 그 칸으로 직행</strong>합니다.
|
||||
"이름 순서대로 찾기"가 아니라 "이름이 곧 위치"인 거예요.
|
||||
</p>
|
||||
<p>
|
||||
이 원리는 우리 스택 전체에 깔려 있어요. <strong>PostgreSQL</strong>이 수백만 행에서
|
||||
한 명을 순식간에 찾는 인덱스, HTTP 헤더의 <span className="icode">이름: 값</span> 쌍,
|
||||
React의 <span className="icode">key</span> prop까지 — 전부
|
||||
<strong> "열쇠 → 값"</strong> 구조입니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="트리 — 폴더 안에 폴더" sub="여러분이 이미 매일 쓰고 있는 구조">
|
||||
<p>
|
||||
<strong>트리</strong>(Tree)는 부모-자식 관계로 가지를 뻗는 그릇이에요.
|
||||
이름은 나무인데 보통 <strong>뿌리(root)를 위에 두고 거꾸로</strong> 그립니다.
|
||||
새로 배울 것도 없이, 여러분은 이미 매일 트리를 쓰고 있어요 —
|
||||
PC의 <strong>폴더 구조</strong>가 트리니까요.
|
||||
</p>
|
||||
<Code>{CODE_TREE}</Code>
|
||||
<p>
|
||||
웹 개발자에게 트리는 특히 중요해요. 지금 보고 있는 이 화면의
|
||||
<strong> DOM</strong>도 트리, 백엔드가 보내 주는 <strong>JSON</strong>도 트리,
|
||||
<strong> React 컴포넌트</strong>가 겹겹이 포개진 것도 트리입니다.
|
||||
"태그 안에 태그", "폴더 안에 폴더", "객체 안에 객체" — 이 <strong>포함 관계</strong>가
|
||||
보이면 전부 트리라고 생각하면 돼요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> <span className="kbd">F12</span> →
|
||||
<span className="kbd">Elements</span> 탭을 열어 보세요. 이 페이지의 DOM 트리가
|
||||
펼쳐집니다. 화살표를 눌러 <span className="icode">div.hero</span> 가지를 접었다 펴 보고,
|
||||
지금 읽고 있는 이 문장이 트리의 몇 단계 깊이(뿌리에서 몇 번 내려온 잎)인지 세어 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="시간복잡도 감각 — O(1), O(n), O(n²)" sub="데이터가 10배 늘면 내 코드는 몇 배 느려질까?">
|
||||
<p>
|
||||
그릇을 골랐으면 이제 <strong>"이 그릇, 데이터가 많아져도 버틸까?"</strong>를 묻는
|
||||
감각이 필요해요. 그 감각의 언어가 <strong>시간복잡도</strong>, 표기법이
|
||||
<strong> O(빅오)</strong>입니다. 수학처럼 보이지만, 핵심 질문은 딱 하나예요 —
|
||||
<strong> 데이터가 10배 늘면 일은 몇 배 느나?</strong>
|
||||
</p>
|
||||
<Code>{CODE_BIGO}</Code>
|
||||
<p>
|
||||
이제 앞 섹션들이 하나로 이어져요. 배열의 <span className="icode">indexOf</span>가
|
||||
느린 이유 = <strong>O(n)</strong>, 해시맵의 <span className="icode">get</span>이
|
||||
빠른 이유 = <strong>O(1)</strong>. 개발하다 화면이 버벅이면 십중팔구 어딘가에
|
||||
숨어 있는 <strong>O(n²)</strong>(반복문 속 반복문) 때문입니다.
|
||||
정확한 계산은 아침수업에서 다듬고, 지금은 이 <strong>세 단계의 체감 차이</strong>만
|
||||
가져가면 충분해요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 오해</b> "O(n)은 나쁘다"가 아니에요. 데이터가 100개뿐이면 O(n²)도
|
||||
멀쩡히 돌아갑니다. 문제는 <strong>데이터가 커질 때</strong> — 그래서 진짜 질문은
|
||||
"지금 빠른가?"가 아니라 <strong>"10만 개여도 빠를까?"</strong>입니다.
|
||||
바로 다음 섹션에서 그 10만 개를 직접 만들어 봅니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습 — 배열 vs Map, 10만 개 속도 대결" sub="O(n)과 O(1)의 차이를 숫자로 목격하기">
|
||||
<p>
|
||||
말로만 듣던 차이를 <strong>직접 측정</strong>해 볼 시간이에요. 수강생 10만 명을
|
||||
가짜로 등록해 놓고, 배열과 Map에게 <strong>같은 학생 1,000명을 찾아보라</strong>고
|
||||
시킵니다. <span className="icode">console.time</span>이 스톱워치 역할을 해요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 (메인 실습)</b> 아무 페이지에서나 <span className="kbd">F12</span> →
|
||||
<span className="kbd">Console</span> 탭을 열고, 아래 코드를 <strong>통째로</strong> 붙여넣고
|
||||
<span className="kbd">Enter</span>. 두 줄의 시간이 몇 배 차이 나는지 확인하세요.
|
||||
</div>
|
||||
<Code>{CODE_RACE}</Code>
|
||||
<p>
|
||||
결과를 봤나요? 같은 질문 1,000개에 배열은 수백 ms, Map은 1ms 미만 —
|
||||
<strong> 수백 배 차이</strong>가 흔하게 나옵니다. 코드 몇 줄 차이가 아니라
|
||||
<strong> 그릇 선택</strong>의 차이예요. 사용자 눈에는 "버벅이는 사이트"와
|
||||
"쾌적한 사이트"의 차이로 보이고요.
|
||||
</p>
|
||||
<p>
|
||||
한 발 더: <span className="icode">N</span>을 <span className="icode">100000</span>에서
|
||||
<span className="icode">1000000</span>(100만)으로 바꿔 다시 돌려 보세요.
|
||||
배열의 시간은 대략 10배로 뛰는데(O(n)이니까), Map은 거의 그대로(O(1))인 걸
|
||||
직접 확인할 수 있어요 — 섹션 6의 표가 눈앞에서 재현됩니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🧺 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분의 부엌에는 그릇 다섯 개가 생겼어요 — 순서의 <strong>배열</strong>,
|
||||
되감기의 <strong>스택</strong>, 줄 세우기의 <strong>큐</strong>,
|
||||
한 번에 찾는 <strong>해시맵</strong>, 포함 관계의 <strong>트리</strong>.
|
||||
그리고 "10만 개여도 괜찮을까?"라고 묻는 <strong>시간복잡도 감각</strong>까지.
|
||||
이번 주 <strong>아침수업 자료구조 주간</strong>에서 각 그릇의 내부 구현을 파고들 때,
|
||||
오늘 콘솔에서 잰 숫자들을 꼭 다시 꺼내 보세요. 이어서{' '}
|
||||
<Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스에서 이 그릇들을
|
||||
실제 기능 만들기에 써 보면, 오늘 배운 게 진짜 도구가 됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
434
frontend/src/pages/courses/DebuggingPage.jsx
Normal file
434
frontend/src/pages/courses/DebuggingPage.jsx
Normal file
@ -0,0 +1,434 @@
|
||||
// 이 파일이 하는 일: "디버깅과 에러 읽는 법" 코스 — 빨간 에러 메시지를 무서워하지 않고
|
||||
// '단서'로 읽는 법을 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (에러 = 단서 → 자바 스택트레이스 → JS 콘솔 에러 → console.log 정석
|
||||
// → IntelliJ 브레이크포인트 → 이분 탐색 → 재현 조건 → 30분 룰과 좋은 질문)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 에러 메시지·터미널 예시는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 에러 예시 · 텍스트 다이어그램 상수들 ──
|
||||
|
||||
const CODE_ERROR_ANATOMY = `에러 메시지는 사실 '사건 보고서'예요. 세 가지 질문에 답해 줍니다.
|
||||
|
||||
① 무슨 일이야? → 에러의 종류 (NullPointerException, TypeError...)
|
||||
② 왜 그랬는데? → 에러 옆의 설명 문장 ("... is null", "... is not a function")
|
||||
③ 어디서? → 파일 이름 + 줄 번호 (UserService.java:42, App.jsx:17)
|
||||
|
||||
이 셋만 읽어도 문제의 절반은 푼 겁니다.
|
||||
"에러 났다" 하고 창을 닫는 순간, 범인이 남긴 자백서를 버리는 거예요.`;
|
||||
|
||||
const CODE_JAVA_STACKTRACE = `Spring Boot 콘솔에 뜨는 전형적인 스택트레이스:
|
||||
|
||||
java.lang.NullPointerException: Cannot invoke "User.getName()"
|
||||
because "user" is null ← ①② 종류 + 이유
|
||||
at com.awesomedev.mirim.service.UserService
|
||||
.greet(UserService.java:42) ← ③ 여기가 사건 현장!
|
||||
at com.awesomedev.mirim.controller.UserController
|
||||
.hello(UserController.java:18) ← 현장으로 부른 사람
|
||||
at java.base/jdk.internal.reflect... ← 여기부터는 자바/스프링 내부
|
||||
at org.springframework.web.servlet... ← (우리가 쓴 코드 아님)
|
||||
... 47 more
|
||||
|
||||
읽는 법:
|
||||
1) 첫 줄 = 무슨 에러 + 왜 (user가 null인데 getName()을 불렀다)
|
||||
2) 위에서 아래로 내려가며 "내 패키지(com.awesomedev)"가 나오는
|
||||
첫 줄을 찾는다 → 거기가 내 코드에서 터진 지점
|
||||
3) org.springframework, java.base 줄은 스프링/자바 내부 —
|
||||
대부분 건너뛰어도 됩니다. 범인은 거의 항상 내 코드에 있어요.`;
|
||||
|
||||
const CODE_CAUSED_BY = `스택트레이스가 아주 길 때의 치트키 — "Caused by"를 찾아라!
|
||||
|
||||
org.springframework.web.util.NestedServletException:
|
||||
Request processing failed ... ← 겉포장 (증상)
|
||||
at ... (수십 줄)
|
||||
Caused by: org.hibernate.exception.ConstraintViolationException:
|
||||
could not execute statement ← 한 겹 벗김
|
||||
at ... (수십 줄)
|
||||
Caused by: org.postgresql.util.PSQLException: ← 진짜 원인!
|
||||
ERROR: duplicate key value violates unique
|
||||
constraint "users_email_key"
|
||||
at ...
|
||||
|
||||
에러는 양파처럼 겹겹이 포장돼서 올라와요.
|
||||
가장 아래쪽 "Caused by"가 가장 깊은 원인 —
|
||||
여기서는 "이미 있는 이메일로 또 가입하려다 DB가 거절"이 진범입니다.
|
||||
긴 스택트레이스를 만나면 Ctrl+F로 "Caused by"부터 검색하세요.`;
|
||||
|
||||
const CODE_JS_CONSOLE = `브라우저 F12 → Console 탭에서 자주 만나는 빨간 줄들:
|
||||
|
||||
TypeError: Cannot read properties of undefined (reading 'name')
|
||||
at UserCard (UserCard.jsx:12) ← 파일:줄번호, 클릭하면 이동!
|
||||
|
||||
→ 해석: "undefined인 무언가의 .name을 읽으려 했다"
|
||||
user.name을 썼는데 user가 아직 없다(undefined)는 뜻.
|
||||
API 응답이 도착하기 전에 화면을 그리려 할 때 단골로 나옵니다.
|
||||
|
||||
ReferenceError: userNmae is not defined
|
||||
→ 해석: userNmae라는 변수는 세상에 없다 — 오타!
|
||||
|
||||
Failed to fetch / 404 Not Found / 500 Internal Server Error
|
||||
→ 해석: 프론트가 아니라 '서버와의 통화'가 문제.
|
||||
404 = 그런 주소 없음(URL 오타?), 500 = 서버 쪽에서 터짐
|
||||
→ 이럴 땐 백엔드(Spring Boot) 콘솔의 스택트레이스를 봐야 해요.
|
||||
|
||||
프론트 에러인지 서버 에러인지 구분하는 것만으로도
|
||||
"어느 콘솔을 봐야 하는지"가 정해집니다.`;
|
||||
|
||||
const CODE_CONSOLE_LOG = `console.log 디버깅의 정석 — 아무 데나 찍지 말고 '질문'을 찍자.
|
||||
|
||||
// ❌ 이렇게 찍으면 나중에 뭐가 뭔지 몰라요
|
||||
console.log(data);
|
||||
console.log(data2);
|
||||
|
||||
// ✅ 라벨을 붙이고, '의심 지점의 앞뒤'에 찍는다
|
||||
console.log('[UserCard] 받은 props.user =', user);
|
||||
console.log('[fetchUsers] 응답 status =', res.status);
|
||||
console.log('[fetchUsers] 파싱된 데이터 =', json);
|
||||
|
||||
어디에 찍을까? — 데이터가 지나가는 '관문'마다:
|
||||
1) 함수 입구 → 들어온 값이 애초에 정상인가?
|
||||
2) 분기 직전 → if문이 어느 길로 갔는가?
|
||||
3) 반환 직전 → 나가는 값이 기대한 모양인가?
|
||||
|
||||
객체는 통째로 찍기: console.log('user =', user)
|
||||
→ 콘솔에서 펼쳐 보며 실제 모양(필드 이름!)을 확인할 수 있어요.
|
||||
"user.userName인 줄 알았는데 실제론 user.username이더라" 같은
|
||||
사건의 8할이 여기서 해결됩니다.`;
|
||||
|
||||
const CODE_BREAKPOINT = `IntelliJ 브레이크포인트 실습 — 시간을 멈추고 변수 구경하기
|
||||
|
||||
1) UserService.java의 의심 가는 줄, 줄 번호 옆 여백을 클릭
|
||||
→ 빨간 점(브레이크포인트)이 생김
|
||||
2) 실행을 Run(▶)이 아니라 Debug(🐞)로 시작
|
||||
3) 브라우저에서 해당 API를 호출 (페이지 새로고침 등)
|
||||
4) 빨간 점 줄에서 프로그램이 '일시정지'됨!
|
||||
├─ Variables 창: 이 순간 모든 변수의 실제 값이 보임
|
||||
├─ F8 (Step Over): 한 줄씩 실행하며 값 변화 관찰
|
||||
├─ F7 (Step Into): 지금 줄에서 부르는 함수 '안으로' 들어가기
|
||||
└─ F9 (Resume): 다음 브레이크포인트까지 다시 진행
|
||||
|
||||
console.log가 "지나간 자리에 남긴 CCTV 사진"이라면,
|
||||
브레이크포인트는 "현장에서 시간을 정지시키고 직접 둘러보기"예요.
|
||||
값을 확인하려고 로그 찍고 → 재시작하는 반복이 사라집니다.`;
|
||||
|
||||
const CODE_BISECT = `이분 탐색식 원인 좁히기 — 업다운 게임처럼 반씩 자르기
|
||||
|
||||
증상: "버튼을 누르면 화면이 하얗게 죽어요" (원인 후보: 코드 전체 😱)
|
||||
|
||||
[입력] → A(이벤트 핸들러) → B(fetch 호출) → C(응답 가공) → D(화면 그리기) → [출력]
|
||||
|
||||
1) 가운데(B와 C 사이)에 로그를 찍는다
|
||||
→ 로그가 찍혔다? 앞쪽(A·B)은 무죄. 범인은 C·D 중에.
|
||||
→ 안 찍혔다? 범인은 A·B 중에.
|
||||
2) 남은 절반의 가운데에 또 찍는다 → 또 반으로 줄어듦
|
||||
3) 반복하면 후보 16곳도 단 4번 만에 한 곳으로 좁혀져요 (2⁴=16)
|
||||
|
||||
코드뿐 아니라 '변경 이력'에도 통합니다:
|
||||
"어제는 됐는데 오늘 안 돼요" → 어제와 오늘 사이의 커밋들을
|
||||
반씩 갈라 가운데 커밋으로 되돌려 실행 — 되면 뒤쪽 절반이,
|
||||
안 되면 앞쪽 절반이 범인. (git엔 이걸 자동화한 bisect도 있어요.)`;
|
||||
|
||||
const CODE_REPRODUCE = `재현 조건 찾기 — "가끔 안 돼요"를 "이럴 때 항상 안 돼요"로
|
||||
|
||||
버그 신고의 등급:
|
||||
❌ "가끔 로그인이 안 돼요" → 아무도 못 고침
|
||||
⭕ "비밀번호에 공백이 들어가면
|
||||
항상 로그인이 안 돼요" → 5분이면 고침
|
||||
|
||||
재현 조건을 좁히는 질문 목록:
|
||||
- 항상? 가끔? (가끔이면 → 뭐가 다를 때 나는지 찾는 게 숙제)
|
||||
- 특정 계정만? 특정 데이터만? (테스트 계정 vs 내 계정)
|
||||
- 새로고침하면? 다른 브라우저에선? 시크릿 창에선?
|
||||
(시크릿 창에서 되면 → 캐시나 저장된 로그인 정보가 용의자)
|
||||
- 방금 뭘 바꿨지? (내 마지막 커밋/저장이 1순위 용의자)
|
||||
|
||||
"항상 터지게 만드는 최소 재현 절차"를 찾으면
|
||||
그 버그는 이미 반쯤 고쳐진 겁니다 — 고친 뒤에
|
||||
같은 절차로 '이제 안 터짐'을 증명할 수도 있고요.`;
|
||||
|
||||
const CODE_GOOD_QUESTION = `30분 룰 — 그리고 '답을 부르는 질문'의 3요소
|
||||
|
||||
30분 룰: 혼자 30분 파고들었는데 진전이 없으면 질문한다.
|
||||
- 5분 만에 질문 → 스스로 클 기회를 버림
|
||||
- 3시간 붙잡기 → 팀의 시간을 버림 (여러분의 막힘은 팀의 일이에요)
|
||||
|
||||
질문 템플릿 (Gitea 이슈·팀 채팅 공통):
|
||||
─────────────────────────────────────────
|
||||
[하려던 것] 회원가입 API에 이메일 중복 검사를 붙이는 중
|
||||
[에러 전문] (스크린샷 말고 텍스트로, 요약하지 말고 통째로!)
|
||||
Caused by: org.postgresql.util.PSQLException: ERROR:
|
||||
duplicate key value violates unique constraint ...
|
||||
[재현 방법] 같은 이메일로 두 번 가입 요청하면 항상 발생
|
||||
[시도한 것] ① 스택트레이스에서 Caused by 확인 → DB 제약 위반
|
||||
② 검사 로직에 로그 찍음 → 검사 함수가 아예 호출 안 됨
|
||||
[내 추측] 컨트롤러에서 검사 서비스를 안 거치는 것 같은데,
|
||||
어디서 연결해야 할지 모르겠습니다
|
||||
─────────────────────────────────────────
|
||||
|
||||
에러를 요약하지 말고 '전문'을 붙이는 이유:
|
||||
여러분이 사소하다고 지운 그 줄에 답이 있는 경우가 정말 많아요.
|
||||
그리고 '시도한 것'을 쓰다가 스스로 답을 찾는 일 — 매우 흔합니다.
|
||||
(고무 오리에게 설명하다 깨닫는 그 현상, 질문 쓰기에서도 일어나요.)`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '에러 = 단서' },
|
||||
{ n: 2, label: '자바 스택트레이스' },
|
||||
{ n: 3, label: 'JS 콘솔 에러' },
|
||||
{ n: 4, label: 'console.log 정석' },
|
||||
{ n: 5, label: '브레이크포인트' },
|
||||
{ n: 6, label: '이분 탐색' },
|
||||
{ n: 7, label: '재현 조건' },
|
||||
{ n: 8, label: '30분 룰 · 좋은 질문' },
|
||||
];
|
||||
|
||||
export default function DebuggingPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스의 목표 — 빨간 글씨 공포증 탈출 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 소프트웨어</div>
|
||||
<h1>디버깅과 에러 읽는 법<br />— 빨간 글씨는 자백서다</h1>
|
||||
<p>
|
||||
에러 메시지는 프로그램이 남긴 <strong>사건 보고서</strong>예요 — 무섭다고 창을
|
||||
닫으면 범인이 쓴 자백서를 버리는 셈이죠. 이 코스에서는 자바 스택트레이스와
|
||||
브라우저 콘솔을 <strong>읽는 순서</strong>, 그리고 원인을 반씩 잘라 좁혀 가는
|
||||
탐정의 기술을 배웁니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 70분</span>
|
||||
<span className="chip">실습 3개</span>
|
||||
<span className="chip">준비물: 내 PC + IntelliJ</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="에러는 적이 아니라 단서" sub="사건 보고서를 읽는 탐정이 되자">
|
||||
<p>
|
||||
코드가 터지면 화면 가득 빨간 글씨가 쏟아지죠. 이때 개발자는 두 부류로 갈립니다 —
|
||||
<strong> 창을 닫고 코드를 노려보는 사람</strong>과,
|
||||
<strong> 빨간 글씨를 천천히 읽는 사람</strong>. 실력 차이는 대부분 여기서 나요.
|
||||
</p>
|
||||
<p>
|
||||
에러 메시지는 프로그램이 죽기 직전에 남긴 <strong>사건 보고서</strong>입니다.
|
||||
누가(어느 파일·몇 번째 줄), 무엇을(어떤 종류의 에러), 왜(설명 문장) —
|
||||
범인이 자백서까지 써 놓고 갔는데 안 읽을 이유가 없죠.
|
||||
</p>
|
||||
<Code>{CODE_ERROR_ANATOMY}</Code>
|
||||
<div className="tip">
|
||||
<b>탐정의 첫 습관</b> 에러가 나면 고치려 들기 전에, 에러 메시지를
|
||||
<strong> 소리 내어(속으로라도) 한국어로 번역</strong>해 보세요.
|
||||
"user가 null인데 getName을 불렀대" — 번역이 되면 이미 절반은 푼 겁니다.
|
||||
번역이 안 되는 단어는 그대로 검색하면 돼요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="자바 스택트레이스 읽는 법" sub="위에서 아래로, '내 코드'가 나올 때까지">
|
||||
<p>
|
||||
Spring Boot 서버가 터지면 콘솔에 수십 줄짜리 <strong>스택트레이스</strong>가
|
||||
쏟아집니다. 스택트레이스는 "사건 현장까지 누가 누구를 불렀는지"를 적은
|
||||
<strong> 호출의 족보</strong>예요 — 맨 위가 사건 현장, 아래로 갈수록
|
||||
그 현장으로 이어진 호출의 조상들입니다.
|
||||
</p>
|
||||
<Code>{CODE_JAVA_STACKTRACE}</Code>
|
||||
<p>
|
||||
핵심은 <strong>전부 읽지 않는 것</strong>이에요. 첫 줄(무슨 에러·왜)을 읽고,
|
||||
위에서 아래로 훑으며 <strong>내 패키지 이름이 나오는 첫 줄</strong>을 찾으면 끝.
|
||||
스프링 내부 줄 수십 개는 "택배가 거쳐 온 물류 센터 목록"일 뿐, 범인이 아닙니다.
|
||||
</p>
|
||||
<p>
|
||||
그런데 스택트레이스가 여러 덩어리로 이어질 때가 있어요. 그럴 땐 이 단어를 찾으세요.
|
||||
</p>
|
||||
<Code>{CODE_CAUSED_BY}</Code>
|
||||
<div className="warn">
|
||||
<b>흔한 함정</b> 맨 위 에러만 보고 "스프링이 이상해요!"라고 결론 내리기.
|
||||
겉포장(NestedServletException)은 증상이고, 진범은 거의 항상
|
||||
<strong> 가장 아래쪽 Caused by</strong>에 있습니다. 긴 로그에서는
|
||||
<span className="kbd">Ctrl</span>+<span className="kbd">F</span>로
|
||||
<span className="icode">Caused by</span>를 검색하는 게 제일 빠른 길이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="JS 콘솔 에러 읽기" sub="F12 콘솔의 단골 손님들과 인사하기">
|
||||
<p>
|
||||
프론트(React)에서 터진 에러는 브라우저의 <span className="kbd">F12</span> →
|
||||
<span className="kbd">Console</span> 탭에 모입니다. 자바처럼 긴 족보 대신
|
||||
짧고 굵은 한 줄 + <strong>파일:줄번호 링크</strong>가 뜨는데, 그 링크를 클릭하면
|
||||
문제의 코드로 바로 이동해요.
|
||||
</p>
|
||||
<Code>{CODE_JS_CONSOLE}</Code>
|
||||
<p>
|
||||
특히 중요한 감각이 <strong>"이게 프론트 문제야, 서버 문제야?"</strong>를 가르는
|
||||
거예요. <span className="icode">TypeError</span>·<span className="icode">ReferenceError</span>는
|
||||
내 JS 코드 문제, <span className="icode">404</span>·<span className="icode">500</span> 같은
|
||||
숫자가 보이면 서버와의 통신 문제 — 후자라면 브라우저가 아니라
|
||||
<strong> Spring Boot 콘솔</strong>(섹션 2!)을 열어야 합니다. 병원으로 치면
|
||||
접수처에서 내과로 갈지 외과로 갈지 정하는 단계죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 우리 학습 플랫폼을 열고 <span className="kbd">F12</span> →
|
||||
Console 탭에서 <span className="icode">window.없는변수.name</span>을 직접 입력해 보세요.
|
||||
방금 배운 <span className="icode">TypeError: Cannot read properties of undefined</span>가
|
||||
그대로 나타나요. 이번엔 <span className="icode">엉뚱한이름()</span>을 입력해서
|
||||
<span className="icode">ReferenceError</span>도 만나 보세요 — 일부러 내 본 에러는
|
||||
실전에서 만나도 반갑습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="console.log 디버깅의 정석" sub="아무 데나 찍지 말고, 질문을 찍자">
|
||||
<p>
|
||||
가장 원시적이면서 가장 많이 쓰는 무기가 <span className="icode">console.log</span>예요
|
||||
(자바라면 로그 출력). 그런데 무작정 찍으면 콘솔이 쓰레기장이 됩니다.
|
||||
로그 하나하나가 <strong>"이 지점에서 이 값은 뭐지?"라는 질문</strong>이 되도록 찍는 게 정석이에요.
|
||||
</p>
|
||||
<Code>{CODE_CONSOLE_LOG}</Code>
|
||||
<div className="olist">
|
||||
<ol>
|
||||
<li><strong>라벨을 붙인다</strong> — <span className="icode">'[함수명] 변수명 ='</span> 형식이면 어디서 찍힌 건지 바로 알아요.</li>
|
||||
<li><strong>관문마다 찍는다</strong> — 함수 입구·분기 직전·반환 직전. 데이터가 어디서부터 이상해졌는지 드러납니다.</li>
|
||||
<li><strong>객체는 통째로 찍는다</strong> — 필드 이름을 내 기억이 아니라 실제 데이터로 확인하세요.</li>
|
||||
<li><strong>다 잡으면 지운다</strong> — 디버그 로그를 커밋에 남기면 다음 사람의 콘솔이 쓰레기장이 돼요.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>주의</b> 로그가 <strong>아예 안 찍히는 것</strong>도 중요한 단서입니다!
|
||||
"값이 이상하다"가 아니라 "이 함수 자체가 호출되지 않는다"는 뜻이니까요.
|
||||
안 찍힌 로그는 실패가 아니라 용의자 절반을 지워 준 수확이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="IntelliJ 브레이크포인트" sub="시간을 멈추고 변수 값을 직접 구경하기">
|
||||
<p>
|
||||
<span className="icode">console.log</span>가 지나간 자리의 <strong>CCTV 사진</strong>이라면,
|
||||
브레이크포인트는 <strong>사건 현장에서 시간을 정지</strong>시키는 마법이에요.
|
||||
멈춘 그 순간의 모든 변수 값을 눈으로 보고, 한 줄씩 재생하며 어디서 값이
|
||||
틀어지는지 실시간으로 관찰할 수 있습니다.
|
||||
</p>
|
||||
<Code>{CODE_BREAKPOINT}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> IntelliJ에서 우리 백엔드 프로젝트를 열고, 아무
|
||||
컨트롤러 메서드의 첫 줄에 브레이크포인트(줄 번호 옆 클릭)를 걸어 보세요.
|
||||
<strong> Debug(🐞)</strong>로 실행 → 브라우저에서 그 API를 호출(페이지 접속) →
|
||||
IntelliJ가 그 줄에서 멈추면, Variables 창에서 요청으로 들어온 값을 구경하고
|
||||
<span className="kbd">F8</span>로 세 줄만 전진해 보세요. 마지막엔
|
||||
<span className="kbd">F9</span>로 풀어 주기 — 안 풀면 브라우저는 계속
|
||||
로딩 중으로 기다립니다(이것도 직접 목격해 보세요!).
|
||||
</div>
|
||||
<p>
|
||||
언제 로그를 쓰고 언제 브레이크포인트를 쓸까요? <strong>빠른 확인 한 번</strong>이면
|
||||
로그, <strong>값이 변해 가는 과정을 추적</strong>해야 하면 브레이크포인트 —
|
||||
둘 다 쓸 줄 알아야 상황에 맞는 무기를 고를 수 있어요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="이분 탐색식 원인 좁히기" sub="업다운 게임처럼, 용의자를 반씩 지운다">
|
||||
<p>
|
||||
에러 메시지가 친절하지 않을 때도 있어요. "화면이 하얗다", "아무 반응이 없다" —
|
||||
단서가 없으면 코드 전체가 용의자죠. 이럴 때 처음부터 한 줄씩 읽는 건 최악의 수사예요.
|
||||
<strong> 1~100 업다운 게임</strong>을 떠올리세요. 1부터 차례로 부르면 최대 100번,
|
||||
반씩 자르면 <strong>7번</strong>이면 끝납니다.
|
||||
</p>
|
||||
<Code>{CODE_BISECT}</Code>
|
||||
<p>
|
||||
요령은 <strong>"데이터가 지나가는 길의 한가운데"에 검문소(로그)를 세우는 것</strong>.
|
||||
검문소를 통과했으면 앞 구간은 무죄, 못 왔으면 앞 구간이 유죄 — 어느 쪽이든
|
||||
용의자가 절반으로 줄어요. 코드가 아니라 "어제는 됐는데 오늘 안 되는" 상황이라면
|
||||
커밋 이력을 반씩 잘라 가는 것도 똑같은 원리입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>버릇 들이기</b> "어디가 문제지?" 싶을 때마다 속으로 외치세요 —
|
||||
<strong> "반으로 자르자."</strong> 감으로 여기저기 찔러 보는 것보다
|
||||
항상 빠르고, 항상 끝이 납니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="재현 조건 찾기" sub='"가끔 안 돼요"를 "이럴 때 항상 안 돼요"로'>
|
||||
<p>
|
||||
고칠 수 없는 버그는 없지만, <strong>다시 일으킬 수 없는 버그</strong>는 못 고칩니다.
|
||||
그래서 수사의 중요한 반환점이 "이렇게 하면 <strong>항상</strong> 터진다"는
|
||||
<strong> 재현 절차</strong>를 손에 넣는 순간이에요.
|
||||
</p>
|
||||
<Code>{CODE_REPRODUCE}</Code>
|
||||
<p>
|
||||
재현 절차는 두 번 일합니다. 고치기 <strong>전</strong>엔 원인을 가리키는 나침반이 되고,
|
||||
고친 <strong>후</strong>엔 같은 절차로 "이제 안 터진다"를 증명하는 검증 도구가 돼요.
|
||||
범인을 잡는 것도, 잡았다고 확인하는 것도 같은 절차 하나로 하는 거죠.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>1순위 용의자</b> "갑자기 안 돼요"의 8할은 <strong>방금 내가 바꾼 것</strong>입니다.
|
||||
귀신이 아니라 내 마지막 저장·마지막 커밋부터 의심하세요. Gitea에서 내 최근 커밋의
|
||||
변경 파일 목록을 열어 보는 게 훌륭한 첫 수입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="30분 룰과 좋은 질문" sub="혼자 파는 시간과 팀의 시간, 그 사이의 예의">
|
||||
<p>
|
||||
어썸데브의 약속, <strong>30분 룰</strong> — 혼자 30분 진지하게 파 보고, 그래도
|
||||
진전이 없으면 질문합니다. 5분 만에 묻는 건 스스로 클 기회를 버리는 것이고,
|
||||
3시간을 혼자 끙끙대는 건 팀의 시간을 버리는 거예요. 여러분이 막혀 있는 건
|
||||
부끄러운 일이 아니라 <strong>팀이 알아야 할 정보</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_GOOD_QUESTION}</Code>
|
||||
<div className="olist">
|
||||
<ol>
|
||||
<li><strong>하려던 것</strong> — 목표를 알아야 답도 목표에 맞출 수 있어요.</li>
|
||||
<li><strong>에러 전문</strong> — 요약·스크린샷 말고 텍스트 통째로. 여러분이 지운 그 줄에 답이 있는 경우가 정말 많아요.</li>
|
||||
<li><strong>재현 방법 + 시도한 것</strong> — 이걸 쓰다가 스스로 답을 찾는 일이 놀랄 만큼 자주 일어납니다.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③</b> 이번 주에 만난 에러 하나를 골라, 위 템플릿대로
|
||||
<strong> edu.awesomedevapp.com의 Gitea 이슈</strong>로 실제로 작성해 보세요.
|
||||
이미 해결한 에러여도 좋아요 — "시도한 것"까지 다 쓴 다음 멘토에게 링크를 보내고,
|
||||
"이 질문 받으면 바로 도와줄 수 있겠어요?"라고 피드백을 받아 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🔍 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 빨간 글씨를 <strong>읽고</strong>(스택트레이스·콘솔),
|
||||
시간을 <strong>멈추고</strong>(브레이크포인트), 용의자를 <strong>반씩 지우고</strong>(이분 탐색),
|
||||
사건을 <strong>재현하고</strong>, 막히면 <strong>답이 오는 질문</strong>을 쓸 수 있는
|
||||
탐정이에요. 디버깅으로 잡은 버그가 다시 돌아오지 못하게 막는 법 —{' '}
|
||||
<Link to="/learn/git"><strong>Git과 협업</strong></Link> 코스에서 커밋으로 수사 기록을
|
||||
남기는 법을 이어서 배우고, 다음 과제에서 만나는 첫 에러에 오늘 배운 순서를 그대로
|
||||
적용해 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
372
frontend/src/pages/courses/DesignSystemPage.jsx
Normal file
372
frontend/src/pages/courses/DesignSystemPage.jsx
Normal file
@ -0,0 +1,372 @@
|
||||
// 이 파일이 하는 일: "디자인 시스템 입문" 코스 — 디자인 시스템이 왜 '제품의 문법'인지에서
|
||||
// 출발해, 토큰 → 컴포넌트 → 피그마↔React 짝 맞추기 → 문서화 → 우리 플랫폼 미니 디자인 시스템
|
||||
// 정리 실습(티켓 DZ-03)까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_GRAMMAR = `디자인 시스템이 없을 때 vs 있을 때
|
||||
|
||||
없을 때 — 페이지마다 각자 알아서
|
||||
로그인 페이지 버튼: 파랑 #3B82F6, 둥근 모서리 8px
|
||||
마이페이지 버튼: 파랑 #2F80ED, 둥근 모서리 6px
|
||||
과제 제출 버튼: 파랑 #4A90D9, 둥근 모서리 10px
|
||||
→ 셋 다 "파란 버튼"인데 미묘하게 다 다름. 사용자는 어수선함을 느끼고,
|
||||
개발자는 매번 "이번엔 무슨 파랑이지?" 고민.
|
||||
|
||||
있을 때 — 약속(문법)이 하나
|
||||
버튼은 <Button> 하나. 파랑은 --color-primary 하나.
|
||||
→ 어느 페이지를 만들어도 같은 부품을 조립만 하면 됨.
|
||||
|
||||
디자인 시스템 = 색·글꼴·간격·부품·사용 규칙을 모아 둔 "제품의 문법책"`;
|
||||
|
||||
const CODE_TOKENS = `디자인 토큰 = 디자인 결정에 이름을 붙인 변수
|
||||
|
||||
/* global.css 의 :root — 우리 플랫폼 토큰의 실물 (예시) */
|
||||
:root {
|
||||
/* 색 — "무슨 파랑?"이 아니라 "primary"라고 부르기로 약속 */
|
||||
--color-primary: #...;
|
||||
--color-danger: #...;
|
||||
--color-text: #...;
|
||||
|
||||
/* 간격 — 4의 배수 눈금자. 아무 숫자나 쓰지 않기 */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-4: 16px;
|
||||
|
||||
/* 글꼴 크기 — 제목/본문/캡션 단계 */
|
||||
--font-lg: 1.25rem;
|
||||
--font-md: 1rem;
|
||||
--font-sm: 0.875rem;
|
||||
}
|
||||
|
||||
/* 쓰는 쪽은 값 대신 이름을 부른다 */
|
||||
.btn-primary { background: var(--color-primary); }
|
||||
|
||||
토큰의 힘: 브랜드 색을 바꾸는 날, :root 한 줄만 고치면
|
||||
플랫폼 전체의 버튼·링크·배지가 한꺼번에 바뀐다.`;
|
||||
|
||||
const CODE_BUTTON_STATES = `버튼 하나에도 상태가 최소 5가지
|
||||
|
||||
상태 언제? 겉모습 약속 (예)
|
||||
──────────────────────────────────────────────────────
|
||||
default 평소 primary 색, 또렷한 글자
|
||||
hover 마우스를 올렸을 때 살짝 어두워짐 → "눌러도 돼요" 신호
|
||||
active 누르는 순간 더 어두워지거나 살짝 꺼진 느낌
|
||||
disabled 지금은 못 누를 때 회색 + 커서 금지 표시
|
||||
loading 처리 중(제출 직후 등) 스피너 + 다시 못 누르게 잠금
|
||||
|
||||
→ "버튼을 디자인한다" = 예쁜 네모 하나가 아니라
|
||||
이 5가지 상태 세트를 전부 정하는 일이에요.
|
||||
하나라도 빠지면? disabled가 없는 제출 버튼은
|
||||
따닥 두 번 눌려 과제가 두 번 제출되는 사고로 이어집니다.`;
|
||||
|
||||
const CODE_BADGE_PAIR = `피그마 컴포넌트 ↔ React 컴포넌트 — 이름까지 1:1로 짝 맞추기
|
||||
|
||||
피그마 쪽 React 쪽
|
||||
──────────────────────── ────────────────────────
|
||||
컴포넌트: Badge function Badge({ tone, children })
|
||||
variant 속성: tone props: tone
|
||||
= info | success | danger = 'info' | 'success' | 'danger'
|
||||
|
||||
// Badge.jsx — 피그마의 variant가 그대로 props가 된다
|
||||
function Badge({ tone = 'info', children }) {
|
||||
return <span className={'badge badge-' + tone}>{children}</span>;
|
||||
}
|
||||
|
||||
// 쓰는 쪽 — 디자이너와 개발자가 같은 단어로 대화 가능
|
||||
<Badge tone="success">제출 완료</Badge>
|
||||
<Badge tone="danger">기한 초과</Badge>
|
||||
|
||||
짝이 맞으면: 디자이너 "Badge의 tone을 danger로 바꿔 주세요"
|
||||
개발자 "네, tone='danger' 한 글자요" — 대화 끝.
|
||||
짝이 안 맞으면: "그 빨간 거... 라벨? 칩? 태그?" — 매번 통역이 필요해요.`;
|
||||
|
||||
const CODE_DOC_EXAMPLE = `문서 없는 컴포넌트는 설명서 없는 조립 가구예요.
|
||||
|
||||
좋은 컴포넌트 문서에 꼭 들어가는 4가지:
|
||||
|
||||
1) 언제 쓰나 (When)
|
||||
"Badge는 상태 표시용. 클릭되는 건 Badge가 아니라 Button."
|
||||
|
||||
2) 어떻게 쓰나 (How) — 복사해서 바로 쓰는 예제 코드
|
||||
<Badge tone="success">제출 완료</Badge>
|
||||
|
||||
3) 하지 말 것 (Don't)
|
||||
"Badge 안에 문장 넣지 않기 — 두 단어 이내."
|
||||
"danger tone을 '강조'용으로 쓰지 않기 — 진짜 경고에만."
|
||||
|
||||
4) 상태·변형 전체 목록
|
||||
tone 3종 × 크기 2종 = 전부 몇 가지 모습인지 한눈에.
|
||||
|
||||
→ 문서가 있으면 새 수습이 와도 "물어볼 필요 없이" 조립을 시작할 수 있어요.
|
||||
디자인 시스템의 절반은 부품이고, 나머지 절반은 이 설명서입니다.`;
|
||||
|
||||
const CODE_DZ03_TABLE = `[실습 · 티켓 DZ-03] 우리 플랫폼 미니 디자인 시스템 표 만들기
|
||||
|
||||
우리 학습 플랫폼 화면을 직접 뒤져서 아래 표를 채워 보세요.
|
||||
(정답을 외우는 게 아니라, '찾아내는 눈'을 만드는 실습이에요)
|
||||
|
||||
① 색 (global.css의 :root에서 찾기)
|
||||
역할 토큰 이름 실제 값
|
||||
─────────────────────────────────────────
|
||||
주 색상 --color-???? #______
|
||||
위험/경고 --color-???? #______
|
||||
본문 글자 --color-???? #______
|
||||
|
||||
② 버튼 (아무 페이지의 버튼을 F12로 관찰)
|
||||
상태 겉모습 변화 메모
|
||||
─────────────────────────────────────────
|
||||
default ______________________
|
||||
hover ______________________
|
||||
disabled ______________________
|
||||
|
||||
③ 카드 (이 페이지의 step-card가 바로 실물!)
|
||||
항목 값
|
||||
─────────────────────────────────────────
|
||||
모서리 반지름 ______ px
|
||||
안쪽 여백 ______ px
|
||||
그림자 있음 / 없음
|
||||
|
||||
완성한 표는 DZ-03 티켓에 댓글로 올리고 멘토 리뷰를 받으세요.
|
||||
이 표가 곧 여러분이 만든 첫 '디자인 시스템 문서'입니다.`;
|
||||
|
||||
const CODE_QUIZ = `셀프 체크 4문항 — 말로 설명할 수 있으면 통과!
|
||||
|
||||
Q1. 페이지마다 파란색 코드값이 조금씩 다르면
|
||||
사용자와 개발자에게 각각 어떤 문제가 생길까요? (섹션 1)
|
||||
|
||||
Q2. 브랜드 색을 바꿔야 할 때, 토큰을 쓴 프로젝트와
|
||||
안 쓴 프로젝트의 작업량은 어떻게 다를까요? (섹션 2)
|
||||
|
||||
Q3. loading 상태가 없는 제출 버튼은 어떤 사고를 부를까요? (섹션 3)
|
||||
|
||||
Q4. 피그마 컴포넌트와 React 컴포넌트의 '이름'까지
|
||||
맞추라고 하는 이유는 뭘까요? (섹션 4)
|
||||
|
||||
→ 막히는 문항은 해당 섹션으로 돌아가서 한 번 더.
|
||||
비유를 들어 설명할 수 있으면 진짜 이해한 거예요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
// (지금 이 Section/Code가 바로 이 코스에서 배우는 '컴포넌트 재사용'의 실물이에요!)
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '제품의 문법' },
|
||||
{ n: 2, label: '디자인 토큰' },
|
||||
{ n: 3, label: '컴포넌트와 상태' },
|
||||
{ n: 4, label: '피그마 ↔ React' },
|
||||
{ n: 5, label: '문서화' },
|
||||
{ n: 6, label: '실습: DZ-03' },
|
||||
{ n: 7, label: '정리 퀴즈' },
|
||||
];
|
||||
|
||||
export default function DesignSystemPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>디자인 시스템 입문<br />— 예쁜 화면이 아니라 '약속'을 만드는 일</h1>
|
||||
<p>
|
||||
왜 큰 회사마다 자기만의 디자인 시스템을 만들까요? 색 하나, 버튼 하나에
|
||||
<strong> 이름과 규칙</strong>을 붙이는 순간 디자이너와 개발자가 같은 언어로
|
||||
말하게 되기 때문이에요. 이 코스에서는 우리 학습 플랫폼 자체를 교재 삼아,
|
||||
토큰에서 문서화까지 디자인 시스템의 뼈대를 따라가 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 티켓 DZ-03</span>
|
||||
<span className="chip">준비물: 브라우저와 F12</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="디자인 시스템 = 제품의 문법" sub="왜 큰 회사마다 하나씩 갖고 있을까?">
|
||||
<p>
|
||||
문법이 없으면 문장마다 규칙이 달라져서 글을 읽기 힘들죠. 제품도 똑같아요.
|
||||
페이지를 만드는 사람이 늘어날수록 버튼 모양·파란색 코드값·여백 크기가
|
||||
<strong> 사람 수만큼 갈라지기</strong> 시작합니다. 사용자는 "어딘가 어수선하다"고
|
||||
느끼고, 개발자는 화면을 만들 때마다 사소한 결정을 처음부터 다시 해요.
|
||||
</p>
|
||||
<Code>{CODE_GRAMMAR}</Code>
|
||||
<p>
|
||||
그래서 사람이 많은 회사일수록 디자인 시스템이 <strong>필수</strong>가 됩니다.
|
||||
결정을 한 번만 하고 문법책에 적어 두면, 열 명이 만들어도 한 사람이 만든 것처럼
|
||||
보이거든요. 새로 온 수습(여러분!)도 문법책만 읽으면 바로 팀의 말투로 화면을
|
||||
만들 수 있고요. 어썸데브처럼 작은 팀도 마찬가지 — 지금 규칙을 세워 두는 게
|
||||
나중에 팀이 커졌을 때의 혼란을 미리 막는 보험이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>핵심 한 줄</b> 디자인 시스템은 "예쁘게 꾸미는 기술"이 아니라
|
||||
<strong> "결정을 재사용하는 기술"</strong>이에요. 한 번 정한 약속을
|
||||
모든 화면이 돌려쓰는 거죠.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="디자인 토큰 — 색·간격·글꼴에 이름 붙이기" sub="우리 global.css의 :root가 바로 실물">
|
||||
<p>
|
||||
문법책의 첫 장은 <strong>단어장</strong>이에요. 디자인 시스템의 단어장이 바로
|
||||
<strong> 토큰(token)</strong>입니다. "이 파랑은 <span className="icode">#3B82F6</span>"이
|
||||
아니라 <span className="icode">--color-primary</span>라는 <strong>이름</strong>으로
|
||||
부르기로 약속하는 거예요. 반 친구를 "안경 쓰고 키 큰 애"가 아니라 이름으로 부르는
|
||||
것과 같죠 — 겉모습(값)이 바뀌어도 이름은 그대로니까요.
|
||||
</p>
|
||||
<Code>{CODE_TOKENS}</Code>
|
||||
<p>
|
||||
멀리 갈 것 없이 <strong>우리 플랫폼의 <span className="icode">global.css</span></strong>를
|
||||
열어 보면 <span className="icode">:root</span> 블록에 이 토큰들이 실제로 살고 있어요.
|
||||
지금 여러분이 보고 있는 이 페이지의 카드·팁 박스·코드 블록 색이 전부 거기서
|
||||
출발합니다. 이 파일이 우리 플랫폼 디자인 시스템의 <strong>1층</strong>인 셈이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 이 페이지에서 <span className="kbd">F12</span>를 눌러
|
||||
개발자 도구를 열고, <span className="kbd">Elements</span> 탭에서 맨 위의
|
||||
<span className="icode">html</span>을 선택해 보세요. 오른쪽 Styles 창의
|
||||
<span className="icode">:root</span>에 <span className="icode">--</span>로 시작하는
|
||||
변수들이 주르륵 보이면 성공! 그중 색 변수 하나를 골라 값을 살짝 바꿔 보세요 —
|
||||
화면 곳곳이 <strong>한꺼번에</strong> 바뀌는 걸 볼 수 있어요.
|
||||
(새로고침하면 원래대로 돌아오니 마음껏!)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="컴포넌트 라이브러리 — 버튼 하나의 다섯 얼굴" sub="부품을 만든다 = 상태 세트를 만든다">
|
||||
<p>
|
||||
단어(토큰)를 조합하면 <strong>부품(컴포넌트)</strong>이 됩니다. 버튼, 카드, 입력창,
|
||||
배지... 이런 부품들을 모아 둔 것이 <strong>컴포넌트 라이브러리</strong>예요.
|
||||
그런데 여기서 많은 초보가 놓치는 게 있어요 — 버튼은 <strong>한 가지 모습이 아니라는
|
||||
것</strong>. 사람의 표정처럼, 버튼도 상황에 따라 얼굴이 달라져야 해요.
|
||||
</p>
|
||||
<Code>{CODE_BUTTON_STATES}</Code>
|
||||
<p>
|
||||
디자인 시스템이 있는 팀은 이 5가지 상태를 <strong>버튼을 만들 때 딱 한 번</strong>
|
||||
정합니다. 그 뒤로는 어느 페이지에 버튼을 놓아도 hover·disabled·loading이
|
||||
공짜로 따라와요. 없는 팀은? 페이지마다 "아 맞다, disabled 처리..." 를 반복하다가
|
||||
어딘가 하나는 꼭 빼먹죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 우리 플랫폼에서 아무 버튼이나 하나 골라
|
||||
마우스를 <strong>올려 보고(hover)</strong>, <strong>꾹 눌러 보세요(active)</strong>.
|
||||
색이나 그림자가 어떻게 변하는지 관찰한 뒤, <span className="kbd">F12</span> →
|
||||
해당 버튼 우클릭 → 검사로 어떤 클래스가 그 변화를 만드는지 찾아보세요.
|
||||
표정 5가지 중 우리 플랫폼에 없는 상태가 있다면 — 그게 바로 개선 아이디어예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="피그마 컴포넌트 ↔ React 컴포넌트 짝 맞추기" sub="Badge 하나로 보는 디자이너-개발자 협업">
|
||||
<p>
|
||||
디자이너는 피그마에서 부품을 만들고, 개발자는 React에서 부품을 만들어요.
|
||||
이 둘이 <strong>이름과 속성까지 1:1로 짝이 맞을 때</strong> 디자인 시스템이
|
||||
진짜 힘을 냅니다. 번역기 없이 대화하는 두 사람처럼요. 우리 플랫폼의
|
||||
<strong> Badge</strong>(제출 완료 · 기한 초과 같은 작은 상태 표시)를 예로 볼게요.
|
||||
</p>
|
||||
<Code>{CODE_BADGE_PAIR}</Code>
|
||||
<p>
|
||||
포인트는 <strong>속성 이름</strong>이에요. 피그마에서 variant를
|
||||
<span className="icode">tone</span>이라고 지었으면 React props도
|
||||
<span className="icode">tone</span>. 피그마에서 <span className="icode">danger</span>면
|
||||
코드에서도 <span className="icode">danger</span>. 이렇게 맞춰 두면 디자인 시안을
|
||||
보는 순간 코드가 머릿속에 그려지고, 반대로 코드만 봐도 시안이 그려집니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>짝이 어긋나기 시작하는 순간을 조심!</b> 디자이너가 피그마에 variant를
|
||||
하나 추가했는데 코드에는 없다면? 그 순간부터 두 세계가 갈라져요.
|
||||
부품을 바꿀 땐 <strong>피그마와 코드를 같은 PR·같은 티켓에서</strong> 함께
|
||||
바꾸는 게 규칙입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="문서화 — 부품의 절반은 설명서" sub="물어보지 않아도 조립할 수 있게">
|
||||
<p>
|
||||
부품을 아무리 잘 만들어도 <strong>어떻게 쓰는지</strong>가 머릿속에만 있으면,
|
||||
그 지식은 그 사람이 자리를 비운 날 함께 사라져요. 설명서 없는 조립 가구를
|
||||
받아 본 적 있나요? 나사는 다 있는데 어디에 끼울지 모르는 상태 — 문서 없는
|
||||
컴포넌트 라이브러리가 딱 그렇습니다.
|
||||
</p>
|
||||
<Code>{CODE_DOC_EXAMPLE}</Code>
|
||||
<p>
|
||||
특히 <strong>"하지 말 것(Don't)"</strong>이 중요해요. 규칙은 대부분
|
||||
누군가 실제로 겪은 사고에서 나오거든요. "danger를 강조용으로 쓰지 말 것"이라는
|
||||
한 줄은, 진짜 경고가 필요한 순간에 사용자가 빨간색에 무뎌져 있으면 안 된다는
|
||||
경험의 기록이에요. 여러분이 다음 섹션에서 만들 표가 바로 이런 문서의 출발점입니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="실습 — 우리 플랫폼 미니 디자인 시스템 표 만들기" sub="티켓 DZ-03 · 배운 걸 전부 손으로">
|
||||
<p>
|
||||
이제 1~5섹션을 전부 합칠 시간이에요. 우리 학습 플랫폼을 <strong>탐험하듯 뒤져서</strong>,
|
||||
색 토큰(섹션 2)·버튼 상태(섹션 3)·카드 규격을 표 하나로 정리합니다.
|
||||
남이 만든 디자인 시스템을 읽는 것과, 화면에서 규칙을 <strong>직접 캐내는 것</strong>은
|
||||
완전히 다른 경험이에요 — 후자를 해 본 사람만 디자인 시스템을 "만들" 수 있습니다.
|
||||
</p>
|
||||
<Code>{CODE_DZ03_TABLE}</Code>
|
||||
<div className="olist">
|
||||
<ol>
|
||||
<li>Gitea(edu.awesomedevapp.com)에서 <strong>티켓 DZ-03</strong>을 열고 본인을 담당자로 지정해요.</li>
|
||||
<li>위 표의 ①번부터 순서대로 채워요 — ①은 <span className="icode">global.css</span>, ②·③은 <span className="kbd">F12</span>가 무기예요.</li>
|
||||
<li>완성한 표를 DZ-03에 댓글로 올리고, "우리 플랫폼에 빠져 있다고 느낀 상태·토큰 1가지"도 함께 적어요.</li>
|
||||
<li>멘토 리뷰를 받으면 실습 완료! 지적받은 부분은 표를 고쳐서 다시 올려요.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>막히면</b> 섹션 2의 "직접 확인해 보기 ①"로 돌아가서 :root 보는 법을 복습하세요.
|
||||
값이 <span className="icode">var(--...)</span>로 나오면 그 변수를 :root에서
|
||||
한 번 더 찾아 들어가면 됩니다 — 이름을 따라가는 것 자체가 토큰 훈련이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="정리 퀴즈 — 셀프 체크 4문항" sub="설명할 수 있어야 아는 것">
|
||||
<Code>{CODE_QUIZ}</Code>
|
||||
<div className="tip">
|
||||
<b>공부 팁</b> 이번 코스의 답은 전부 <strong>우리 플랫폼 화면 안</strong>에 있어요.
|
||||
퀴즈를 풀다 막히면 이론으로 돌아가지 말고, F12를 열어 실물을 먼저 보세요.
|
||||
디자인 시스템은 읽는 게 아니라 만지는 과목입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🧩 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 색 하나에도 이름이 있고, 버튼 하나에도 다섯 얼굴이 있다는 걸 알게 됐어요.
|
||||
디자인 시스템의 부품은 결국 <strong>React 컴포넌트</strong>로 태어납니다 —{' '}
|
||||
<Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스에서 컴포넌트를
|
||||
직접 만드는 법을 이어서 배우고, <strong>티켓 DZ-03</strong>의 미니 디자인 시스템
|
||||
표를 꼭 완성해서 멘토 리뷰까지 받아 보세요. 그 표가 여러분의 첫 디자인 문서예요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
371
frontend/src/pages/courses/DnsDeepPage.jsx
Normal file
371
frontend/src/pages/courses/DnsDeepPage.jsx
Normal file
@ -0,0 +1,371 @@
|
||||
// 이 파일이 하는 일: "DNS 깊이 보기" 코스 — 네트워크 코스 5섹션에서 맛본 DNS를
|
||||
// 뿌리(루트 서버)부터 우리 회사의 실제 Route53 구성까지 7개 섹션으로 파고드는 정적 학습 페이지.
|
||||
// (재귀 조회의 여행 → 레코드 타입 → TTL과 캐시 → hosts 파일 실습 → nslookup 실습 → 우리 Route53 → 정리 퀴즈)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip, warn 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_RECURSIVE_TRIP = `edu.awesomedevapp.com 의 IP를 아무도 캐시에 안 갖고 있을 때 — 풀코스 여행
|
||||
|
||||
[내 PC] ── "edu.awesomedevapp.com의 IP 알려줘" ──> [리졸버(통신사 DNS)]
|
||||
│
|
||||
리졸버가 대신 뛰어다니기 시작 (그래서 '재귀' 조회): │
|
||||
▼
|
||||
① 루트 서버 13곳 중 하나에게: "edu.awesomedevapp.com?"
|
||||
루트: "몰라. 근데 .com 담당은 쟤네야" ──> .com TLD 서버 주소를 알려줌
|
||||
|
||||
② .com TLD 서버에게: "edu.awesomedevapp.com?"
|
||||
TLD: "몰라. 근데 awesomedevapp.com 담당은 쟤네야" ──> 권한 서버 주소를 알려줌
|
||||
|
||||
③ 권한(Authoritative) 서버에게: "edu.awesomedevapp.com?"
|
||||
권한 서버(우리는 Route53!): "그건 내 담당! x.x.x.x 야" ── 드디어 정답!
|
||||
|
||||
[리졸버] ── "x.x.x.x 입니다" ──> [내 PC] ── 접속 시작!
|
||||
|
||||
포인트: 아무도 전체 전화번호부를 통째로 안 들고 있어요.
|
||||
"거기 말고 저기 물어봐"를 두 번 거쳐 진짜 담당자를 찾는 구조.`;
|
||||
|
||||
const CODE_RECORD_TYPES = `자주 만나는 DNS 레코드 타입 4형제
|
||||
|
||||
타입 뜻 예시 (우리 도메인이라면)
|
||||
──────────────────────────────────────────────────────────────
|
||||
A 이름 → IPv4 주소 edu.awesomedevapp.com → 3.x.x.x
|
||||
CNAME 이름 → 다른 이름 (별명) www → edu.awesomedevapp.com
|
||||
MX 이 도메인의 메일은 어디로 awesomedev.dev → 메일 서버 주소
|
||||
TXT 자유 메모장 (검증·정책용) "이 도메인 주인 맞음" 증명 문자열 등
|
||||
|
||||
CNAME 주의: 별명의 별명도 가능하지만, 결국 끝에는 A 레코드가 있어야
|
||||
진짜 IP에 도착해요. 별명만 뱅뱅 돌면 아무 데도 못 갑니다.
|
||||
TXT 활용: 도메인 소유 확인(예: 인증서 발급), 스팸 방지 정책(SPF) 등
|
||||
"사람이 읽는 공지"가 아니라 "다른 시스템이 읽는 증명서"로 쓰여요.`;
|
||||
|
||||
const CODE_TTL = `TTL(Time To Live) = "이 답, 몇 초 동안 믿고 재사용해도 돼"
|
||||
|
||||
권한 서버가 답을 줄 때 유통기한을 같이 붙여요:
|
||||
edu.awesomedevapp.com A 3.x.x.x TTL=300 (5분)
|
||||
|
||||
TTL이 길면(예: 86400 = 하루):
|
||||
+ DNS 질문이 줄어 빠르고 서버 부담 적음
|
||||
- IP를 바꿔도 최대 하루 동안 옛 주소로 가는 사람이 남음
|
||||
|
||||
TTL이 짧으면(예: 60 = 1분):
|
||||
+ 변경이 금방 퍼짐
|
||||
- 질문이 잦아짐 (Route53은 질문 수만큼 과금!)
|
||||
|
||||
실전 요령: 서버 이사 계획이 있으면 "미리" TTL을 줄여 두고,
|
||||
이사가 끝나 안정되면 다시 늘린다.`;
|
||||
|
||||
const CODE_HOSTS = `hosts 파일 — 내 PC 전용 수제 전화번호부 (DNS보다 먼저 찾아봄!)
|
||||
|
||||
위치(Windows): C:\\Windows\\System32\\drivers\\etc\\hosts
|
||||
편집: 메모장을 "관리자 권한으로 실행" → 이 파일 열기
|
||||
|
||||
맨 아래에 한 줄 추가해 보기:
|
||||
127.0.0.1 haha.awesomedev.test
|
||||
|
||||
저장 후 브라우저에서 http://haha.awesomedev.test 접속
|
||||
→ 127.0.0.1(내 PC 자신)로 가려다 실패하거나,
|
||||
로컬 개발 서버가 떠 있다면 그 화면이 보여요!
|
||||
|
||||
원리: OS는 DNS에 물어보기 전에 이 파일부터 봅니다.
|
||||
여기 적힌 이름은 인터넷에 안 물어보고 즉시 그 IP로 연결해요.
|
||||
|
||||
실험 끝나면 추가한 줄을 꼭 지우고 저장! (원상복구)`;
|
||||
|
||||
const CODE_NSLOOKUP_DEEP = `# 1) 기본 조회 — 우리 플랫폼의 A 레코드
|
||||
nslookup edu.awesomedevapp.com
|
||||
|
||||
서버: ... ← 답해 준 리졸버 (보통 통신사 DNS)
|
||||
Name: edu.awesomedevapp.com
|
||||
Address: x.x.x.x ← 우리 EC2(서울)의 IP!
|
||||
|
||||
# 2) 레코드 타입을 콕 집어 물어보기
|
||||
nslookup -type=A edu.awesomedevapp.com # IPv4 주소
|
||||
nslookup -type=TXT awesomedevapp.com # TXT 레코드가 있다면 표시
|
||||
|
||||
# 3) 리졸버를 바꿔서 물어보기 (뒤에 DNS 서버 지정)
|
||||
nslookup edu.awesomedevapp.com 8.8.8.8 # 구글 공용 DNS에게
|
||||
nslookup edu.awesomedevapp.com 1.1.1.1 # 다른 공용 DNS에게
|
||||
|
||||
# 두 리졸버의 답(과 응답 속도)이 같은지 비교해 보세요.
|
||||
# TTL이 남아 있는 리졸버는 캐시로 즉답, 처음 받는 리졸버는
|
||||
# 섹션 1의 풀코스 여행을 다녀와서 답합니다.`;
|
||||
|
||||
const CODE_ROUTE53 = `우리 도메인은 AWS Route53이 '권한 서버' 역할을 해요
|
||||
|
||||
[도메인 등록] awesomedevapp.com
|
||||
│ "이 도메인의 권한 서버는 Route53 이 4대다" 라고 등록기관에 신고
|
||||
▼
|
||||
[Route53 호스팅 존] awesomedevapp.com
|
||||
├── A edu.awesomedevapp.com → EC2(서울)의 공인 IP
|
||||
├── (필요 시) CNAME·TXT 레코드 추가
|
||||
└── 각 레코드마다 TTL 설정
|
||||
▼
|
||||
[EC2 서울] ── Caddy가 443 받음 ── React 프론트 + Spring Boot 백엔드 + PostgreSQL
|
||||
|
||||
즉, 여러분이 주소창에 edu.awesomedevapp.com 을 치면:
|
||||
섹션 1의 여행 끝에 Route53이 "서울 EC2의 IP"를 답하고,
|
||||
브라우저가 그 IP의 443 포트로 접속 → Caddy → 우리 서비스!`;
|
||||
|
||||
const CODE_DNS_QUIZ = `셀프 체크 5문항 — 말로 설명할 수 있으면 통과!
|
||||
|
||||
Q1. 루트 서버는 edu.awesomedevapp.com의 IP를 모르는데,
|
||||
왜 여행의 첫 목적지일까요? (섹션 1)
|
||||
|
||||
Q2. www를 CNAME으로 걸었는데 사이트가 안 열려요.
|
||||
별명 사슬의 끝에서 뭘 확인해야 할까요? (섹션 2)
|
||||
|
||||
Q3. 서버 IP를 바꿨는데 친구 PC에선 아직 옛날 화면이 보여요.
|
||||
고장일까요? 언제까지 기다리면 될까요? (섹션 3)
|
||||
|
||||
Q4. hosts 파일에 적은 주소는 왜 DNS 서버 설정과 상관없이
|
||||
항상 이길까요? (섹션 4)
|
||||
|
||||
Q5. nslookup 뒤에 8.8.8.8을 붙이는 건 무엇을 바꾸는 걸까요?
|
||||
권한 서버가 바뀌는 걸까요, 질문받는 심부름꾼이 바뀌는 걸까요? (섹션 5)
|
||||
|
||||
→ 막히는 문항은 해당 섹션의 "직접 확인해 보기"를 한 번 더!`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '재귀 조회의 여행' },
|
||||
{ n: 2, label: '레코드 타입' },
|
||||
{ n: 3, label: 'TTL과 캐시' },
|
||||
{ n: 4, label: 'hosts 파일 실습' },
|
||||
{ n: 5, label: 'nslookup 실습' },
|
||||
{ n: 6, label: '우리 Route53' },
|
||||
{ n: 7, label: '정리 퀴즈' },
|
||||
];
|
||||
|
||||
export default function DnsDeepPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 네트워크 코스의 DNS 섹션을 뿌리까지 파고드는 심화 코스 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>DNS 깊이 보기<br />— 전화번호부의 뒷면까지</h1>
|
||||
<p>
|
||||
네트워크 코스에서 "DNS는 인터넷의 전화번호부"라고 배웠죠. 이번엔 그 전화번호부를
|
||||
<strong> 누가, 어떤 순서로, 얼마나 오래 들고 다니는지</strong>까지 파고듭니다.
|
||||
마지막엔 우리 회사 도메인이 실제로 어떻게 굴러가는지(Route53) 직접 확인해요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 명령어 다수</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="재귀 조회의 전체 여행" sub="루트 → TLD → 권한 서버, 세 번의 환승">
|
||||
<p>
|
||||
"DNS 서버에 물어보면 IP를 알려준다"고 했는데 — 그 DNS 서버는 전 세계 도메인
|
||||
수억 개의 IP를 다 외우고 있을까요? <strong>아니요.</strong> DNS의 진짜 설계는
|
||||
"다 아는 한 명"이 아니라 <strong>"담당자를 아는 사람들의 릴레이"</strong>예요.
|
||||
모르는 문제를 교무실에 물으러 갔더니 "그건 3학년부 선생님께", 3학년부에선
|
||||
"그건 담임 선생님께" 하고 정확한 담당자에게 안내받는 것과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_RECURSIVE_TRIP}</Code>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>리졸버(Resolver)</strong> — 내 PC 대신 뛰어다니는 심부름꾼.
|
||||
보통 통신사가 운영하고, 이 심부름을 "재귀(recursive) 조회"라고 불러요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>루트 서버</strong> — 여행의 출발점. 도메인을 뒤에서부터 읽어
|
||||
"<span className="icode">.com</span>이면 저쪽" 하고 TLD 서버를 안내해요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>TLD 서버</strong> — <span className="icode">.com</span>·
|
||||
<span className="icode">.dev</span>·<span className="icode">.kr</span> 같은
|
||||
최상위 도메인별 안내소. "<span className="icode">awesomedevapp.com</span>의
|
||||
담당은 저기"라고 권한 서버를 알려줘요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>권한(Authoritative) 서버</strong> — 그 도메인의 진짜 명부를 가진 최종 담당자.
|
||||
우리 도메인은 <strong>AWS Route53</strong>이 이 역할이에요(섹션 6에서 자세히!).
|
||||
</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>비유 한 줄 정리</b> 도메인은 <strong>뒤에서부터</strong> 읽는 주소예요.
|
||||
<span className="icode">edu.awesomedevapp.com</span> =
|
||||
"com 나라 → awesomedevapp 동네 → edu 건물". 여행도 정확히 그 순서로 갑니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="레코드 타입 — 전화번호부의 항목 종류" sub="A · CNAME · MX · TXT">
|
||||
<p>
|
||||
전화번호부에도 "집 전화", "휴대폰", "팩스" 칸이 나뉘어 있듯이,
|
||||
DNS의 답변에도 <strong>종류(레코드 타입)</strong>가 있어요.
|
||||
같은 도메인이라도 "IP 주세요(A)"와 "메일 서버 주세요(MX)"는 다른 질문이고,
|
||||
답도 다르게 옵니다.
|
||||
</p>
|
||||
<Code>{CODE_RECORD_TYPES}</Code>
|
||||
<p>
|
||||
<strong>A와 CNAME의 차이</strong>가 제일 헷갈려요. A는
|
||||
<strong> "이름 → 숫자"</strong>로 여행이 끝나는 종착역이고, CNAME은
|
||||
<strong> "이름 → 다른 이름"</strong>이라 한 정거장 더 가야 해요.
|
||||
별명(CNAME)을 아무리 겹쳐도 사슬의 맨 끝에는 반드시 A 레코드가 있어야
|
||||
브라우저가 실제 IP에 도착합니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>실무 함정</b> 도메인의 맨 꼭대기(루트, <span className="icode">awesomedevapp.com</span> 자체)에는
|
||||
원칙적으로 CNAME을 못 걸어요. 그래서 Route53에는 이를 우회하는
|
||||
"별칭(Alias)" 기능이 따로 있습니다 — "규칙엔 없는데 다들 필요해서 만든 편법"이
|
||||
실무엔 종종 있어요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="TTL과 캐시 — 전파가 늦는 진짜 이유" sub="우리 도메인 등록 때 실제로 겪은 일">
|
||||
<p>
|
||||
섹션 1의 풀코스 여행은 사실 <strong>매번 일어나지 않아요</strong>. 리졸버도,
|
||||
내 PC도, 브라우저도 한 번 받은 답을 <strong>캐시(임시 저장)</strong>해 두고 재사용하거든요.
|
||||
"몇 초 동안 재사용해도 되는지"를 정하는 게 <strong>TTL</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_TTL}</Code>
|
||||
<p>
|
||||
<strong>우리 회사 실화</strong> — <span className="icode">edu.awesomedevapp.com</span>을
|
||||
처음 등록하던 날, 사무실 PC에서는 사이트가 열리는데 어떤 폰에서는 한동안
|
||||
"사이트에 연결할 수 없음"이 떴어요. 고장이 아니었습니다. 각자 쓰는 통신사
|
||||
리졸버가 <strong>"그런 이름 없음"이라는 옛 답을 캐시</strong>하고 있었던 거예요.
|
||||
기다리니 자연히 풀렸죠. 흔히 "DNS 전파가 느리다"고 말하는 현상의 정체는
|
||||
전파가 아니라 <strong>세계 곳곳의 캐시가 각자 만료되기를 기다리는 것</strong>입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> Windows에서 내 PC의 DNS 캐시를 구경하고 비워 볼 수 있어요.
|
||||
터미널에서 <span className="icode">ipconfig /displaydns</span>로 캐시 목록을 본 다음,
|
||||
<span className="icode"> ipconfig /flushdns</span>로 싹 비워 보세요. 비운 직후
|
||||
사이트에 접속하면 내 PC는 리졸버에게 다시 질문합니다. (사이트가 안 열리게 되는
|
||||
명령이 아니니 안심하고 해봐도 돼요.)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="[실습 1] hosts 파일 장난" sub="DNS를 통째로 건너뛰는 내 PC 전용 명부">
|
||||
<p>
|
||||
OS에는 DNS보다 <strong>먼저</strong> 찾아보는 나만의 수제 전화번호부가 있어요 —
|
||||
바로 <strong>hosts 파일</strong>입니다. 여기에 이름과 IP를 적어 두면 인터넷에
|
||||
물어보지도 않고 그 IP로 연결돼요. 개발자들이 "진짜 도메인을 내 로컬 서버로
|
||||
잠깐 돌려서 테스트"할 때 실제로 쓰는 기법입니다.
|
||||
</p>
|
||||
<Code>{CODE_HOSTS}</Code>
|
||||
<ol className="olist">
|
||||
<li>메모장을 <strong>관리자 권한으로 실행</strong>해 hosts 파일을 열어요.</li>
|
||||
<li>맨 아래에 <span className="icode">127.0.0.1 haha.awesomedev.test</span> 한 줄을 추가하고 저장해요.</li>
|
||||
<li>브라우저에서 <span className="icode">haha.awesomedev.test</span>로 접속해 보고, 어떤 일이 생기는지 관찰해요.</li>
|
||||
<li>실험이 끝나면 <strong>추가한 줄을 지우고 저장</strong>해 원래대로 돌려놔요.</li>
|
||||
</ol>
|
||||
<div className="warn">
|
||||
<b>안전 수칙</b> ① 원래 있던 내용은 절대 지우거나 고치지 마세요 — 한 줄
|
||||
<strong> 추가</strong>만! ② 실존 사이트(포털·우리 플랫폼 등)의 도메인은 적지 마세요.
|
||||
그 사이트가 내 PC에서만 이상하게 열리며 한참 헤매게 됩니다. ③ 실습용 가짜
|
||||
이름(<span className="icode">.test</span>로 끝나는 이름)만 쓰고, 끝나면 꼭 원상복구.
|
||||
학교·회사 공용 PC에서는 하지 마세요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>여기서 배우는 것</b> "이름 → IP" 변환에는 우선순위가 있다는 사실!
|
||||
브라우저 캐시 → OS 캐시 → <strong>hosts 파일</strong> → 리졸버(DNS) 순서로 찾고,
|
||||
먼저 답을 찾으면 뒤에는 안 물어봐요. 악성코드가 hosts를 조작해 가짜 사이트로
|
||||
유인하는 수법이 있는 것도 이 우선순위 때문 — 그래서 관리자 권한이 필요한 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="[실습 2] nslookup으로 우리 도메인 캐보기" sub="리졸버에게 직접 질문 던지기">
|
||||
<p>
|
||||
<span className="icode">nslookup</span>은 브라우저 없이 DNS 질문만 쏙 던져 보는
|
||||
도구예요. 네트워크 코스에서 한 번 써봤죠? 이번엔 <strong>레코드 타입을 지정</strong>하고,
|
||||
<strong> 질문받는 리졸버를 바꿔 가며</strong> 물어봅니다.
|
||||
</p>
|
||||
<Code>{CODE_NSLOOKUP_DEEP}</Code>
|
||||
<ol className="olist">
|
||||
<li>기본 조회로 <span className="icode">edu.awesomedevapp.com</span>의 IP를 확인해요. 이게 우리 서울 EC2의 공인 IP예요.</li>
|
||||
<li><span className="icode">-type=</span> 옵션으로 A·TXT를 각각 물어보고, 답의 모양이 어떻게 다른지 비교해요.</li>
|
||||
<li>명령 끝에 <span className="icode">8.8.8.8</span>·<span className="icode">1.1.1.1</span>을 붙여 다른 리졸버에게도 물어봐요 — 답이 같은가요?</li>
|
||||
<li>같은 질문을 곧바로 한 번 더 던져 보세요. 두 번째가 눈에 띄게 빠르다면, 방금 섹션 3의 캐시를 목격한 거예요.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>관찰 포인트</b> 어느 리졸버에 묻든 <strong>최종 답은 같아야 정상</strong>이에요.
|
||||
왜냐하면 누구에게 묻든 여행의 종착역은 똑같은 권한 서버(우리 Route53)니까요.
|
||||
만약 리졸버마다 답이 다르다면? 캐시 만료 시점이 달라서 생기는 일시적 현상이거나,
|
||||
레코드를 방금 바꾼 직후라는 신호입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="우리 도메인의 실제 구성 — Route53" sub="권한 서버 자리에 앉아 있는 AWS 서비스">
|
||||
<p>
|
||||
섹션 1 여행의 종착역, <strong>권한 서버</strong>를 우리 회사는 직접 운영하지 않아요.
|
||||
AWS의 DNS 서비스 <strong>Route53</strong>에 그 역할을 맡겼습니다. Route53 콘솔에서
|
||||
레코드를 추가·수정하면, 전 세계 리졸버가 받아 가는 "공식 답변"이 바뀌는 구조예요.
|
||||
</p>
|
||||
<Code>{CODE_ROUTE53}</Code>
|
||||
<p>
|
||||
<strong>호스팅 존(Hosted Zone)</strong>은 "<span className="icode">awesomedevapp.com</span>이라는
|
||||
도메인의 공식 명부 한 권"이라고 생각하면 돼요. 그 명부 안에 A·CNAME·TXT 같은
|
||||
레코드들이 줄줄이 적혀 있고, 각 줄마다 TTL이 붙어 있죠. 여러분이 섹션 5에서
|
||||
<span className="icode"> nslookup</span>으로 받은 답은 전부 이 명부에서 출발한 거예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>점 잇기</b> 이 코스의 전부가 한 줄로 이어집니다 —
|
||||
Route53 명부에 적힌 레코드(섹션 2)를, 리졸버가 여행 끝에 받아 와서(섹션 1),
|
||||
TTL만큼 캐시하고(섹션 3), 그걸 여러분이 <span className="icode">nslookup</span>으로
|
||||
들여다본 것(섹션 5). 단, 내 PC의 hosts 파일이 먼저 답하면 이 모든 게 생략(섹션 4)!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="정리 퀴즈 — 셀프 체크 5문항" sub="설명할 수 있어야 아는 것">
|
||||
<Code>{CODE_DNS_QUIZ}</Code>
|
||||
<div className="tip">
|
||||
<b>공부 팁</b> 특히 Q3(전파 지연)은 실무에서 반드시 다시 만나는 상황이에요.
|
||||
"고장인가?"와 "캐시 만료 대기 중인가?"를 구분해 말할 수 있으면,
|
||||
도메인 관련 장애의 절반은 이미 진단할 수 있는 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📇 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 주소창에 도메인을 치는 순간 벌어지는 릴레이(루트→TLD→권한 서버)와,
|
||||
그 답이 캐시로 도는 원리(TTL), 그리고 우리 도메인의 진짜 명부(Route53)까지
|
||||
알게 됐어요. 다음 과제로 <strong>오늘 배운 nslookup 결과를 캡처해 멘토에게
|
||||
설명</strong>해 보고, 전체 그림이 다시 궁금해지면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스의
|
||||
5·7섹션을 복습해 보세요 — DNS가 전체 여행의 첫 단추라는 게 새롭게 보일 거예요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
426
frontend/src/pages/courses/DockerIntroPage.jsx
Normal file
426
frontend/src/pages/courses/DockerIntroPage.jsx
Normal file
@ -0,0 +1,426 @@
|
||||
// 이 파일이 하는 일: "도커 입문" 코스 — "내 PC에선 되는데요"라는 오래된 비극을
|
||||
// 컨테이너로 없애는 여정. 이미지/컨테이너 개념부터 기본 명령, 우리 docker-compose.yml
|
||||
// 한 줄씩 해부, 볼륨·포트 매핑, 그리고 개발 DB를 직접 띄우는 실습까지 8개 섹션.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 정적 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 명령어·YAML 예시는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 명령어 상수들 ──
|
||||
|
||||
const CODE_CONTAINER_SHIP = `옛날 항구: 쌀가마·목재·기계를 제각각 배에 실음
|
||||
→ 싣고 내리는 데 며칠, 배마다 싣는 법이 다름, 자주 부서짐
|
||||
|
||||
컨테이너선의 발명: 뭘 넣든 겉은 똑같은 강철 상자
|
||||
→ 어느 항구, 어느 배, 어느 트럭이든 똑같이 다룰 수 있음
|
||||
|
||||
개발도 똑같아요:
|
||||
|
||||
[ 내 앱 + Java 21 + 라이브러리 + 설정 ] ← 전부 한 상자에!
|
||||
──────────────────────────────────────
|
||||
내 PC 동료 PC AWS EC2 서버
|
||||
(Windows) (Mac) (Linux)
|
||||
|
||||
상자(컨테이너) 안이 똑같으니, 어디서 실행해도 똑같이 돕니다.
|
||||
"내 PC에선 되는데요"의 원인 = PC마다 다른 '항구 사정'을
|
||||
상자가 통째로 없애 버린 거예요.`;
|
||||
|
||||
const CODE_IMAGE_VS_CONTAINER = `이미지 vs 컨테이너 = 붕어빵틀 vs 붕어빵
|
||||
|
||||
이미지 (붕어빵틀) 컨테이너 (붕어빵)
|
||||
────────────────────── ──────────────────────
|
||||
실행에 필요한 모든 것을 이미지를 '실행'한 실체
|
||||
구워 놓은 설계도·틀 (진짜로 돌아가는 프로그램)
|
||||
읽기 전용, 변하지 않음 만들고, 굽고, 먹고(삭제) 가능
|
||||
하나만 있으면 됨 틀 하나로 여러 개 찍어냄
|
||||
|
||||
postgres:17 이미지 하나로 → 개발용 DB 컨테이너 1개
|
||||
테스트용 DB 컨테이너 1개
|
||||
실험용 DB 컨테이너 1개 ... 무한 생산!
|
||||
|
||||
이미지는 Docker Hub 같은 창고에서 받아 옵니다:
|
||||
docker pull postgres:17 ← "postgres 틀의 17번 버전 주세요"
|
||||
(뒤의 :17을 '태그'라고 해요. 버전 표시 스티커!)`;
|
||||
|
||||
const CODE_VM_VS_DOCKER = `가상머신(VM) 도커 컨테이너
|
||||
────────────────────── ──────────────────────
|
||||
집 안에 '집을 통째로' 지음 집 안에 '방 하나'만 빌림
|
||||
OS부터 전부 새로 설치 호스트 OS의 부엌·화장실(커널) 공유
|
||||
부팅에 몇 분 시작에 몇 초 (사실상 즉시)
|
||||
수 GB~수십 GB 수십 MB~수백 MB
|
||||
한 PC에 몇 개 못 띄움 수십 개도 거뜬
|
||||
|
||||
공통점: 둘 다 '격리된 실행 환경'을 만든다
|
||||
차이점: VM은 하드웨어부터 흉내(무겁고 확실),
|
||||
도커는 OS 위에 칸막이만(가볍고 빠름)
|
||||
|
||||
우리 서버(AWS EC2)도 사실 거대한 컴퓨터를 잘라 쓰는
|
||||
가상머신이에요. 그 VM '안에서' 도커 컨테이너들이 돌아갑니다.
|
||||
→ 집(EC2) 안에 방(컨테이너) 여러 개!`;
|
||||
|
||||
const CODE_BASIC_COMMANDS = `# 도커 5대 기본기 — 이것만 알면 팀에서 대화가 됩니다
|
||||
|
||||
docker run hello-world # 이미지를 받아서 컨테이너로 실행
|
||||
docker ps # 지금 돌아가는 컨테이너 목록 (작업 관리자)
|
||||
docker ps -a # 멈춘 것까지 전부
|
||||
docker logs <이름> # 그 컨테이너가 찍은 로그 보기
|
||||
docker logs -f <이름> # 실시간으로 따라가며 보기 (f = follow)
|
||||
docker exec -it <이름> bash # 돌아가는 컨테이너 '안으로' 들어가기
|
||||
docker stop <이름> # 정중하게 멈추기
|
||||
|
||||
# 자주 쓰는 조합:
|
||||
docker run -d --name my-db postgres:17
|
||||
# │ │
|
||||
# │ └ 컨테이너에 붙일 이름 (없으면 랜덤 이름이 생겨요)
|
||||
# └ detached: 백그라운드 실행 (터미널을 안 잡아먹음)`;
|
||||
|
||||
const CODE_COMPOSE_FILE = `# 우리 mirim-app의 docker-compose.yml — 진짜 교재!
|
||||
|
||||
services: # ① "이 앱은 이런 부품들로 되어 있다" 선언
|
||||
db: # ② 부품 1: 데이터베이스
|
||||
image: postgres:17 # ③ 어떤 틀(이미지)로 만들지
|
||||
environment: # ④ 컨테이너 안에 넣어줄 설정값
|
||||
POSTGRES_DB: mirim
|
||||
POSTGRES_PASSWORD: \${DB_PASSWORD} # ⑤ 진짜 비번은 .env 파일에서!
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data # ⑥ 데이터 보존 (섹션 6에서!)
|
||||
ports:
|
||||
- "5432:5432" # ⑦ 포트 매핑 (섹션 7에서!)
|
||||
|
||||
backend: # ⑧ 부품 2: Spring Boot 서버
|
||||
build: ./backend # ⑨ 남의 이미지가 아니라 우리 코드로 직접 구움
|
||||
depends_on:
|
||||
- db # ⑩ "db 먼저 켜고 나 켜줘" 순서 약속
|
||||
environment:
|
||||
DB_URL: jdbc:postgresql://db:5432/mirim # ⑪ 주소가 'db'?!
|
||||
|
||||
frontend: # ⑫ 부품 3: React 앱
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
volumes:
|
||||
db-data: # ⑬ ⑥에서 쓴 데이터 창고를 등록`;
|
||||
|
||||
const CODE_COMPOSE_NOTES = `⑪이 제일 신기한 부분이에요. DB 주소가 IP가 아니라 그냥 'db'!
|
||||
|
||||
compose로 띄운 컨테이너들은 같은 가상 네트워크에 들어가고,
|
||||
서비스 이름(db, backend...)이 그대로 '도메인 이름'이 됩니다.
|
||||
네트워크 코스에서 배운 DNS가 컨테이너 세계 안에도 있는 거예요.
|
||||
|
||||
⑤의 \${DB_PASSWORD}도 중요: 비밀번호를 파일에 직접 쓰면
|
||||
Gitea에 올라가는 순간 전 직원에게 공개되는 셈이라,
|
||||
같은 폴더의 .env 파일(git에 안 올림!)에서 주입받습니다.`;
|
||||
|
||||
const CODE_VOLUME = `컨테이너는 '지우면 안이 통째로 사라지는' 일회용 도시락통이에요.
|
||||
|
||||
docker rm my-db → 그 안에 쌓은 DB 데이터도 함께 증발! 😱
|
||||
|
||||
그래서 볼륨(volume) = 컨테이너 밖에 두는 외장하드가 있습니다.
|
||||
|
||||
[ db 컨테이너 ] [ db-data 볼륨 ]
|
||||
/var/lib/postgresql/data ───→ (호스트 PC 어딘가에 안전 보관)
|
||||
↑ 컨테이너 안에서 이 폴더에 컨테이너가 죽어도
|
||||
쓰는 척하지만, 실제 저장은 → 여기 데이터는 그대로!
|
||||
|
||||
- db-data:/var/lib/postgresql/data
|
||||
│ └ 컨테이너 안의 경로 (포스트그레스가 데이터 쓰는 곳)
|
||||
└ 볼륨 이름 (외장하드 라벨)
|
||||
|
||||
컨테이너를 지우고 새로 띄워도 볼륨만 다시 꽂으면
|
||||
어제 만든 테이블과 데이터가 그대로 살아 있어요.`;
|
||||
|
||||
const CODE_PORT_MAPPING = `컨테이너는 기본적으로 '방음 처리된 방'이라 밖에서 못 두드려요.
|
||||
포트 매핑 = 방문에 구멍(문구멍)을 뚫어 바깥 세계와 연결하는 것.
|
||||
|
||||
ports:
|
||||
- "5432:5432"
|
||||
│ └ 컨테이너 안쪽 포트 (포스트그레스가 듣고 있는 문)
|
||||
└ 내 PC(호스트) 쪽 포트 (내가 두드릴 문)
|
||||
|
||||
내 PC의 localhost:5432 를 두드리면
|
||||
→ 도커가 컨테이너 안 5432번으로 그대로 전달!
|
||||
|
||||
숫자를 다르게 쓸 수도 있어요:
|
||||
- "15432:5432" ← 내 PC의 15432 → 컨테이너의 5432
|
||||
(내 PC에 이미 다른 프로그램이 5432를 쓰고 있을 때의 회피 기술)
|
||||
|
||||
네트워크 코스의 "IP=건물, 포트=방 번호" 기억나죠?
|
||||
포트 매핑은 '건물 정문의 우편함 번호'와 '실제 방 번호'를
|
||||
연결해 주는 안내판이에요.`;
|
||||
|
||||
const CODE_PRACTICE = `# 실습: docker compose로 개발용 PostgreSQL 띄우기 (5분 완성)
|
||||
|
||||
# 0) Docker Desktop이 켜져 있는지 확인 (고래 아이콘!)
|
||||
|
||||
# 1) 아무 데나 연습 폴더를 만들고 docker-compose.yml 파일 작성:
|
||||
services:
|
||||
db:
|
||||
image: postgres:17
|
||||
environment:
|
||||
POSTGRES_DB: practice
|
||||
POSTGRES_PASSWORD: devonly
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- practice-data:/var/lib/postgresql/data
|
||||
volumes:
|
||||
practice-data:
|
||||
|
||||
# 2) 그 폴더에서 터미널을 열고:
|
||||
docker compose up -d # 부품 전체 기동! (처음엔 이미지 다운로드로 1~2분)
|
||||
docker ps # postgres:17 컨테이너가 보이면 성공
|
||||
docker logs <컨테이너이름> # "ready to accept connections" 찾기
|
||||
|
||||
# 3) 진짜 DB인지 확인 — 컨테이너 안으로 들어가서 SQL 치기:
|
||||
docker exec -it <컨테이너이름> psql -U postgres -d practice
|
||||
practice=# CREATE TABLE hello (msg TEXT);
|
||||
practice=# INSERT INTO hello VALUES ('나의 첫 컨테이너 DB!');
|
||||
practice=# SELECT * FROM hello;
|
||||
practice=# \\q # psql 나가기
|
||||
|
||||
# 4) 볼륨의 마법 확인 — 컨테이너를 없앴다가 다시:
|
||||
docker compose down # 컨테이너 삭제! (볼륨은 남음)
|
||||
docker compose up -d # 새 컨테이너로 부활
|
||||
docker exec -it <컨테이너이름> psql -U postgres -d practice -c "SELECT * FROM hello;"
|
||||
# → 아까 넣은 데이터가 그대로! 이게 볼륨입니다.
|
||||
|
||||
# 5) 다 놀았으면 정리:
|
||||
docker compose down -v # -v: 볼륨까지 삭제 (연습용이니까)`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '왜 컨테이너인가' },
|
||||
{ n: 2, label: '이미지 vs 컨테이너' },
|
||||
{ n: 3, label: '가상머신과의 차이' },
|
||||
{ n: 4, label: '기본 명령 5가지' },
|
||||
{ n: 5, label: 'compose 해부' },
|
||||
{ n: 6, label: '볼륨 = 데이터 보존' },
|
||||
{ n: 7, label: '포트 매핑' },
|
||||
{ n: 8, label: '실습: DB 띄우기' },
|
||||
];
|
||||
|
||||
export default function DockerIntroPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 무엇을 없애 주는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>도커 입문<br />— "내 PC에선 되는데요"를 없애는 마법 상자</h1>
|
||||
<p>
|
||||
내 PC에선 잘 되는데 동료 PC나 서버에선 안 되는 이유는, 코드가 아니라
|
||||
<strong> 환경</strong>이 다르기 때문이에요. 도커는 앱과 환경을 통째로 상자에 담아
|
||||
어디서든 똑같이 실행하는 도구 — 우리 회사 서비스도 전부 이 상자로 배달됩니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 70분</span>
|
||||
<span className="chip">실습 2개 포함</span>
|
||||
<span className="chip">준비물: Docker Desktop</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="왜 컨테이너인가" sub={'"내 PC에선 되는데요"라는 오래된 비극'}>
|
||||
<p>
|
||||
이런 상황을 상상해 보세요. 내 PC에서 완벽하게 돌던 Spring Boot 프로젝트를 동료에게
|
||||
보냈더니 "안 돌아가는데?"라는 답이 옵니다. 알고 보니 동료 PC엔 Java 버전이 다르고,
|
||||
환경변수 하나가 빠져 있고, PostgreSQL 설정도 달랐던 거죠. 코드는 같은데
|
||||
<strong> 코드가 밟고 선 땅</strong>이 달랐던 겁니다.
|
||||
</p>
|
||||
<p>
|
||||
이 문제를 해운업계가 먼저 풀었어요. 바로 <strong>컨테이너선</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_CONTAINER_SHIP}</Code>
|
||||
<p>
|
||||
도커의 컨테이너도 똑같은 발상이에요. 앱뿐 아니라 <strong>앱이 필요로 하는 모든 것</strong>
|
||||
— 언어 런타임, 라이브러리, 설정 — 을 한 상자에 담습니다. 상자 안이 똑같으니
|
||||
내 PC든, 동료의 Mac이든, 서울 리전의 AWS EC2든 <strong>똑같이 돌아요</strong>.
|
||||
우리 학습 플랫폼(React + Spring Boot + PostgreSQL)이 서버에서 돌아가는 방식이
|
||||
정확히 이겁니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="이미지 vs 컨테이너" sub="붕어빵틀과 붕어빵">
|
||||
<p>
|
||||
도커 대화의 90%에 나오는 두 단어가 <strong>이미지</strong>와
|
||||
<strong> 컨테이너</strong>인데, 처음엔 헷갈려요. 붕어빵을 떠올리면 절대 안 헷갈립니다.
|
||||
</p>
|
||||
<Code>{CODE_IMAGE_VS_CONTAINER}</Code>
|
||||
<p>
|
||||
<strong>틀(이미지)은 하나, 붕어빵(컨테이너)은 여러 개</strong> — 이게 핵심이에요.
|
||||
<span className="icode">postgres:17</span> 이미지를 한 번만 받아 두면, 프로젝트마다
|
||||
독립된 DB 컨테이너를 몇 개든 찍어낼 수 있어요. 하나를 실험하다 망가뜨려도
|
||||
지우고 틀에서 새로 구우면 그만입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> Docker Desktop을 켜고 터미널에서{' '}
|
||||
<span className="icode">docker pull hello-world</span> →{' '}
|
||||
<span className="icode">docker images</span>를 쳐 보세요. 방금 받은 '틀' 목록이 보이면,
|
||||
이어서 <span className="icode">docker run hello-world</span>로 붕어빵을 하나 구워 보세요.
|
||||
환영 메시지가 나오면 도커 입문 성공!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="가상머신과 뭐가 달라요?" sub="집을 통째로 짓기 vs 방 하나 빌리기">
|
||||
<p>
|
||||
"격리된 환경"이라는 말만 들으면 <strong>가상머신(VM)</strong>과 비슷해 보여요.
|
||||
맞아요, 목적은 비슷합니다. 하지만 방법이 완전히 달라서 무게가 하늘과 땅 차이예요.
|
||||
</p>
|
||||
<Code>{CODE_VM_VS_DOCKER}</Code>
|
||||
<p>
|
||||
VM은 집 안에 <strong>집을 통째로 새로 짓는</strong> 방식이라 OS까지 전부 새로 설치해요.
|
||||
도커는 이미 있는 집(호스트 OS)의 부엌과 수도(커널)를 같이 쓰면서
|
||||
<strong> 방에 칸막이만 치는</strong> 방식이라, 몇 초 만에 뜨고 용량도 몇십 배 가볍습니다.
|
||||
우리 EC2 서버 한 대에 프론트·백엔드·DB·Caddy 컨테이너를 전부 띄울 수 있는 이유가
|
||||
바로 이 가벼움이에요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>가볍다 ≠ 항상 정답</b> — 커널을 공유하니까, 호스트와 완전히 다른 OS가 필요하거나
|
||||
더 강한 격리가 필요하면 여전히 VM을 씁니다. 도구엔 각자의 자리가 있어요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="기본 명령 5가지" sub="run · ps · logs · exec · stop — 이것만 있으면 대화 가능">
|
||||
<p>
|
||||
도커 명령은 수십 개지만, 실무에서 매일 쓰는 건 다섯 개예요. 각각을 한 문장으로
|
||||
기억해 두세요 — <strong>run은 굽기, ps는 작업 관리자, logs는 일기장, exec은 문 열고
|
||||
들어가기, stop은 정지</strong>.
|
||||
</p>
|
||||
<Code>{CODE_BASIC_COMMANDS}</Code>
|
||||
<p>
|
||||
특히 <span className="icode">docker logs</span>와{' '}
|
||||
<span className="icode">docker exec</span>은 문제가 생겼을 때의 생명줄이에요.
|
||||
"서버가 왜 죽었지?" → 먼저 <span className="icode">logs</span>로 일기장을 읽고,
|
||||
더 파야 하면 <span className="icode">exec</span>으로 컨테이너 안에 직접 들어가 봅니다.
|
||||
우리 서버에서 장애를 잡을 때도 이 순서 그대로예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 섹션 2에서 실행한 컨테이너들의 흔적을 추적해 보세요.{' '}
|
||||
<span className="icode">docker ps -a</span>를 치면 이미 종료된{' '}
|
||||
<span className="icode">hello-world</span> 컨테이너가 STATUS "Exited"로 남아 있어요.
|
||||
그 이름으로 <span className="icode">docker logs 이름</span>을 치면 아까 봤던
|
||||
환영 메시지가 다시 나옵니다 — 컨테이너가 죽어도 일기장은 남는다!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="우리 docker-compose.yml 한 줄씩 해부" sub="플랫폼 그 자체가 교재입니다">
|
||||
<p>
|
||||
컨테이너가 하나면 <span className="icode">docker run</span>으로 충분한데, 우리 앱처럼
|
||||
<strong> 프론트 + 백엔드 + DB</strong>가 한 팀으로 움직이면 매번 셋을 순서 맞춰 켜는 게
|
||||
고역이에요. 그래서 <strong>docker compose</strong> — 부품 목록과 연결 방법을 파일 하나에
|
||||
적어 두고 <span className="icode">docker compose up</span> 한 방으로 전체를 켜는
|
||||
도구가 있습니다. 여러분이 지금 보고 있는 이 플랫폼의 설계도를 같이 읽어 봐요.
|
||||
</p>
|
||||
<Code>{CODE_COMPOSE_FILE}</Code>
|
||||
<Code>{CODE_COMPOSE_NOTES}</Code>
|
||||
<p>
|
||||
⑨의 <span className="icode">build</span>와 ③의 <span className="icode">image</span>{' '}
|
||||
차이도 짚고 갈게요. PostgreSQL은 남이 잘 만들어 둔 틀을 그대로 쓰면 되니까{' '}
|
||||
<span className="icode">image:</span>로 받아 오고, 우리 백엔드·프론트는 세상에 없는
|
||||
우리 코드니까 <span className="icode">build:</span>로 <strong>직접 틀을 굽습니다</strong>
|
||||
(그 굽는 레시피가 각 폴더의 Dockerfile이에요 — 다음 코스에서 자세히!).
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="볼륨 — 컨테이너가 죽어도 데이터는 산다" sub="일회용 도시락통과 외장하드">
|
||||
<p>
|
||||
컨테이너의 가장 큰 장점은 "지우고 새로 만들면 그만"인 일회용성이에요. 그런데
|
||||
DB 컨테이너를 지우면? <strong>안에 쌓은 데이터도 같이 사라집니다.</strong>
|
||||
한 달 치 학습 기록이 증발하는 대참사죠. 그래서 데이터는 컨테이너 <strong>밖</strong>에
|
||||
둡니다 — 그게 <strong>볼륨</strong>이에요.
|
||||
</p>
|
||||
<Code>{CODE_VOLUME}</Code>
|
||||
<p>
|
||||
섹션 5의 ⑥번 줄이 바로 이거였어요. 우리 플랫폼의 DB 컨테이너는 업데이트 때마다
|
||||
지워지고 새로 만들어지지만, 여러분의 계정·진도·과제 데이터는{' '}
|
||||
<span className="icode">db-data</span> 볼륨에 있어서 멀쩡합니다.
|
||||
<strong> 컨테이너는 갈아 끼우는 부품, 볼륨은 소중한 금고</strong> — 이 감각이 중요해요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="포트 매핑 — 상자에 문구멍 뚫기" sub="호스트 포트 : 컨테이너 포트">
|
||||
<p>
|
||||
컨테이너는 격리된 상자라서, 안에서 PostgreSQL이 아무리 열심히 돌아도 밖(내 PC)에서는
|
||||
기본적으로 접근할 수 없어요. 그래서 <strong>포트 매핑</strong>으로 내 PC의 문과
|
||||
컨테이너 안의 문을 연결해 줍니다.
|
||||
</p>
|
||||
<Code>{CODE_PORT_MAPPING}</Code>
|
||||
<p>
|
||||
왼쪽이 <strong>내 PC(호스트)</strong>, 오른쪽이 <strong>컨테이너 안</strong> —
|
||||
"<strong>바깥:안쪽</strong>" 순서만 외우면 됩니다. 헷갈리면 항상 이렇게 읽으세요:
|
||||
"내 PC의 <span className="icode">5432</span>를 두드리면, 컨테이너의{' '}
|
||||
<span className="icode">5432</span>로 통한다." 네트워크 코스(
|
||||
<Link to="/learn/network">네트워크의 이해</Link>)에서 배운 포트 개념이 여기서
|
||||
그대로 다시 나오죠 — 그 코스를 아직 안 들었다면 잠깐 다녀오는 것도 좋아요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="실습 — compose로 개발 DB 띄우기" sub="오늘 배운 전부를 5분 안에">
|
||||
<p>
|
||||
이제 손으로 마무리합시다. 아래 실습은 <strong>이미지 받기(2) → compose로 켜기(5) →
|
||||
exec으로 들어가기(4) → 볼륨으로 데이터 살리기(6) → 포트(7)</strong>까지 오늘 배운 걸
|
||||
전부 한 번씩 통과해요. 실제로 백엔드 개발자들이 새 프로젝트를 시작할 때 개발용 DB를
|
||||
준비하는 방법 그대로입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 아래를 순서대로 따라 하세요. 특히 <strong>4번 단계</strong> —
|
||||
컨테이너를 지웠다 다시 만들었는데 데이터가 살아 있는 순간이 이 코스의 하이라이트예요.
|
||||
꼭 눈으로 확인하세요.
|
||||
</div>
|
||||
<Code>{CODE_PRACTICE}</Code>
|
||||
<div className="warn">
|
||||
<b>흔한 막힘 포인트</b> — <span className="icode">port is already allocated</span>{' '}
|
||||
에러가 나면 내 PC의 5432번 방을 다른 프로그램(예: 로컬에 설치한 PostgreSQL)이 이미
|
||||
쓰고 있는 거예요. 섹션 7에서 배운 회피 기술로{' '}
|
||||
<span className="icode">"15432:5432"</span>처럼 바깥 번호만 바꿔 보세요.
|
||||
그리고 실습용 비밀번호 <span className="icode">devonly</span>는 연습 전용 —
|
||||
진짜 프로젝트에선 반드시 <span className="icode">.env</span>로 관리합니다(섹션 5의 ⑤!).
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🐳 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 <strong>이미지와 컨테이너를 구분</strong>하고, <strong>기본 명령 5가지</strong>로
|
||||
컨테이너를 다루고, 우리 <strong>docker-compose.yml을 읽을 줄</strong> 알고,
|
||||
<strong> 볼륨과 포트 매핑</strong>의 이유를 설명할 수 있어요. 방금 띄운 개발 DB는
|
||||
다음 과제에서 진짜로 씁니다 — <Link to="/learn/network"><strong>네트워크의 이해</strong></Link>에서
|
||||
배운 포트 개념이 흐릿하면 복습하고, 준비가 됐다면 멘토에게 다음 과제
|
||||
(Spring Boot를 방금 띄운 DB에 연결하기)를 요청해 보세요. 컨테이너선은 이미 출항했습니다!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
355
frontend/src/pages/courses/FilesPage.jsx
Normal file
355
frontend/src/pages/courses/FilesPage.jsx
Normal file
@ -0,0 +1,355 @@
|
||||
// 이 파일이 하는 일: "파일과 폴더의 원리" 코스 — 파일시스템이라는 사서에서 출발해
|
||||
// 확장자·경로·숨김 파일·휴지통·압축까지, 매일 쓰는 파일의 뒤편을 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (파일시스템 → 확장자의 진실 → 절대/상대 경로 → 숨김·시스템 파일 → 휴지통과 완전삭제 → 압축 한 입 → 확장자 표시 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_FILESYSTEM = `디스크의 민낯 = 그냥 거대한 0과 1의 바다
|
||||
|
||||
[디스크 실제 모습] 0110100101110... (수조 개의 비트가 쭉 이어져 있을 뿐)
|
||||
|
||||
여기에 파일시스템(사서)이 '장부'를 얹으면:
|
||||
|
||||
장부(메타데이터) 실제 데이터
|
||||
───────────────────────── ──────────────
|
||||
보고서.docx │ 크기 24KB │ 위치 #4021 ──→ [#4021~#4026 구역]
|
||||
사진.jpg │ 크기 3MB │ 위치 #7113 ──→ [#7113~#7800 구역]
|
||||
|
||||
대표 선수들:
|
||||
NTFS Windows 기본 (권한·저널링 지원)
|
||||
exFAT USB·SD카드 단골 (양쪽 OS에서 잘 읽힘)
|
||||
ext4 리눅스 기본 — 우리 EC2 서버가 쓰는 것!`;
|
||||
|
||||
const CODE_EXTENSION = `확장자는 '내용물'이 아니라 '이름표'일 뿐이에요.
|
||||
|
||||
photo.jpg 의 진짜 정체는 파일 맨 앞 몇 바이트(매직 넘버)에 있어요:
|
||||
|
||||
파일 종류 맨 앞 바이트(16진수) 사람 눈으로 보면
|
||||
─────────────────────────────────────────────
|
||||
JPG FF D8 FF (알 수 없는 문자)
|
||||
PNG 89 50 4E 47 .PNG
|
||||
ZIP 50 4B 03 04 PK..
|
||||
PDF 25 50 44 46 %PDF
|
||||
|
||||
photo.jpg → photo.txt 로 이름을 바꾸면?
|
||||
- 내용물(바이트)은 1도 안 변함. 여전히 JPG 데이터.
|
||||
- 단지 Windows가 "텍스트구나!" 하고 메모장으로 열 뿐.
|
||||
- 메모장엔 외계어가 가득 → 다시 .jpg로 바꾸면 멀쩡히 열림!`;
|
||||
|
||||
const CODE_PATH = `경로 = 파일까지 찾아가는 '길 안내'
|
||||
|
||||
절대 경로 — 뿌리(루트)부터 전체 주소
|
||||
Windows: C:\\awesomedev\\mirim-app\\frontend\\src\\App.jsx
|
||||
리눅스: /home/ubuntu/mirim-app/frontend/src/App.jsx
|
||||
(드라이브 문자가 없고, / 가 뿌리!)
|
||||
|
||||
상대 경로 — "지금 내가 서 있는 곳" 기준
|
||||
현재 위치가 frontend/src/pages/ 라면:
|
||||
./courses/FilesPage.jsx ← 여기서 한 층 내려가기 (.= 현재 폴더)
|
||||
../App.jsx ← 한 층 올라가기 (..= 부모 폴더)
|
||||
../../package.json ← 두 층 올라가기
|
||||
|
||||
React 코드에서 매일 보는 그 줄이 바로 상대 경로예요:
|
||||
import App from '../App'; ← "내 부모 폴더의 App 파일"`;
|
||||
|
||||
const CODE_HIDDEN = `숨김 파일 — 감춰진 게 아니라 '표시 안 함' 체크된 것
|
||||
|
||||
Windows: 파일 속성에 '숨김' 딱지 하나 붙은 것뿐
|
||||
리눅스: 이름이 점(.)으로 시작하면 자동으로 숨김!
|
||||
|
||||
개발자가 매일 만나는 점(.) 파일들 — 우리 저장소에도 있어요:
|
||||
.git/ Git이 커밋 역사를 통째로 보관하는 방
|
||||
.gitignore "이건 커밋하지 마" 목록
|
||||
.env 비밀번호·API 키 (그래서 .gitignore에 꼭 넣음!)
|
||||
|
||||
시스템 파일: OS가 "나 없으면 부팅 못 해" 하는 파일들.
|
||||
C:\\Windows\\System32 안의 파일들이 대표 — 구경은 OK, 삭제는 절대 금지.`;
|
||||
|
||||
const CODE_RECYCLE = `휴지통의 비밀: '삭제'는 사실 '이사'예요.
|
||||
|
||||
Delete 키 → 파일이 사라진 게 아니라
|
||||
C:\\$Recycle.Bin 이라는 숨김 폴더로 이사 + 원래 위치 기록
|
||||
복원 → 기록을 보고 원래 자리로 다시 이사
|
||||
|
||||
휴지통 비우기(또는 Shift+Delete) → 이때도 데이터는 안 지워져요!
|
||||
사서의 장부에서 "이 자리 비었음"이라고 지우개질만 함.
|
||||
책(데이터)은 서가에 그대로 → 새 파일이 덮어쓰기 전까지는
|
||||
복구 프로그램으로 살릴 수 있는 이유가 이것.
|
||||
|
||||
완전삭제가 필요하면? → 그 자리를 무의미한 값으로 여러 번 덮어쓰기.
|
||||
회사 PC 반납·중고 판매 전에 '디스크 완전 삭제'를 하는 이유예요.`;
|
||||
|
||||
const CODE_ZIP = `압축의 핵심 아이디어: '반복'을 짧게 줄여 쓰기
|
||||
|
||||
원본: aaaaaaaaaabbbbb (15글자)
|
||||
압축: a×10, b×5 (훨씬 짧다!)
|
||||
|
||||
실제 zip은 훨씬 똑똑한 사전(딕셔너리) 방식을 써요:
|
||||
"자주 나오는 패턴은 번호표로 바꿔치기"
|
||||
|
||||
그래서 파일마다 압축률이 달라요:
|
||||
회의록.txt (반복 많은 글) → 팍팍 줄어듦 (70~90% 감소)
|
||||
사진.jpg, 영상.mp4 → 거의 안 줄어듦!
|
||||
(이미 자기 방식으로 압축돼 있어서 더 짜낼 반복이 없음)
|
||||
|
||||
zip 하나로 폴더째 묶는 건 덤 — 파일 100개를 상자 하나에
|
||||
포장해서 옮기는 '묶음' 기능도 겸하고 있어요.`;
|
||||
|
||||
const CODE_SHOW_EXT = `탐색기에서 확장자 표시 켜기 (Windows 11 기준, 30초 컷)
|
||||
|
||||
1) 탐색기 열기 (Win + E)
|
||||
2) 상단 메뉴에서 [보기] 클릭
|
||||
3) [표시] → [파일 확장명] 체크!
|
||||
(같은 김에 [숨긴 항목]도 체크 — 섹션 4의 점 파일들이 보여요)
|
||||
|
||||
전: 보고서 사진 메모
|
||||
후: 보고서.docx 사진.jpg 메모.txt
|
||||
|
||||
왜 개발자는 무조건 켜 둘까?
|
||||
"invoice.pdf" 로 보이던 파일이 사실 "invoice.pdf.exe"(실행 파일!)
|
||||
일 수 있어요 — 악성코드의 단골 위장술. 확장자를 켜면 바로 들통납니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '파일시스템' },
|
||||
{ n: 2, label: '확장자의 진실' },
|
||||
{ n: 3, label: '절대/상대 경로' },
|
||||
{ n: 4, label: '숨김·시스템 파일' },
|
||||
{ n: 5, label: '휴지통과 완전삭제' },
|
||||
{ n: 6, label: '압축(zip) 한 입' },
|
||||
{ n: 7, label: '확장자 표시 실습' },
|
||||
];
|
||||
|
||||
export default function FilesPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 매일 쓰는 파일·폴더의 뒤편을 들여다보는 코스 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>파일과 폴더의 원리<br />— 더블클릭 뒤에 숨은 이야기</h1>
|
||||
<p>
|
||||
매일 만들고 지우는 파일, 그 뒤에서 디스크와 운영체제가 실제로 무슨 일을 하는지
|
||||
들여다봅니다. 확장자를 바꾸면 파일이 망가질까? 휴지통을 비우면 정말 사라질까? —
|
||||
이런 궁금증을 <strong>"직접 확인해 보기"</strong>로 내 PC에서 하나씩 실험해 보세요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 45분</span>
|
||||
<span className="chip">실습 4개</span>
|
||||
<span className="chip">준비물: Windows PC 하나</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="파일시스템 — 디스크의 사서" sub="NTFS, exFAT, ext4... 이름은 달라도 하는 일은 하나">
|
||||
<p>
|
||||
디스크를 현미경으로 보면 파일도 폴더도 없어요. 그냥 <strong>0과 1이 수조 개</strong> 늘어서
|
||||
있을 뿐이죠. 이 바다에서 "보고서.docx는 여기부터 여기까지"라고 <strong>장부를 관리해 주는
|
||||
사서</strong>가 바로 <strong>파일시스템</strong>입니다. 도서관에 사서가 없으면 책이 아무리
|
||||
많아도 한 권도 못 찾는 것처럼, 파일시스템이 없는 디스크는 그냥 비트 덩어리예요.
|
||||
</p>
|
||||
<Code>{CODE_FILESYSTEM}</Code>
|
||||
<p>
|
||||
폴더도 사실은 <strong>"이 안에 뭐가 있는지 적어 둔 목록 파일"</strong>에 가까워요.
|
||||
파일을 다른 폴더로 옮겨도(같은 디스크 안에서라면) 데이터는 그대로 두고
|
||||
<strong> 장부의 소속만 고쳐 씁니다</strong> — 그래서 몇 GB짜리 파일도 같은 드라이브 안
|
||||
이동은 순식간에 끝나요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>여기서 잠깐</b> USB를 다른 컴퓨터에 꽂았을 때 "포맷하시겠습니까?"가 뜨는 건,
|
||||
그 컴퓨터가 USB의 <strong>장부(파일시스템)를 못 읽는다</strong>는 뜻이에요.
|
||||
포맷 = 장부를 백지로 새로 쓰기. 그래서 포맷하면 파일이 다 사라지는 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="확장자의 진실" sub=".jpg를 .txt로 바꾸면 사진이 망가질까?">
|
||||
<p>
|
||||
결론부터: <strong>안 망가져요.</strong> 확장자는 파일 내용의 일부가 아니라
|
||||
<strong> 이름 끝에 붙은 이름표</strong>일 뿐입니다. 상자에 붙은 "유리컵" 스티커를
|
||||
"접시"로 바꿔 붙여도 안에 든 건 여전히 유리컵인 것과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_EXTENSION}</Code>
|
||||
<p>
|
||||
그럼 확장자는 왜 있을까요? Windows가 <strong>"어떤 프로그램으로 열지"</strong> 정하는
|
||||
기준이기 때문이에요. <span className="icode">.jpg</span>면 사진 뷰어,
|
||||
<span className="icode">.txt</span>면 메모장, <span className="icode">.jsx</span>면
|
||||
여러분의 코드 에디터. 파일의 정체는 그대로인데 <strong>여는 도구만 엉뚱하게
|
||||
바뀌는 것</strong> — 그게 확장자를 바꿨을 때 벌어지는 일의 전부입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 아무 사진이나 <strong>복사본을 만든 뒤</strong>(원본 보호!)
|
||||
이름을 <span className="icode">사진.txt</span>로 바꿔 메모장으로 열어 보세요.
|
||||
외계어가 가득하죠? 맨 앞에 <span className="icode">JFIF</span> 같은 글자가 보이면
|
||||
그게 JPG의 흔적이에요. 다시 <span className="icode">.jpg</span>로 되돌리면 —
|
||||
멀쩡히 열립니다. 내용물은 한 번도 변한 적이 없으니까요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="절대 경로와 상대 경로" sub="전체 주소로 안내할까, '여기서 두 블록'으로 안내할까">
|
||||
<p>
|
||||
친구에게 우리 회사를 알려 주는 두 가지 방법이 있어요.
|
||||
<strong>"서울시 ○○구 ○○로 12"</strong>처럼 전체 주소를 불러 주는 게
|
||||
<strong> 절대 경로</strong>, <strong>"지금 있는 카페에서 나와서 오른쪽으로 두 블록"</strong>이
|
||||
<strong> 상대 경로</strong>입니다. 상대 경로는 짧지만, <strong>출발점이 어디냐</strong>에
|
||||
따라 도착지가 완전히 달라진다는 게 포인트예요.
|
||||
</p>
|
||||
<Code>{CODE_PATH}</Code>
|
||||
<p>
|
||||
Windows는 <span className="icode">C:\</span>처럼 드라이브 문자에 역슬래시(\)를 쓰고,
|
||||
리눅스는 <span className="icode">/</span> 하나가 모든 것의 뿌리예요. 우리 서비스가
|
||||
돌아가는 <strong>AWS EC2 서버는 리눅스</strong>라서, 서버 작업을 하는 순간부터는
|
||||
<span className="icode">/home/ubuntu/...</span> 스타일과 친해져야 합니다 —
|
||||
이 감각은 <strong>리눅스 기초</strong> 코스에서 터미널로 직접 걸어 다니며 익혀요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>신입 개발자 단골 에러</b> <span className="icode">Module not found</span>는
|
||||
십중팔구 상대 경로 실수예요. <span className="icode">./</span>(현재 폴더)와
|
||||
<span className="icode">../</span>(부모 폴더)를 헷갈리면, "여기서 두 블록"을
|
||||
반대 방향으로 걸은 것과 같습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="숨김 파일과 시스템 파일" sub="개발자의 보물은 대부분 점(.)으로 시작한다">
|
||||
<p>
|
||||
숨김 파일은 비밀 파일이 아니에요. <strong>"평소엔 보여 주지 마"라는 딱지</strong>가
|
||||
붙어 있을 뿐이죠. 실수로 건드리면 곤란한 파일들을 초보자 눈에서 치워 두는,
|
||||
일종의 <strong>어린이 보호 캡</strong>입니다. 그런데 개발자가 되면 이 캡을 열 일이
|
||||
아주 많아져요.
|
||||
</p>
|
||||
<Code>{CODE_HIDDEN}</Code>
|
||||
<p>
|
||||
여러분이 Gitea(<span className="icode">edu.awesomedevapp.com</span>)에서 클론한
|
||||
저장소 폴더에도 <span className="icode">.git</span>이라는 숨김 폴더가 있어요.
|
||||
커밋 하나하나의 역사가 전부 그 안에 들어 있죠 — <strong>그 폴더를 지우는 순간
|
||||
평범한 폴더로 돌아가 버립니다.</strong> "저장소를 복사했는데 Git이 안 돼요"의
|
||||
단골 원인이, 숨김 폴더라 <span className="icode">.git</span>이 복사에서 빠진 경우예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 탐색기에서 [보기] → [표시] → <strong>[숨긴 항목]</strong>을
|
||||
켜고, 실습용으로 클론한 프로젝트 폴더를 열어 보세요. 반투명한
|
||||
<span className="icode">.git</span> 폴더가 나타나면 성공! 안을 구경하는 건 자유지만
|
||||
수정·삭제는 금지 — 여러분의 커밋 역사가 통째로 담긴 금고예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="휴지통의 원리와 완전삭제" sub="'삭제'라는 말의 세 가지 단계">
|
||||
<p>
|
||||
Delete 키를 눌렀을 때 파일이 곧바로 소멸한다고 생각하기 쉽지만, 실제로는
|
||||
<strong> 3단계의 안전장치</strong>가 있어요. ① 휴지통으로 이사 → ② 장부에서만 지움 →
|
||||
③ 진짜 덮어쓰기. 대부분의 "삭제"는 ①이나 ②에서 멈춥니다.
|
||||
</p>
|
||||
<Code>{CODE_RECYCLE}</Code>
|
||||
<p>
|
||||
도서관 비유로 다시 보면: 휴지통 비우기는 <strong>장부에서 책 제목을 지우는 것</strong>이지
|
||||
서가에서 책을 태우는 게 아니에요. 사서(파일시스템)는 "그 자리 비었음"으로 표시할 뿐이고,
|
||||
새 책이 그 자리를 차지하기 전까지 원래 책은 그대로 꽂혀 있죠. 삭제한 파일을
|
||||
복구하는 프로그램들은 이 <strong>"장부엔 없지만 서가엔 남은 책"</strong>을 찾아내는 도구예요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>회사에서 특히 중요한 이유</b> 고객 정보나 소스 코드가 담긴 디스크는 "삭제 +
|
||||
휴지통 비우기"만으로 반납하면 안 됩니다. 복구가 가능하니까요. PC 반납·교체 전엔
|
||||
반드시 정해진 <strong>완전삭제 절차</strong>를 따르세요 — 어썸데브도 장비 반납 시
|
||||
디스크 초기화를 규칙으로 두고 있어요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 메모장으로 <span className="icode">연습.txt</span>를 만들어
|
||||
Delete로 지운 뒤, 휴지통에서 <strong>[복원]</strong>을 눌러 보세요. 원래 폴더로
|
||||
정확히 돌아오죠? 휴지통이 "원래 위치"까지 기억하는 이사 센터라는 증거입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="압축(zip)의 원리 한 입" sub="반복을 찾아 줄여 쓰는 기술">
|
||||
<p>
|
||||
"아아아아아 진짜 최고"를 문자로 보낼 때 <strong>"아×5 진짜 최고"</strong>라고 줄여 쓸 수
|
||||
있죠? 압축의 기본 아이디어가 정확히 이거예요. 데이터 속에서 <strong>반복되는 패턴을 찾아
|
||||
더 짧은 표현으로 바꿔치기</strong>하고, 풀 때는 그 반대로 복원합니다. 원본과 100% 똑같이
|
||||
되살아나기 때문에 <strong>무손실 압축</strong>이라고 불러요.
|
||||
</p>
|
||||
<Code>{CODE_ZIP}</Code>
|
||||
<p>
|
||||
개발 현장에서도 압축은 공기처럼 쓰여요. <span className="icode">npm install</span>로
|
||||
받는 패키지도 압축된 채로 내려오고, 우리가 쓰는 <strong>Docker 이미지</strong>도 층층이
|
||||
압축돼서 EC2 서버로 전송됩니다. 서버가 브라우저에 보내는 응답도 gzip이라는 방식으로
|
||||
압축해서 보내는 경우가 많아요 — 압축 덕분에 인터넷이 몇 배는 빨라진 셈이죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 메모장에 아무 문장이나 쓰고 복사-붙여넣기로
|
||||
<strong> 수백 줄</strong>로 불린 뒤 저장하세요. 그 파일을 우클릭 →
|
||||
<strong> [ZIP 파일로 압축]</strong>. 원본과 zip의 크기를 비교하면 몇십 분의 일로
|
||||
줄어 있을 거예요. 이번엔 사진(.jpg)을 압축해 보세요 — 거의 안 줄어드는 걸 보면,
|
||||
"이미 압축된 건 더 못 짜낸다"를 눈으로 확인한 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습 — 탐색기에서 확장자 표시 켜기" sub="개발자 PC 셋업 1번, 지금 바로 30초">
|
||||
<p>
|
||||
Windows는 초보자를 배려해 확장자를 기본으로 숨겨요. 하지만 개발자에게 확장자는
|
||||
파일의 <strong>신분증</strong>입니다. <span className="icode">App.jsx</span>와
|
||||
<span className="icode">App.css</span>를 이름만으로 구분해야 하고, 위장한 실행
|
||||
파일도 걸러내야 하니까요. 어썸데브 개발 PC 셋업의 사실상 1번 항목입니다.
|
||||
</p>
|
||||
<Code>{CODE_SHOW_EXT}</Code>
|
||||
<p>
|
||||
켜고 나면 세상이 조금 다르게 보여요. 지금까지 "보고서"였던 파일이
|
||||
<span className="icode">보고서.docx</span>로, "index"였던 파일이
|
||||
<span className="icode">index.html</span>로 — 파일마다 정체가 이름에 드러납니다.
|
||||
섹션 2에서 한 확장자 바꾸기 실험도, 표시를 켜야 제대로 할 수 있어요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 위 순서대로 <strong>[파일 확장명]</strong>과
|
||||
<strong> [숨긴 항목]</strong>을 켠 뒤, 다운로드 폴더를 둘러보며 확장자를 하나씩
|
||||
읽어 보세요. 그리고 아직 안 했다면 섹션 2의 <span className="icode">.jpg</span> →
|
||||
<span className="icode">.txt</span> 실험을 지금 해 보세요 — 확장자 표시가 켜져
|
||||
있어야 이름 끝을 직접 고칠 수 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📁 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 파일은 "디스크의 사서가 장부로 관리하는 데이터 구역"이고, 확장자는 이름표,
|
||||
삭제는 장부 지우기라는 걸 알게 됐어요. 다음은 이 파일들 사이를 <strong>키보드만으로</strong>
|
||||
걸어 다닐 차례 — <Link to="/learn/linux"><strong>리눅스 기초</strong></Link> 코스에서
|
||||
오늘 배운 절대/상대 경로를 터미널 명령(<span className="icode">cd</span>,{' '}
|
||||
<span className="icode">ls</span>)으로 직접 밟아 보세요. 우리 서비스가 사는
|
||||
EC2 서버 속을 돌아다니는 첫걸음입니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
373
frontend/src/pages/courses/FirewallPage.jsx
Normal file
373
frontend/src/pages/courses/FirewallPage.jsx
Normal file
@ -0,0 +1,373 @@
|
||||
// 이 파일이 하는 일: "방화벽과 보안 기초" 코스 — 방화벽이라는 문지기의 역할에서 출발해
|
||||
// 포트의 의미, 우리 AWS 보안그룹의 실제 설정, 최소 권한 원칙, 흔한 공격 상식을 거쳐
|
||||
// 서버 보안 체크리스트까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (방화벽=문지기 → 포트 여닫기 → 윈도우 방화벽 실습 → AWS 보안그룹 실례
|
||||
// → 최소 권한 원칙 → 공격 유형 상식 → 보안 체크리스트 → 마무리 카드)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_GATEKEEPER = `방화벽 = 서버(건물) 정문의 문지기
|
||||
|
||||
┌─────────────────────────┐
|
||||
인터넷 ───▶ │ 방화벽(문지기) │ ───▶ 서버 내부
|
||||
│ "어디서 왔죠?" │
|
||||
│ "몇 호(포트)에 가시죠?" │
|
||||
│ → 규칙표와 대조 → 통과/차단│
|
||||
└─────────────────────────┘
|
||||
|
||||
두 방향을 다 지켜요:
|
||||
인바운드(Inbound) = 밖 → 안, "들어오는 손님" 검사 ← 보통 여기가 빡빡함
|
||||
아웃바운드(Outbound) = 안 → 밖, "나가는 사람" 검사 ← 보통 느슨하지만
|
||||
악성코드가 밖으로 정보를 빼돌릴 때 잡는 마지막 그물`;
|
||||
|
||||
const CODE_PORT_DOOR = `포트를 '연다/닫는다'는 것의 진짜 의미
|
||||
|
||||
서버 x.x.x.x (건물)
|
||||
├── :443 HTTPS [열림] ← 문지기: "443호 손님은 누구든 통과"
|
||||
├── :80 HTTP [열림] ← (들어오면 443으로 안내)
|
||||
├── :22 SSH [조건부] ← "사무실 IP에서 온 분만 통과"
|
||||
├── :5432 PostgreSQL [닫힘] ← "5432호요? 그런 방 없습니다" (묵묵부답)
|
||||
└── :8080 백엔드 [닫힘] ← 건물 안(같은 서버)에서만 노크 가능
|
||||
|
||||
포트가 '열려 있다' = 두 조건이 동시에 성립:
|
||||
1) 그 포트에서 귀 기울이는 프로그램이 실제로 실행 중이고 (방에 사람이 있고)
|
||||
2) 방화벽이 그 포트로의 접속을 허용함 (문지기가 통과시킴)
|
||||
|
||||
둘 중 하나라도 아니면 밖에서는 연결 불가!`;
|
||||
|
||||
const CODE_WIN_FIREWALL = `# 윈도우 방화벽 구경하기 (설정은 건드리지 말고 눈으로만!)
|
||||
|
||||
1) 시작 버튼 → "Windows Defender 방화벽" 검색 → 실행
|
||||
→ 내 PC가 지금 어떤 네트워크(개인/공용)로 분류돼 있는지 확인
|
||||
|
||||
2) 왼쪽 메뉴 "고급 설정" 클릭 → "인바운드 규칙" 선택
|
||||
→ 수백 개의 규칙이 주르륵! 프로그램마다
|
||||
"이 앱은 이 포트로 들어오는 연결 허용/차단" 규칙이 있어요
|
||||
|
||||
3) 아무 규칙이나 더블클릭 → [프로토콜 및 포트] 탭
|
||||
→ TCP/UDP, 포트 번호가 보이면 — 방금 배운 개념 그대로!
|
||||
|
||||
# 관찰 포인트: '차단' 규칙(빨간 아이콘)도 찾아보세요.
|
||||
# 기본값이 "명시적으로 허용된 것 외엔 차단"이라는 걸 확인할 수 있어요.`;
|
||||
|
||||
const CODE_NETSTAT = `# 내 PC에서 지금 열려 있는(듣고 있는) 포트 직접 보기
|
||||
# 터미널(PowerShell)에서:
|
||||
netstat -ano | findstr LISTENING
|
||||
|
||||
TCP 0.0.0.0:135 ... LISTENING ...
|
||||
TCP 127.0.0.1:5173 ... LISTENING ... ← Vite 개발 서버가 켜져 있다면!
|
||||
|
||||
# 0.0.0.0 = "모든 네트워크에서 듣는 중" (외부에서도 노크 가능)
|
||||
# 127.0.0.1 = "내 PC 안에서만 듣는 중" (밖에선 안 보임)
|
||||
# 맨 끝 숫자(PID)로 어떤 프로그램인지 추적:
|
||||
tasklist | findstr <PID>`;
|
||||
|
||||
const CODE_SECURITY_GROUP = `AWESOMEDEV 서버(AWS EC2 서울)의 보안그룹 — 실제 방침
|
||||
|
||||
인바운드 규칙 (들어오는 문)
|
||||
──────────────────────────────────────────────
|
||||
포트 대상 허용 범위 이유
|
||||
80 HTTP 전 세계(0.0.0.0/0) → Caddy가 443으로 안내
|
||||
443 HTTPS 전 세계(0.0.0.0/0) 웹 서비스니까 손님은 환영
|
||||
22 SSH(원격 접속) 지정한 IP만! 관리자 전용 뒷문
|
||||
|
||||
그 외 전부: 닫힘 (규칙에 없으면 = 차단)
|
||||
──────────────────────────────────────────────
|
||||
PostgreSQL(5432)? 닫힘 — DB는 같은 서버 안의 백엔드만 접근
|
||||
Spring Boot(8080)? 닫힘 — 밖에서는 Caddy를 거쳐야만 도달
|
||||
|
||||
아웃바운드: 허용 (서버가 배포 파일을 받거나 외부 API를 부를 수 있게)`;
|
||||
|
||||
const CODE_LEAST_PRIVILEGE = `최소 권한 원칙(Principle of Least Privilege)을 문에 비유하면:
|
||||
|
||||
나쁜 건물: "귀찮으니까 모든 방문을 다 열어 두자"
|
||||
→ 도둑이 한 번 들어오면 전 층을 다 털어 감
|
||||
|
||||
좋은 건물: "각자 자기 일에 필요한 문만, 필요한 만큼만"
|
||||
→ 손님(전 세계) : 로비(80/443)까지만
|
||||
→ 관리자(지정 IP) : 관리실(22)까지
|
||||
→ DB(5432) : 아예 외부 문 없음, 내부 복도로만 연결
|
||||
|
||||
코드·계정에도 똑같이 적용돼요:
|
||||
→ DB 계정: 서비스가 쓸 테이블 권한만 (DROP 권한 주지 않기)
|
||||
→ 배포 키: 배포에 필요한 저장소만 접근
|
||||
→ 신규 입사자 계정: 처음엔 최소로, 필요할 때 하나씩 추가`;
|
||||
|
||||
const CODE_ATTACKS = `서버를 인터넷에 올리는 순간 실제로 벌어지는 일들
|
||||
|
||||
1) 포트 스캔 (Port Scan) — "빈집 문고리 돌려보기"
|
||||
공격 봇이 1번부터 65535번까지 모든 포트를 두드려 봄
|
||||
→ 열린 포트 발견 = "이 건물엔 이 문이 열려 있네" 목록 작성
|
||||
→ 대응: 안 쓰는 포트는 전부 닫기 (보안그룹 기본 차단!)
|
||||
|
||||
2) 브루트포스 (Brute Force) — "비밀번호 무차별 대입"
|
||||
SSH(22)나 로그인 화면에 아이디/비번 조합을 기계로 수만 번 시도
|
||||
root/1234, admin/admin, admin/password ...
|
||||
→ 실제로 SSH를 전 세계에 열어 두면 몇 분 안에 시도 로그가 쌓여요
|
||||
→ 대응: 22번을 IP 제한 (우리 보안그룹!) + 강한 비밀번호/키 인증
|
||||
+ 로그인 시도 횟수 제한
|
||||
|
||||
핵심: 공격자는 '우리를 노려서'가 아니라 '봇으로 전부 다' 두드립니다.
|
||||
작은 회사 서버라고 안전한 게 아니에요 — 그래서 기본기가 중요!`;
|
||||
|
||||
const CODE_CHECKLIST = `AWESOMEDEV 서버 보안 체크리스트 — 새 서버를 열기 전 셀프 점검
|
||||
|
||||
[ ] 1. 보안그룹: 80/443만 전체 공개, 나머지는 전부 차단했는가?
|
||||
[ ] 2. SSH(22): 허용 IP를 지정했는가? (0.0.0.0/0 금지!)
|
||||
[ ] 3. SSH: 비밀번호 대신 키 파일 인증을 쓰는가?
|
||||
[ ] 4. DB(PostgreSQL): 외부 포트가 닫혀 있고, 접속 계정 권한이 최소인가?
|
||||
[ ] 5. 비밀값(DB 비번, API 키): 코드/Git에 없고 환경변수로 주입하는가?
|
||||
[ ] 6. HTTPS: Caddy가 인증서를 정상 발급/갱신하고 있는가?
|
||||
[ ] 7. OS·Docker 이미지: 보안 업데이트를 주기적으로 적용하는가?
|
||||
[ ] 8. 백업: DB 백업이 자동으로 돌고, 복원 테스트를 해봤는가?
|
||||
[ ] 9. 로그: 이상한 로그인 시도를 나중에 추적할 로그가 남는가?
|
||||
|
||||
→ 8번이 왜 '보안' 항목일까요?
|
||||
공격이나 실수로 데이터가 날아갔을 때 되살릴 수 있는 능력도 보안입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '방화벽 = 문지기' },
|
||||
{ n: 2, label: '포트를 여닫는다는 것' },
|
||||
{ n: 3, label: '윈도우 방화벽 구경' },
|
||||
{ n: 4, label: '우리 AWS 보안그룹' },
|
||||
{ n: 5, label: '최소 권한 원칙' },
|
||||
{ n: 6, label: '공격 유형 상식' },
|
||||
{ n: 7, label: '보안 체크리스트' },
|
||||
];
|
||||
|
||||
export default function FirewallPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>방화벽과 보안 기초<br />— 서버의 문을 지키는 법</h1>
|
||||
<p>
|
||||
서버를 인터넷에 올리는 순간, 전 세계의 공격 봇이 몇 분 안에 문을 두드리기
|
||||
시작해요. 이 코스에서는 방화벽이라는 문지기가 무엇을 검사하는지, 그리고
|
||||
우리 회사 AWS 서버가 실제로 어떤 규칙으로 문을 여닫는지 배웁니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 (내 PC에서)</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="방화벽 = 건물 정문의 문지기" sub="들어오는 손님도, 나가는 사람도 검사한다">
|
||||
<p>
|
||||
<strong>방화벽(Firewall)</strong>은 네트워크의 문지기예요. 서버(건물)를 드나드는
|
||||
모든 데이터에게 <strong>"어디서 왔죠? 몇 호(포트)에 가시죠?"</strong>를 묻고,
|
||||
미리 정해 둔 <strong>규칙표</strong>와 대조해서 통과시키거나 돌려보냅니다.
|
||||
규칙표에 없는 손님은? 기본은 <strong>차단</strong>이에요.
|
||||
</p>
|
||||
<Code>{CODE_GATEKEEPER}</Code>
|
||||
<p>
|
||||
방향이 두 가지라는 점이 중요해요. <strong>인바운드</strong>는 밖에서 서버로
|
||||
들어오는 연결(손님), <strong>아웃바운드</strong>는 서버에서 밖으로 나가는
|
||||
연결이에요. 공격은 대부분 인바운드로 들어오니까 인바운드 규칙이 빡빡하고,
|
||||
아웃바운드는 보통 느슨하게 둡니다 — 다만 악성코드가 서버 안에서 데이터를
|
||||
<strong> 밖으로 빼돌릴 때</strong>는 아웃바운드 검사가 마지막 그물이 돼요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>비유 하나 더</b> 학교 정문 지도 선생님을 떠올려 보세요. 등교(인바운드)는
|
||||
학생증을 꼼꼼히 확인하지만, 하교(아웃바운드)는 비교적 자유롭죠. 그래도
|
||||
수업 시간에 몰래 나가는 건(이상한 아웃바운드) 잡습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="포트를 '연다/닫는다'는 것" sub="방문을 여는 것과 문지기가 통과시키는 것, 둘 다 필요">
|
||||
<p>
|
||||
<Link to="/learn/network">네트워크의 이해</Link> 코스에서 <strong>IP는 건물 주소,
|
||||
포트는 방 번호</strong>라고 배웠죠. "포트를 연다"는 말은 그 방 번호로 오는 손님을
|
||||
문지기가 통과시키도록 규칙표에 적어 준다는 뜻이에요.
|
||||
</p>
|
||||
<Code>{CODE_PORT_DOOR}</Code>
|
||||
<p>
|
||||
여기서 중요한 사실 — 포트가 진짜로 "열려 있다"고 하려면 <strong>두 가지가
|
||||
동시에</strong> 필요해요. 방 안에 손님을 맞을 프로그램이 실제로 실행 중이어야
|
||||
하고(예: 443에서 기다리는 Caddy), 방화벽이 그 방으로 가는 길을 허용해야 하죠.
|
||||
프로그램만 켜고 방화벽이 막고 있으면? 밖에서는 아무리 노크해도 조용합니다.
|
||||
반대로 방화벽만 열고 프로그램이 없어도 연결은 안 돼요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>닫힌 포트의 좋은 매너: 묵묵부답</b> — "그 방은 잠겨 있어요"라고 친절하게
|
||||
답해 주는 것보다 <strong>아예 대답하지 않는 것</strong>이 더 안전해요.
|
||||
대답 자체가 "여기 서버가 살아 있다"는 힌트가 되기 때문이에요. AWS 보안그룹이
|
||||
기본으로 이렇게(응답 없이 버리기) 동작합니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="직접 확인해 보기 ① — 윈도우 방화벽 구경" sub="내 PC에도 문지기가 이미 일하고 있다">
|
||||
<p>
|
||||
방화벽은 서버에만 있는 게 아니에요. 지금 이 페이지를 보고 있는 여러분의
|
||||
윈도우 PC에도 문지기가 <strong>이미 근무 중</strong>입니다. 규칙표를 직접
|
||||
구경해 볼까요? (보기만 하고 <strong>설정은 바꾸지 마세요!</strong>)
|
||||
</p>
|
||||
<Code>{CODE_WIN_FIREWALL}</Code>
|
||||
<p>
|
||||
이어서 <strong>내 PC가 지금 어떤 포트에서 귀 기울이고 있는지</strong>도 직접
|
||||
볼 수 있어요. 개발 서버(<span className="icode">npm run dev</span>)를 켠 상태로
|
||||
해 보면 더 재밌습니다.
|
||||
</p>
|
||||
<Code>{CODE_NETSTAT}</Code>
|
||||
<div className="tip">
|
||||
<b>관찰 미션</b> <span className="icode">npm run dev</span>로 개발 서버를 켠 뒤
|
||||
<span className="icode"> netstat</span>을 다시 실행해서 <strong>5173 포트가 새로
|
||||
나타나는지</strong> 확인해 보세요. 끄면 사라지고요 — "포트가 열린다 = 프로그램이
|
||||
그 방에 입주한다"를 눈으로 확인하는 순간입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="우리 AWS 보안그룹 — 실제 규칙표" sub="80/443만 공개, SSH는 IP 제한 — 왜?">
|
||||
<p>
|
||||
이제 진짜 우리 이야기를 해볼게요. AWESOMEDEV의 서버는 <strong>AWS EC2(서울)</strong>에서
|
||||
돌고 있고, AWS에서 방화벽 역할을 하는 게 <strong>보안그룹(Security Group)</strong>이에요.
|
||||
서버 앞에 서 있는 클라우드 문지기라고 보면 됩니다. 우리 규칙표는 이렇게 생겼어요.
|
||||
</p>
|
||||
<Code>{CODE_SECURITY_GROUP}</Code>
|
||||
<p>왜 이렇게 정했는지 한 줄씩 뜯어 보면 —</p>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>80/443은 전 세계 공개</strong> — 웹 서비스니까요. 여러분이 학습 플랫폼에
|
||||
접속하려면 지구 어디서든 이 문은 열려 있어야 해요. 80으로 온 손님은
|
||||
Caddy가 "안전한 문(443)으로 오세요" 하고 안내합니다.
|
||||
</li>
|
||||
<li>
|
||||
<strong>SSH(22)는 지정 IP만</strong> — SSH는 서버를 원격 조종하는
|
||||
<strong> 관리자 전용 뒷문</strong>이에요. 뚫리면 서버 전체를 내주는 문이라,
|
||||
"정해진 곳에서 온 관리자만 통과"로 잠가 뒀어요. 전 세계에 열어 두는 순간
|
||||
공격 봇의 비밀번호 대입 시도가 쏟아집니다(섹션 6에서 자세히!).
|
||||
</li>
|
||||
<li>
|
||||
<strong>PostgreSQL(5432)·백엔드(8080)는 아예 비공개</strong> — DB는 같은 서버 안의
|
||||
Spring Boot만 접근하면 되고, 백엔드는 Caddy를 거쳐서만 만나면 돼요.
|
||||
밖에서 직접 노크할 이유가 없는 문은 <strong>존재 자체를 숨깁니다.</strong>
|
||||
</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 브라우저 주소창에 우리 도메인
|
||||
<span className="icode"> edu.awesomedevapp.com</span> 뒤에
|
||||
<span className="icode">:5432</span>를 붙여 접속해 보세요. 한참 기다리다
|
||||
실패하죠? 문지기가 <strong>응답 없이 버린 것</strong>입니다(섹션 2의 묵묵부답!).
|
||||
반면 그냥 도메인만 치면(443) 바로 열리고요 — 보안그룹 규칙표를 몸으로 확인한 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="최소 권한 원칙" sub="필요한 문만, 필요한 사람에게, 필요한 만큼만">
|
||||
<p>
|
||||
섹션 4의 규칙표를 관통하는 철학이 하나 있어요. 바로 <strong>최소 권한 원칙</strong>
|
||||
(Principle of Least Privilege) — <strong>"일하는 데 꼭 필요한 최소한의 권한만
|
||||
준다"</strong>는 보안의 제1원칙입니다.
|
||||
</p>
|
||||
<Code>{CODE_LEAST_PRIVILEGE}</Code>
|
||||
<p>
|
||||
핵심은 <strong>"뚫렸을 때 피해를 최소로"</strong>예요. 완벽한 방어는 없다고
|
||||
가정하고, 한 문이 뚫려도 도둑이 그 방 하나에서 멈추게 설계하는 거죠.
|
||||
모든 문을 열어 두면 편하긴 해요 — 하지만 편한 만큼 한 번의 사고가
|
||||
전체 사고가 됩니다. 보안은 늘 <strong>편리함과 안전함 사이의 저울질</strong>이고,
|
||||
기본값은 항상 "잠금"이어야 해요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>수습 개발자가 가장 흔하게 하는 실수</b> — "테스트하려고 잠깐 다 열어 뒀어요"를
|
||||
그대로 두는 것. 잠깐 연 문은 반드시 <strong>다시 닫는 것까지가 한 세트</strong>입니다.
|
||||
공격 봇은 여러분이 문을 여는 순간을 기다려 주지 않고, 항상 두드리고 있으니까요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="공격 유형 상식 — 포트 스캔과 브루트포스" sub="공격자는 '우리'가 아니라 '전부'를 노린다">
|
||||
<p>
|
||||
"우리같이 작은 회사를 누가 공격해요?"라고 생각하기 쉬운데, 현실은 반대예요.
|
||||
공격 대부분은 사람이 아니라 <strong>봇</strong>이 하고, 봇은 인터넷의
|
||||
<strong> 모든 서버를 자동으로</strong> 두드립니다. 표적이 아니라
|
||||
<strong> 그물</strong>이에요. 대표적인 두 가지만 알아 둡시다.
|
||||
</p>
|
||||
<Code>{CODE_ATTACKS}</Code>
|
||||
<p>
|
||||
<strong>포트 스캔</strong>은 도둑이 골목을 돌며 모든 집 문고리를 돌려 보는 것과
|
||||
같아요. 그 자체로 뭘 훔치진 않지만, "열린 문 목록"이 다음 공격의 지도가 되죠.
|
||||
<strong> 브루트포스</strong>는 그렇게 찾은 로그인 문 앞에서 비밀번호를 기계로
|
||||
수만 번 대입하는 공격이고요. 우리가 섹션 4에서 <strong>SSH를 IP 제한</strong>으로
|
||||
잠근 이유가 바로 이거예요 — 문고리를 돌려 볼 기회 자체를 주지 않는 겁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>우리 플랫폼과 연결하기</b> 로그인 화면에서 비밀번호를 여러 번 틀리면 잠시
|
||||
막아 두는 것, 비밀번호에 최소 길이를 요구하는 것 — 전부 브루트포스의 시도
|
||||
횟수를 견딜 수 없게 만드는 방어예요. 여러분이 만들 서비스의 로그인에도
|
||||
똑같이 넣어야 할 기본기입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="서버 보안 체크리스트" sub="새 서버를 인터넷에 올리기 전, 이것만은">
|
||||
<p>
|
||||
지금까지 배운 걸 <strong>실전 점검표</strong>로 정리했어요. 나중에 여러분이
|
||||
직접 EC2 인스턴스를 하나 받아서 서비스를 올리게 되는 날, 이 표를 그대로
|
||||
꺼내 쓰면 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_CHECKLIST}</Code>
|
||||
<p>
|
||||
1~4번은 이 코스에서 배운 <strong>문단속</strong>이고, 5번부터는 문 안쪽의
|
||||
살림살이 — 비밀값 관리, 암호화, 업데이트, 백업, 로그예요. 방화벽은 보안의
|
||||
<strong> 첫 번째 층</strong>일 뿐, 층을 여러 겹 쌓아야(이걸 <strong>심층 방어</strong>라고
|
||||
해요) 하나가 뚫려도 다음 층이 막아 줍니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>셀프 체크</b> 체크리스트의 각 항목을 보고 "이게 왜 필요하지?"를
|
||||
<strong> 말로 설명</strong>해 보세요. 1~4번은 이 코스의 섹션 번호와 연결되니,
|
||||
막히면 해당 섹션으로 돌아가 복습! 5~9번 중 낯선 항목은 멘토에게 물어보면
|
||||
우리 서버의 실제 설정을 보여 줄 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🛡️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 방화벽이 무엇을 검사하는지, 포트를 여닫는다는 게 무슨 뜻인지, 그리고
|
||||
우리 AWS 서버가 <strong>왜 80/443만 열고 SSH를 IP로 잠갔는지</strong>까지
|
||||
설명할 수 있게 됐어요. 핵심은 한 문장 — <strong>"필요한 문만 열고, 기본은
|
||||
잠금."</strong> 아직 <Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스를
|
||||
안 봤다면 IP·포트 개념을 먼저 다지고 오세요. 다음으로는 이 문단속 위에서
|
||||
실제 서비스가 어떻게 배포되는지, 배포 관련 코스와 과제에서 이어집니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
466
frontend/src/pages/courses/GitDeepPage.jsx
Normal file
466
frontend/src/pages/courses/GitDeepPage.jsx
Normal file
@ -0,0 +1,466 @@
|
||||
// 이 파일이 하는 일: "Git 깊이 보기" 코스 — 커밋을 스냅샷으로 이해하는 것에서 출발해
|
||||
// 브랜치·머지·충돌 해결·되돌리기·PR 리뷰 매너까지 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (스냅샷 커밋 → 브랜치 → 머지 vs 리베이스 → 충돌 실전 → .gitignore → log/diff/stash → 되돌리기 3형제 → PR 매너 + 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 예시 상수들 ──
|
||||
|
||||
const CODE_SNAPSHOT = `커밋 = 프로젝트 전체를 찍은 '사진' 한 장
|
||||
|
||||
시간 →
|
||||
[사진1]────[사진2]────[사진3]────[사진4]
|
||||
로그인 회원가입 버그 수정 디자인 개선
|
||||
페이지 폼 추가 (지금 여기, HEAD)
|
||||
|
||||
- 커밋마다 "그 순간 프로젝트 전체의 모습"이 통째로 기록돼요.
|
||||
(변경된 부분만 저장하는 게 아니라, 스냅샷을 찍되 안 바뀐 파일은
|
||||
이전 사진 것을 재활용 — 그래서 빠르고 용량도 작아요.)
|
||||
- 각 사진에는 고유 번호(해시)가 붙어요: a3f9c21 처럼.
|
||||
- HEAD = "내가 지금 보고 있는 사진"을 가리키는 손가락.`;
|
||||
|
||||
const CODE_THREE_ZONES = `Git의 3단계 — 사진을 찍기까지
|
||||
|
||||
[작업 폴더] → git add → [스테이지] → git commit → [저장소]
|
||||
코드를 고침 "이 파일들 찰칵 준비 완료 사진첩에
|
||||
사진에 넣자" 정식 보관
|
||||
|
||||
비유: 단체 사진을 찍을 때
|
||||
작업 폴더 = 교실에서 각자 놀고 있는 상태
|
||||
스테이지 = "사진 찍을 사람 이리 모여!" 하고 줄 세운 상태
|
||||
커밋 = 찰칵! 사진첩에 붙이고 날짜·설명 기록
|
||||
|
||||
git add를 왜 따로 하냐면 — 고친 것 중 '일부만' 골라서
|
||||
사진에 담을 수 있게 하려는 거예요. 관련 없는 수정을
|
||||
한 커밋에 뒤섞지 않는 게 좋은 습관의 시작입니다.`;
|
||||
|
||||
const CODE_BRANCH = `브랜치 = 평행우주 분기점
|
||||
|
||||
┌──[C4]──[C5] feature/login-page (실험 우주)
|
||||
[C1]──[C2]──[C3]
|
||||
└──[C6] main (원래 우주, 항상 안정)
|
||||
|
||||
- 브랜치를 만드는 건 "여기서부터 평행우주 하나 열게"라는 선언.
|
||||
- 실험 우주에서 아무리 코드를 부숴도 main 우주는 멀쩡해요.
|
||||
- 브랜치는 사실 커밋 하나를 가리키는 '이름표'일 뿐이라
|
||||
만드는 데 0.1초, 용량도 거의 0. 마음껏 만들어도 됩니다.
|
||||
|
||||
git switch -c feature/login-page # 평행우주 열고 이동
|
||||
git switch main # 원래 우주로 복귀`;
|
||||
|
||||
const CODE_MERGE_REBASE = `머지 vs 리베이스 — 평행우주를 합치는 두 가지 방법
|
||||
|
||||
① merge: 두 우주를 '합류'시키고 합류 지점을 기록
|
||||
┌──[C4]──[C5]──┐
|
||||
[C1]──[C2]──[C3] [M] ← 머지 커밋(합류 기념 사진)
|
||||
└──[C6]────────┘
|
||||
장점: 역사가 있는 그대로 남음 (누가 언제 갈라졌는지 다 보임)
|
||||
단점: 합류가 많아지면 그래프가 지하철 노선도처럼 복잡해짐
|
||||
|
||||
② rebase: 내 우주의 커밋들을 '들어 올려' 최신 지점 뒤로 옮겨 심기
|
||||
[C1]──[C2]──[C3]──[C6]──[C4']──[C5']
|
||||
장점: 역사가 한 줄로 깔끔해짐
|
||||
단점: 커밋을 '다시 쓰는' 것이라, 이미 팀원과 공유한
|
||||
커밋에 하면 대혼란 (같은 사진이 두 장 생김)
|
||||
|
||||
우리 팀 기본 규칙: 잘 모르겠으면 merge.
|
||||
rebase는 '아직 나 혼자만 가진 커밋'에만 쓰세요.`;
|
||||
|
||||
const CODE_CONFLICT = `충돌이 났을 때 파일에 나타나는 것 — 외계어 아님!
|
||||
|
||||
<<<<<<< HEAD
|
||||
const title = "어썸데브 학습 센터";
|
||||
=======
|
||||
const title = "AWESOMEDEV 학습 플랫폼";
|
||||
>>>>>>> feature/rename
|
||||
|
||||
읽는 법:
|
||||
<<<<<<< HEAD 부터 ======= 까지 → 내 우주(현재 브랜치)의 내용
|
||||
======= 부터 >>>>>>> 까지 → 상대 우주(합치려는 브랜치)의 내용
|
||||
|
||||
Git이 하는 말은 딱 하나예요:
|
||||
"같은 줄을 두 우주에서 다르게 고쳤는데, 어느 쪽이 맞는지
|
||||
나는 판단 못 하겠어. 사람이 골라 줘."
|
||||
|
||||
해결 절차 (3단계):
|
||||
1) 두 내용을 읽고 → 하나를 고르거나, 둘을 합쳐 새로 씀
|
||||
2) <<<<<<< ======= >>>>>>> 마커 줄들을 전부 지움
|
||||
3) git add 해당파일 → git commit (해결 완료 선언!)`;
|
||||
|
||||
const CODE_GITIGNORE = `.gitignore — "이건 사진에 찍지 마" 명단
|
||||
|
||||
# mirim-app 프론트엔드의 실제 .gitignore와 비슷한 예:
|
||||
node_modules/ # 라이브러리 창고 — npm install로 언제든 재생성
|
||||
dist/ # 빌드 결과물 — 소스에서 다시 만들 수 있음
|
||||
.env # 비밀번호·API 키 — 절대 저장소에 올리면 안 됨!
|
||||
*.log # 로그 파일 — 내 PC에서만 의미 있음
|
||||
|
||||
원리: git은 커밋할 때 이 명단의 파일들을 '아예 없는 셈' 쳐요.
|
||||
기준은 간단합니다 — "다시 만들 수 있거나, 비밀이거나, 나만의 것"
|
||||
이면 ignore. 손으로 쓴 소스 코드면 커밋.
|
||||
|
||||
주의: 이미 한 번 커밋된 파일은 나중에 .gitignore에 추가해도
|
||||
계속 추적돼요. (사진첩에 이미 붙은 사진은 명단으로 못 빼요 —
|
||||
git rm --cached 파일명 으로 추적을 먼저 끊어야 합니다.)`;
|
||||
|
||||
const CODE_LOG_DIFF_STASH = `사진첩을 탐험하는 세 가지 도구
|
||||
|
||||
# log — 사진첩 넘겨 보기
|
||||
git log --oneline --graph --all
|
||||
* a3f9c21 (HEAD -> main) 로그인 버그 수정
|
||||
* 7be0d44 회원가입 폼 추가
|
||||
* 1c2e9ab 프로젝트 시작
|
||||
|
||||
# diff — 두 사진의 '다른 그림 찾기'
|
||||
git diff # 작업 폴더 vs 마지막 커밋
|
||||
git diff main feature/x # 두 브랜치 사이 차이
|
||||
- const port = 3000; ← 빨간 줄: 지워진 내용
|
||||
+ const port = 8080; ← 초록 줄: 새로 생긴 내용
|
||||
|
||||
# stash — 하던 일 '서랍에 임시 보관'
|
||||
git stash # 지금 수정 중인 것들을 서랍에 넣고 폴더를 깨끗하게
|
||||
git stash pop # 서랍에서 도로 꺼내기
|
||||
쓰는 순간: 작업 중인데 "급한 버그부터 고쳐줘!" 요청이 왔을 때.
|
||||
커밋하기엔 애매한 미완성 코드를 잠시 치워두는 용도예요.`;
|
||||
|
||||
const CODE_UNDO_TRIO = `되돌리기 3형제 — 위험도 순으로 줄 세우기
|
||||
|
||||
① checkout(switch/restore) — 위험도 ★☆☆ "구경만"
|
||||
git checkout a3f9c21 # 과거 사진 시점으로 '구경' 가기
|
||||
git restore 파일명 # 파일 하나를 마지막 커밋 상태로 복원
|
||||
→ 역사를 바꾸지 않아요. 단, restore는 저장 안 한 수정을 날림!
|
||||
|
||||
② revert — 위험도 ★★☆ "반대 사진을 새로 찍기"
|
||||
git revert a3f9c21
|
||||
→ 그 커밋의 정반대 내용을 '새 커밋'으로 추가.
|
||||
역사를 지우지 않고 되돌려서, 팀과 공유한 브랜치에서도 안전.
|
||||
실무에서 "그 기능 롤백해 주세요"는 대부분 이것.
|
||||
|
||||
③ reset — 위험도 ★★★ "사진첩에서 사진을 뜯어냄"
|
||||
git reset --hard a3f9c21
|
||||
→ 그 이후 커밋들이 역사에서 사라짐. --hard는 작업 내용까지 삭제.
|
||||
이미 push한 커밋에 쓰면 팀원들의 역사와 어긋나 대참사.
|
||||
|
||||
한 줄 요약: 공유 전엔 reset도 OK, 공유 후엔 무조건 revert,
|
||||
확신이 없으면 일단 멘토에게. (reset --hard는 Ctrl+Z가 없어요!)`;
|
||||
|
||||
const CODE_PR_MANNERS = `PR(Pull Request) 리뷰 매너 — 주고받기 양쪽 다
|
||||
|
||||
[보내는 사람 (PR 올릴 때)]
|
||||
- 제목만 봐도 알게: "feat: 출석 체크 페이지 추가" (X: "수정함")
|
||||
- 설명에 3줄: 뭘 / 왜 / 어떻게 확인하면 되는지
|
||||
- 작게 쪼개기: 파일 30개짜리 PR은 리뷰가 아니라 고행이에요
|
||||
- 올리기 전 셀프 리뷰: 내 diff를 내가 먼저 한 번 읽기
|
||||
|
||||
[리뷰하는 사람 (댓글 달 때)]
|
||||
- 코드를 비판하지, 사람을 비판하지 않기
|
||||
(X) "이걸 왜 이렇게 짰어요?"
|
||||
(O) "이 부분은 map을 쓰면 더 짧아질 것 같아요. 어떻게 생각해요?"
|
||||
- 좋은 부분도 말하기: "이 변수 이름 정말 알아보기 쉽네요"
|
||||
- 제안과 필수를 구분: "취향 차이지만~" / "이건 버그라 꼭 수정 필요"
|
||||
|
||||
[받는 사람 (리뷰 답할 때)]
|
||||
- 리뷰는 나에 대한 공격이 아니라 코드를 향한 선물
|
||||
- 반영했으면 "반영했습니다 + 커밋 링크", 생각이 다르면 근거로 토론
|
||||
- 이해 안 되면 그냥 물어보기 — 질문은 신입의 특권입니다`;
|
||||
|
||||
const CODE_CONFLICT_LAB = `실습: 일부러 충돌을 만들어 해결해 보기 (내 PC, 10분)
|
||||
|
||||
# 0) 연습용 새 저장소 만들기 (진짜 프로젝트 아님, 마음껏 부숴도 됨)
|
||||
mkdir git-lab && cd git-lab
|
||||
git init
|
||||
echo 안녕하세요 > hello.txt
|
||||
git add . && git commit -m "첫 커밋"
|
||||
|
||||
# 1) 평행우주를 열고, 같은 줄을 다르게 고치기
|
||||
git switch -c universe-a
|
||||
echo 어썸데브 > hello.txt
|
||||
git commit -am "우주 A: 어썸데브로 변경"
|
||||
|
||||
git switch main
|
||||
echo 미림마이스터고 > hello.txt
|
||||
git commit -am "main: 미림으로 변경"
|
||||
|
||||
# 2) 합치기 → 충돌 발생! (당황 금지, 예정된 이벤트)
|
||||
git merge universe-a
|
||||
# CONFLICT (content): Merge conflict in hello.txt
|
||||
|
||||
# 3) hello.txt를 열면 <<<<<<< ======= >>>>>>> 마커가 보여요.
|
||||
# 섹션 4의 3단계대로: 내용 고르기 → 마커 지우기 → add & commit
|
||||
git add hello.txt
|
||||
git commit -m "충돌 해결: 두 문구를 합침"
|
||||
|
||||
# 4) 결과 감상
|
||||
git log --oneline --graph
|
||||
# 두 우주가 합류한 그래프가 보이면 성공!`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '커밋 = 스냅샷' },
|
||||
{ n: 2, label: '브랜치 = 평행우주' },
|
||||
{ n: 3, label: '머지 vs 리베이스' },
|
||||
{ n: 4, label: '충돌 해결 실전' },
|
||||
{ n: 5, label: '.gitignore' },
|
||||
{ n: 6, label: 'log·diff·stash' },
|
||||
{ n: 7, label: '되돌리기 3형제' },
|
||||
{ n: 8, label: 'PR 매너 + 실습' },
|
||||
];
|
||||
|
||||
export default function GitDeepPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 소프트웨어</div>
|
||||
<h1>Git 깊이 보기<br />— 사진첩에서 평행우주까지</h1>
|
||||
<p>
|
||||
add·commit·push를 외워서 쓰는 단계를 졸업하고, Git이 <strong>속으로 무슨 생각을
|
||||
하는지</strong> 이해하는 코스예요. 충돌 마커를 봐도 겁나지 않고, 실수를 해도
|
||||
스스로 되돌릴 수 있게 되는 것 — 그게 이 코스의 목표입니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 90분</span>
|
||||
<span className="chip">실습 3개 포함</span>
|
||||
<span className="chip">준비물: Git 설치된 내 PC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="커밋 = 프로젝트의 스냅샷" sub="Git은 변경 기록이 아니라 '사진첩'이다">
|
||||
<p>
|
||||
Git을 처음 배울 때 가장 큰 오해가 "커밋 = 바뀐 부분의 기록"이라는 거예요.
|
||||
아닙니다. 커밋은 <strong>그 순간 프로젝트 전체를 통째로 찍은 사진 한 장</strong>이에요.
|
||||
여러분의 폰 사진첩과 똑같아요 — 어제 사진과 오늘 사진은 각각 완전한 사진이지,
|
||||
"어제와 달라진 픽셀 목록"이 아니잖아요.
|
||||
</p>
|
||||
<Code>{CODE_SNAPSHOT}</Code>
|
||||
<p>
|
||||
그리고 사진을 찍기 전에 <strong>줄 세우는 단계</strong>가 하나 있어요.
|
||||
이게 처음엔 번거로워 보이지만, Git을 잘 쓰는 사람과 못 쓰는 사람을
|
||||
가르는 핵심 개념입니다.
|
||||
</p>
|
||||
<Code>{CODE_THREE_ZONES}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 학습 플랫폼 저장소를 클론받았다면 그 폴더에서{' '}
|
||||
<span className="icode">git log --oneline</span>을 쳐 보세요. 지금까지 찍힌
|
||||
사진(커밋)들의 목록이 최신순으로 나와요. 맨 위 7자리 코드가 바로 그 사진의
|
||||
고유 번호(해시)입니다. Gitea(edu.awesomedevapp.com)에서 같은 저장소의
|
||||
커밋 목록을 열어 두 화면이 똑같은지 비교해 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="브랜치 = 평행우주" sub="만드는 데 0.1초, 부숴도 안전한 실험실">
|
||||
<p>
|
||||
"지금 잘 돌아가는 코드를 건드리기 무서워요" — 모든 개발자의 공통 고민이고,
|
||||
Git의 답이 <strong>브랜치</strong>예요. 브랜치를 만드는 순간, 지금 이 지점에서
|
||||
<strong> 평행우주</strong>가 하나 열립니다. 그 우주에서 코드를 아무리 부수고
|
||||
실험해도 원래 우주(<span className="icode">main</span>)는 1도 안 변해요.
|
||||
</p>
|
||||
<Code>{CODE_BRANCH}</Code>
|
||||
<p>
|
||||
더 놀라운 건 비용이에요. 브랜치는 프로젝트를 복사하는 게 아니라
|
||||
<strong> 커밋 하나를 가리키는 이름표</strong>일 뿐이라, 파일이 10만 개여도
|
||||
브랜치 생성은 즉시 끝납니다. 그래서 실무에서는 <strong>기능 하나 = 브랜치 하나</strong>가
|
||||
기본이에요. 우리 회사도 <span className="icode">feature/기능이름</span> 브랜치에서
|
||||
작업하고, 완성되면 PR로 <span className="icode">main</span>에 합칩니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 실수</b> — <span className="icode">main</span>에서 바로 작업하다가
|
||||
"아 브랜치 안 만들었다!" 하는 경우. 괜찮아요, 커밋 전이라면{' '}
|
||||
<span className="icode">git switch -c feature/새브랜치</span>를 치는 순간
|
||||
지금까지의 수정 내용이 새 브랜치로 그대로 따라옵니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="머지 vs 리베이스" sub="평행우주를 합치는 두 가지 철학 (개념만!)">
|
||||
<p>
|
||||
평행우주를 열었으면 언젠가 다시 합쳐야죠. 방법이 두 가지인데,
|
||||
<strong> 결과 코드는 같아도 '역사책'이 다르게 쓰여요.</strong>
|
||||
</p>
|
||||
<Code>{CODE_MERGE_REBASE}</Code>
|
||||
<p>
|
||||
비유하면 이래요. <strong>머지</strong>는 "두 반이 따로 수학여행 갔다가 합류했다"고
|
||||
역사책에 있는 그대로 쓰는 것. <strong>리베이스</strong>는 "처음부터 한 반이
|
||||
쭉 같이 다닌 것처럼" 역사책을 다시 쓰는 것. 다시 쓴 역사책이 읽기엔 깔끔하지만,
|
||||
이미 옛 역사책을 <strong>복사해 간 사람(=push된 커밋을 받은 팀원)</strong>이 있다면
|
||||
두 역사책이 충돌해 난리가 나요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>수습 단계 처방전</b> — 지금은 <strong>merge만 써도 충분</strong>해요.
|
||||
리베이스는 "이런 게 있다"만 알아 두고, 나중에 혼자만의 로컬 커밋을 정리하고
|
||||
싶어질 때 멘토와 함께 처음 해 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="충돌 해결 실전" sub="<<<<<<< 마커는 에러가 아니라 질문이다">
|
||||
<p>
|
||||
두 우주에서 <strong>같은 파일의 같은 줄</strong>을 다르게 고쳤다면?
|
||||
Git은 어느 쪽이 맞는지 판단할 수 없으니 여러분에게 질문을 던져요.
|
||||
그 질문지가 바로 악명 높은 충돌 마커입니다. 처음 보면 파일이 깨진 것 같아
|
||||
심장이 철렁하지만 — <strong>파일은 안 깨졌고, Git은 화나지 않았어요.</strong>
|
||||
그냥 객관식 문제가 하나 출제된 것뿐이에요.
|
||||
</p>
|
||||
<Code>{CODE_CONFLICT}</Code>
|
||||
<p>
|
||||
답은 셋 중 하나예요. <strong>내 것을 고르거나, 상대 것을 고르거나,
|
||||
둘을 읽고 더 나은 제3의 코드를 새로 쓰거나.</strong> 셋 다 정답이 될 수 있고,
|
||||
뭐가 맞는지는 코드의 맥락이 정해 줍니다. VS Code를 쓰면 충돌 구간 위에
|
||||
"Accept Current / Accept Incoming / Accept Both" 버튼이 떠서 클릭으로도
|
||||
고를 수 있어요 — 하지만 원리(마커 읽기)를 알고 누르는 것과 모르고 누르는 것은
|
||||
하늘과 땅 차이입니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>진짜 사고는 이것</b> — 충돌 자체가 아니라, 마커를 <strong>안 지우고</strong>{' '}
|
||||
커밋해 버리는 것. <span className="icode"><<<<<<< HEAD</span> 같은
|
||||
줄이 코드에 남으면 당연히 빌드가 깨져요. 해결 후 커밋 전에 파일을 한 번 더
|
||||
훑어보는 습관을 들이세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title=".gitignore의 원리" sub="사진에 찍히면 안 되는 것들의 명단">
|
||||
<p>
|
||||
프로젝트 폴더의 모든 파일이 사진첩에 들어가야 할까요? 아니에요.{' '}
|
||||
<span className="icode">node_modules</span>는 라이브러리 수천 개가 든 창고라
|
||||
용량이 수백 MB인데, <span className="icode">npm install</span> 한 방이면 언제든
|
||||
다시 만들 수 있어요. <span className="icode">.env</span>에는 DB 비밀번호 같은
|
||||
비밀이 들어 있고요. 이런 것들은 <strong>사진에 찍히면 안 되는 명단</strong>,
|
||||
즉 <span className="icode">.gitignore</span>에 올립니다.
|
||||
</p>
|
||||
<Code>{CODE_GITIGNORE}</Code>
|
||||
<div className="warn">
|
||||
<b>비밀은 한 번 찍히면 끝</b> — <span className="icode">.env</span>를 실수로
|
||||
커밋해서 push까지 했다면, 나중에 지워도 <strong>과거 사진(커밋 이력)에는 영원히
|
||||
남아요.</strong> 이 경우 파일 삭제가 아니라 <strong>비밀번호·키 자체를 교체</strong>해야
|
||||
합니다. 사고가 났다면 숨기지 말고 즉시 멘토에게 알리세요 — 빠른 보고가
|
||||
가장 훌륭한 대처예요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 플랫폼 프론트엔드 폴더에서{' '}
|
||||
<span className="icode">.gitignore</span> 파일을 열어 보세요. 그리고{' '}
|
||||
<span className="icode">git status</span>를 쳤을 때{' '}
|
||||
<span className="icode">node_modules</span>가 목록에 <strong>안 보이는 것</strong>을
|
||||
확인하세요. 수천 개 파일이 있는데도 Git이 완벽하게 못 본 척하고 있는 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="log · diff · stash" sub="사진첩 넘기기, 다른 그림 찾기, 서랍에 넣어두기">
|
||||
<p>
|
||||
이 셋은 코드를 <strong>바꾸지 않는</strong> 탐험 도구(stash만 살짝 예외)라서
|
||||
아무리 써도 안전해요. 그래서 제일 자주, 제일 편하게 쓰게 되는 명령들입니다.
|
||||
</p>
|
||||
<Code>{CODE_LOG_DIFF_STASH}</Code>
|
||||
<p>
|
||||
특히 <span className="icode">git diff</span>는 <strong>커밋 버튼을 누르기 전의
|
||||
마지막 거울</strong>이에요. "내가 지금 뭘 바꿨더라?"를 눈으로 확인하고 커밋하는
|
||||
사람과, 확인 없이 <span className="icode">git add .</span>부터 치는 사람의
|
||||
커밋 품질은 시간이 갈수록 벌어집니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 아무 저장소에서 파일 하나를 살짝 고친 뒤{' '}
|
||||
<span className="icode">git diff</span> → <span className="icode">git stash</span>{' '}
|
||||
→ 파일이 원래대로 돌아간 것 확인 → <span className="icode">git stash pop</span> →
|
||||
수정이 되살아난 것까지 확인해 보세요. "서랍에 넣었다 꺼내기" 한 사이클을
|
||||
손으로 돌려 보면 절대 안 잊어버려요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="되돌리기 3형제" sub="checkout · revert · reset — 위험도가 전부 다르다">
|
||||
<p>
|
||||
실수했을 때 쓰는 명령이 세 개나 되는 이유는, <strong>"되돌린다"의 의미가
|
||||
세 가지</strong>이기 때문이에요. 과거를 <strong>구경</strong>만 할지,
|
||||
과거의 반대를 <strong>새로 기록</strong>할지, 아예 역사를 <strong>지울지</strong>.
|
||||
위험도가 완전히 다르니 꼭 구분해서 기억하세요.
|
||||
</p>
|
||||
<Code>{CODE_UNDO_TRIO}</Code>
|
||||
<p>
|
||||
기억법: <strong>checkout은 타임머신 관광</strong>(역사 그대로),{' '}
|
||||
<strong>revert는 정정 보도</strong>(틀린 기사를 지우지 않고 정정 기사를 새로 냄),{' '}
|
||||
<strong>reset은 신문사 창고 방화</strong>(기사 자체를 없앰 — 혼자 볼 일기장에서만!).
|
||||
push한 커밋 앞에서 <span className="icode">reset --hard</span>가 손가락에
|
||||
맴돌면, 그건 명령어를 칠 때가 아니라 멘토를 부를 때예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>안심 포인트</b> — 사실 Git에서 커밋까지 마친 코드는 웬만해선 안 사라져요.{' '}
|
||||
<span className="icode">git reflog</span>라는 "HEAD의 블랙박스"가 있어서,
|
||||
reset으로 날린 커밋도 대부분 구조할 수 있습니다. 그러니 실수해도 우선 침착하게 —
|
||||
단, <strong>커밋 안 한 수정</strong>은 구조 불가이니 작업이 아까우면 자주 커밋!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="PR 리뷰 매너 + 실습" sub="코드는 함께 만드는 것 — 그리고 충돌을 직접 겪어 보기">
|
||||
<p>
|
||||
어썸데브에서 여러분의 코드는 PR을 거쳐 <span className="icode">main</span>에
|
||||
들어가요. PR은 시험 제출이 아니라 <strong>대화의 시작</strong>입니다.
|
||||
리뷰를 주고받는 매너가 곧 팀 개발 실력이에요.
|
||||
</p>
|
||||
<Code>{CODE_PR_MANNERS}</Code>
|
||||
<p>
|
||||
마지막으로, 이 코스의 하이라이트 실습이에요. 충돌은 <strong>피하는 게 아니라
|
||||
익숙해지는 것</strong>이고, 익숙해지는 가장 빠른 길은 안전한 곳에서 일부러
|
||||
한 번 겪어 보는 겁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 (메인 실습)</b> 아래를 터미널에 한 줄씩 그대로 따라 치세요.
|
||||
연습용 폴더라 뭘 해도 안전해요. 3단계에서 충돌 마커를 만나면 —
|
||||
이제 여러분은 읽는 법을 압니다.
|
||||
</div>
|
||||
<Code>{CODE_CONFLICT_LAB}</Code>
|
||||
<div className="tip">
|
||||
<b>한 걸음 더</b> — 같은 실습을 Gitea(edu.awesomedevapp.com)의 개인 연습
|
||||
저장소에 push해서, 브랜치 두 개로 PR을 만들어 보세요. 웹 화면에서
|
||||
"충돌이 있어 자동 머지 불가" 표시가 뜨는 것까지 보면, 로컬과 원격의
|
||||
충돌 경험이 하나로 이어집니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🌿 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 커밋은 <strong>사진</strong>, 브랜치는 <strong>평행우주</strong>,
|
||||
충돌 마커는 <strong>객관식 질문</strong>으로 보일 거예요. 되돌리기 3형제의
|
||||
위험도를 구분할 줄 알고, PR에서 리뷰를 주고받는 매너까지 챙겼다면 팀 개발
|
||||
준비 완료입니다. 다음은 여러분이 만든 브랜치가 실제 서버에 올라가는 여정 —{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서
|
||||
push된 코드가 어떤 길로 배포 서버까지 달려가는지 이어서 배워 보세요.
|
||||
그리고 오늘 배운 충돌 해결 실습을 마쳤다면, 멘토에게{' '}
|
||||
<span className="icode">git log --oneline --graph</span> 결과를 보여 주며
|
||||
과제 체크를 받아 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
378
frontend/src/pages/courses/HistoryPage.jsx
Normal file
378
frontend/src/pages/courses/HistoryPage.jsx
Normal file
@ -0,0 +1,378 @@
|
||||
// 이 파일이 하는 일: "컴퓨터의 역사" 코스 — 방 하나를 가득 채우던 에니악에서
|
||||
// 주머니 속 스마트폰, 그리고 우리가 매일 쓰는 클라우드·AI 도구까지,
|
||||
// 컴퓨터가 걸어온 길을 7개 시대로 나눠 안내하는 정적 학습 페이지.
|
||||
// (에니악 → 트랜지스터·무어의 법칙 → PC 혁명 → 인터넷·웹 → 스마트폰 → 클라우드 → AI 시대)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_ENIAC = `에니악(ENIAC, 1946) vs 지금 여러분의 노트북
|
||||
|
||||
에니악 요즘 노트북
|
||||
────────────────────────────────────────────────────
|
||||
무게 약 27톤 (버스 2대) 약 1.5kg
|
||||
크기 교실 하나를 가득 채움 가방에 쏙
|
||||
부품 진공관 약 18,000개 트랜지스터 수백억 개
|
||||
전기 150kW (동네가 깜빡) 약 60W (전구 하나)
|
||||
프로그램 교체 케이블을 손으로 다시 꽂음 파일 하나 실행
|
||||
속도 초당 덧셈 약 5,000번 초당 수십억 번 이상
|
||||
|
||||
→ "프로그램을 바꾼다 = 배선을 다시 꽂는다"였던 시대.
|
||||
소프트웨어라는 말조차 아직 없던 출발점이에요.`;
|
||||
|
||||
const CODE_MOORE = `무어의 법칙 (1965, 고든 무어의 관찰)
|
||||
|
||||
"칩 하나에 들어가는 트랜지스터 수는 약 2년마다 2배가 된다"
|
||||
|
||||
1971년 Intel 4004 약 2,300개
|
||||
1993년 Pentium 약 310만 개
|
||||
2006년 Core 2 Duo 약 2억 9천만 개
|
||||
2020년대 스마트폰 칩 100억 개 이상
|
||||
────────────────────────────────────────
|
||||
50년 동안 대략 "2배 × 25번" — 계산해 보면 수백만 배!
|
||||
|
||||
법칙이라기보다 '업계 전체가 지켜온 약속'에 가까워요.
|
||||
그리고 이 약속 덕분에: 어제의 슈퍼컴퓨터 = 오늘의 주머니 속 폰.`;
|
||||
|
||||
const CODE_PC_ERA = `개인용 컴퓨터(PC) 혁명 — "1인 1컴퓨터"라는 미친 생각
|
||||
|
||||
~1970년대 컴퓨터 = 회사·대학의 공용 대형 장비
|
||||
(예약하고 줄 서서 잠깐 빌려 쓰는 물건)
|
||||
|
||||
1977~1984 가정용 PC 등장 — 거실과 책상 위로!
|
||||
+ GUI(그래픽 화면)와 마우스의 등장
|
||||
"명령어를 외운 사람"만 쓰던 기계가
|
||||
"아이콘을 클릭할 줄 아는 사람" 모두의 도구로
|
||||
|
||||
바뀐 것:
|
||||
- 소프트웨어 산업 탄생 (프로그램을 '사고파는' 시장)
|
||||
- 개발자라는 직업의 대중화
|
||||
- 지금 여러분이 PC로 코딩하는 풍경의 시작점`;
|
||||
|
||||
const CODE_WEB = `인터넷과 웹 — '연결'이 만든 폭발
|
||||
|
||||
1969 ARPANET — 대학 연구실 컴퓨터 몇 대를 연결한 실험
|
||||
1983 TCP/IP 표준화 — 서로 다른 네트워크가 한 언어로 대화
|
||||
1989 팀 버너스리, 웹(WWW) 제안 — "문서를 링크로 잇자"
|
||||
1990s 브라우저 등장 → 누구나 클릭만으로 인터넷 항해
|
||||
2000s 웹이 '문서'에서 '애플리케이션'으로 진화
|
||||
|
||||
핵심 구분 (헷갈리기 쉬워요!)
|
||||
인터넷 = 전 세계 컴퓨터를 잇는 도로망 (물리적 연결)
|
||||
웹 = 그 도로 위를 달리는 서비스 중 하나 (HTTP로 문서·앱 주고받기)
|
||||
|
||||
→ 우리가 만드는 React 페이지도, Spring Boot API도
|
||||
전부 이 1989년 아이디어의 후손입니다.`;
|
||||
|
||||
const CODE_SMARTPHONE = `스마트폰(2007~) — 컴퓨터가 주머니로 들어온 날
|
||||
|
||||
바뀐 숫자:
|
||||
- PC 보급률로는 닿지 못한 사람들까지 "전 인류가 컴퓨터 보유"
|
||||
- 인터넷 접속의 기본값: 책상 앞 → 언제 어디서나
|
||||
|
||||
개발자에게 바뀐 것:
|
||||
- '앱'이라는 새로운 소프트웨어 시장
|
||||
- 웹 개발의 상식 변화: 반응형 디자인
|
||||
(같은 페이지가 폰·태블릿·PC에서 다 예뻐야 함)
|
||||
- 위치·카메라·센서 = 새로운 서비스 재료
|
||||
|
||||
→ 우리 학습 플랫폼을 폰으로 열어도 레이아웃이
|
||||
다시 배치되는 것 — 이게 스마트폰 시대가 남긴 숙제이자 기본기.`;
|
||||
|
||||
const CODE_CLOUD = `클라우드(2006~) — "서버를 사지 말고 빌려 쓰세요"
|
||||
|
||||
옛날 방식: 서비스 하나 열려면
|
||||
서버 컴퓨터 구매(수백만 원) → 설치할 공간(전기·냉방) →
|
||||
회선 계약 → 몇 주~몇 달 뒤 오픈
|
||||
|
||||
클라우드 방식:
|
||||
클릭 몇 번 → 몇 분 뒤 서울 데이터센터에 내 서버 완성
|
||||
쓴 만큼만 요금 (전기요금처럼)
|
||||
|
||||
우리 회사(AWESOMEDEV)의 실제 모습:
|
||||
AWS EC2 (서울 리전) ← 우리 서버가 사는 곳
|
||||
├── Docker : 앱을 컨테이너(규격 화물)로 포장해 실행
|
||||
├── Caddy : HTTPS 현관 안내데스크
|
||||
├── Spring Boot : 백엔드
|
||||
└── PostgreSQL : 데이터베이스
|
||||
|
||||
→ 학생 몇 명이서도 전 세계 서비스를 열 수 있는 시대 —
|
||||
클라우드가 창업의 문턱을 확 낮췄어요.`;
|
||||
|
||||
const CODE_AI = `AI 시대(2020s~) — 컴퓨터에게 '말'이 통하기 시작했다
|
||||
|
||||
에니악: 배선을 꽂아서 지시
|
||||
PC: 프로그래밍 언어로 지시
|
||||
스마트폰: 터치로 지시
|
||||
AI: 사람 말(자연어)로 지시 — "이 버그 원인 찾아줘"
|
||||
|
||||
개발자의 일이 바뀌는 중:
|
||||
- 코드 자동완성 → 함수 통째로 제안 → 에이전트가 작업 수행
|
||||
- "코드를 치는 능력"보다
|
||||
"무엇을 만들지 정확히 설명하고, 결과를 검증하는 능력"이 중요해짐
|
||||
|
||||
변하지 않는 것:
|
||||
- AI가 짠 코드도 결국 사람이 읽고 책임져야 해요.
|
||||
- 기초(자료구조·네트워크·DB)를 아는 사람만이
|
||||
AI의 답이 맞는지 틀린지 판별할 수 있습니다.`;
|
||||
|
||||
const CODE_TIMELINE = `일곱 시대 한눈에 — 각 시대가 '지금 우리 개발'에 남긴 것
|
||||
|
||||
1946 에니악 → "컴퓨터"라는 물건 자체
|
||||
1947~ 트랜지스터 → 작고 싸고 빠르게 (무어의 법칙)
|
||||
1977~ PC 혁명 → 1인 1컴퓨터, 개발자라는 직업
|
||||
1989~ 인터넷·웹 → 우리가 만드는 웹 서비스의 무대
|
||||
2007~ 스마트폰 → 반응형 디자인, 모바일 우선
|
||||
2006~ 클라우드 → AWS EC2 위의 우리 서버
|
||||
2020s AI → 말로 하는 개발, 검증하는 개발자
|
||||
|
||||
한 줄 요약: 컴퓨터는 점점 "작아지고, 연결되고, 똑똑해지는"
|
||||
방향으로 달려왔고 — 우리는 그 최전선에서 일합니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '에니악의 시대' },
|
||||
{ n: 2, label: '트랜지스터와 무어의 법칙' },
|
||||
{ n: 3, label: 'PC 혁명' },
|
||||
{ n: 4, label: '인터넷과 웹' },
|
||||
{ n: 5, label: '스마트폰' },
|
||||
{ n: 6, label: '클라우드' },
|
||||
{ n: 7, label: 'AI 시대' },
|
||||
];
|
||||
|
||||
export default function HistoryPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>컴퓨터의 역사<br />— 교실만 한 계산기에서 주머니 속 AI까지</h1>
|
||||
<p>
|
||||
80년 전, 컴퓨터 한 대는 교실 하나를 가득 채우고 프로그램을 바꾸려면 케이블을
|
||||
손으로 다시 꽂아야 했어요. 그 기계가 어떻게 여러분 주머니 속으로 들어오고,
|
||||
이제는 <strong>말로 시키면 코드를 짜 주는</strong> 지경까지 왔는지 — 일곱 시대를 따라가 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 40분</span>
|
||||
<span className="chip">7개 시대 여행</span>
|
||||
<span className="chip">준비물: 내 PC와 폰</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="계산 도구에서 컴퓨터로 — 에니악" sub="27톤짜리 '프로그래밍 가능한' 기계의 탄생">
|
||||
<p>
|
||||
주판, 계산자, 기계식 계산기 — 인류는 오래전부터 계산 도구를 만들어 왔어요.
|
||||
하지만 이 도구들엔 결정적 한계가 있었죠. <strong>한 가지 일만</strong> 할 수 있다는 것.
|
||||
주판으로는 계산만 하지, "이 계산을 100번 반복하고 결과가 크면 저 계산으로 넘어가"
|
||||
같은 <strong>절차</strong>를 시킬 수 없어요.
|
||||
</p>
|
||||
<p>
|
||||
1946년 등장한 <strong>에니악(ENIAC)</strong>이 그 벽을 깼습니다. 배선을 바꾸면
|
||||
<strong> 다른 일을 시킬 수 있는</strong>, 즉 프로그래밍이 가능한 최초의 대형 전자
|
||||
컴퓨터 중 하나였어요. 계산기가 '한 곡만 연주하는 오르골'이라면, 컴퓨터는
|
||||
'악보만 갈아 끼우면 아무 곡이나 연주하는 피아니스트'인 거죠.
|
||||
</p>
|
||||
<Code>{CODE_ENIAC}</Code>
|
||||
<p>
|
||||
이후 "프로그램도 데이터처럼 <strong>메모리에 저장하자</strong>"는 폰 노이만 구조가
|
||||
등장하면서 케이블을 다시 꽂는 대신 <strong>파일을 바꿔 실행</strong>하는, 지금 우리가
|
||||
아는 컴퓨터의 기본 설계가 완성됩니다. 여러분이 <span className="icode">npm run dev</span>를
|
||||
치는 순간에도 이 구조가 돌아가고 있어요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>아침수업과 연결</b> "프로그램의 변화" 수업에서 배선 → 천공카드 → 코드로 이어지는
|
||||
흐름을 다뤘죠? 에니악이 바로 그 이야기의 1페이지입니다. 이 코스를 다 읽고 나서
|
||||
그 수업 노트를 다시 보면 훨씬 입체적으로 보일 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="트랜지스터와 무어의 법칙" sub="'2년마다 2배'가 50년 이어지면 벌어지는 일">
|
||||
<p>
|
||||
에니악의 진공관은 전구처럼 뜨겁고, 크고, 자주 타서 끊어졌어요. 1947년 발명된
|
||||
<strong> 트랜지스터</strong>는 같은 스위치 역할을 하면서 <strong>작고, 차갑고,
|
||||
안 망가지는</strong> 부품이었죠. 그리고 트랜지스터 수천~수십억 개를 손톱만 한 판에
|
||||
새겨 넣은 것이 <strong>집적회로(IC), 즉 '칩'</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_MOORE}</Code>
|
||||
<p>
|
||||
"2배가 25번"이 얼마나 무서운지 감이 안 온다면 — 종이 한 장을 25번 접으면(접을 수만
|
||||
있다면) 두께가 <strong>수 킬로미터</strong>가 됩니다. 이게 <strong>지수적 성장</strong>이에요.
|
||||
컴퓨터가 매년 조금씩이 아니라 <strong>폭발적으로</strong> 좋아진 비밀이고,
|
||||
뒤에 나올 PC·스마트폰·AI가 전부 이 곡선 위에서 태어났습니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>공짜 성능의 시대는 끝나가요</b> — 최근엔 트랜지스터가 원자 크기에 가까워지면서
|
||||
무어의 법칙이 느려지고 있어요. 그래서 요즘은 코어를 여러 개 쓰고(멀티코어),
|
||||
서버를 여러 대로 늘리는(수평 확장 — 클라우드 섹션에서 다시 만나요!) 방향으로
|
||||
성능을 끌어올립니다. "하드웨어가 알아서 빨라지겠지"가 안 통하니
|
||||
<strong> 효율적인 코드</strong>가 다시 중요해진 거죠.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="개인용 PC 혁명" sub="회사의 기계에서 '내 책상 위의 기계'로">
|
||||
<p>
|
||||
칩이 작고 싸지자 미친 생각이 현실이 됩니다 — <strong>"컴퓨터를 한 사람이
|
||||
한 대씩 갖는다면?"</strong> 1970년대까지 컴퓨터는 회사나 대학이 소유하고
|
||||
여러 사람이 시간을 쪼개 빌려 쓰는 물건이었거든요. 도서관의 공용 열람실 같은
|
||||
존재였던 컴퓨터가, 각자의 책상 위로 내려온 거예요.
|
||||
</p>
|
||||
<Code>{CODE_PC_ERA}</Code>
|
||||
<p>
|
||||
특히 <strong>GUI(그래픽 사용자 인터페이스)와 마우스</strong>의 등장이 결정적이었어요.
|
||||
명령어를 외운 전문가만 쓰던 기계가 "폴더를 더블클릭할 줄 아는" 모두의 도구가 됐죠.
|
||||
그리고 이때 <strong>소프트웨어를 사고파는 시장</strong>이 생기면서, 지금 여러분이
|
||||
꿈꾸는 '개발자'라는 직업이 대중적인 직업이 됩니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 내 PC가 에니악보다 얼마나 대단한지 눈으로 확인해 봅시다.
|
||||
<span className="kbd">Ctrl</span>+<span className="kbd">Shift</span>+<span className="kbd">Esc</span>로
|
||||
작업 관리자를 열고 <strong>성능 탭</strong>을 보세요. CPU 코어 수, 속도(GHz = 초당
|
||||
수십억 번!), 메모리 용량이 보여요. 에니악의 초당 덧셈 5,000번과 비교하면 —
|
||||
여러분 책상 위 물건이 80년 전 국가급 슈퍼컴퓨터의 수백만 배입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="인터넷과 웹의 탄생" sub="혼자 좋던 컴퓨터가 '연결'되자 세상이 바뀌었다">
|
||||
<p>
|
||||
PC 시대의 컴퓨터는 성능은 좋아도 <strong>외딴섬</strong>이었어요. 파일을 옮기려면
|
||||
디스켓을 들고 걸어가야 했죠(진짜로 '스니커넷'이라고 불렀어요 — 운동화 신고 나르는
|
||||
네트워크라는 농담). 이 섬들을 잇는 다리가 <strong>인터넷</strong>,
|
||||
그 다리 위에 세워진 도시가 <strong>웹</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_WEB}</Code>
|
||||
<p>
|
||||
웹의 아이디어는 놀랍도록 단순해요 — <strong>"문서에 링크를 걸어 서로 잇자."</strong>
|
||||
그런데 이 단순한 규칙이 검색, 쇼핑, 동영상, 그리고 우리 학습 플랫폼까지 전부 낳았죠.
|
||||
여러분이 배우는 <strong>React는 웹의 화면을</strong>, <strong>Spring Boot는 웹의
|
||||
주방(서버)을</strong> 만드는 도구 — 둘 다 이 시대가 깔아 놓은 무대 위의 배우들입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 1989년의 유산이 지금도 살아 있는 현장을 봅시다. 우리 플랫폼
|
||||
(<span className="icode">edu.awesomedevapp.com</span>)에서 <span className="kbd">F12</span> →
|
||||
<span className="kbd">Network</span> 탭 → 새로고침. 요청 하나를 클릭하면
|
||||
<span className="icode">GET</span>, <span className="icode">200 OK</span> 같은 글자가 보여요.
|
||||
이 대화 규칙(HTTP)이 웹 탄생 때 만들어진 바로 그것 — 30년 넘게 현역인 설계입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="스마트폰이 바꾼 것" sub="컴퓨터가 책상을 떠나 주머니로">
|
||||
<p>
|
||||
2007년 이후 스마트폰은 "전화기에 컴퓨터를 조금 넣은 것"이 아니라
|
||||
<strong> 컴퓨터에 전화 기능을 얹은 것</strong>이었어요. 무어의 법칙(섹션 2) 덕분에
|
||||
손바닥만 한 크기에 PC급 성능이 들어갈 수 있었고, 인터넷(섹션 4)이 무선으로
|
||||
따라 들어왔죠. 앞의 시대들이 전부 합쳐진 결정체인 셈이에요.
|
||||
</p>
|
||||
<Code>{CODE_SMARTPHONE}</Code>
|
||||
<p>
|
||||
개발자 입장에서 가장 큰 변화는 <strong>"사용자가 어떤 화면으로 볼지 모른다"</strong>는
|
||||
전제가 생긴 거예요. 그래서 <strong>반응형 디자인</strong>이 기본기가 됐습니다.
|
||||
한 벌의 옷이 아니라, 입는 사람 몸에 맞춰 늘어나는 옷을 만들어야 하는 거죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 이 페이지에서 <span className="kbd">F12</span> →
|
||||
왼쪽 위 <strong>기기 모양 아이콘</strong>(기기 툴바)을 눌러 보세요. 화면이 스마트폰
|
||||
크기로 바뀌면서 레이아웃이 다시 배치되는 게 보이나요? 폭을 마우스로 늘였다 줄였다
|
||||
하면서 어느 지점에서 배치가 '뚝' 바뀌는지 찾아보세요 — 그 지점이 바로
|
||||
반응형의 분기점(breakpoint)입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="클라우드 시대" sub="서버를 '사는' 시대에서 '빌리는' 시대로">
|
||||
<p>
|
||||
웹 서비스가 폭발하자 새로운 고민이 생겼어요 — <strong>서버를 어디에 둘 것인가.</strong>
|
||||
직접 사서 관리하자니 비싸고, 갑자기 사용자가 몰리면 감당이 안 되고, 한가할 땐
|
||||
비싼 기계가 놀아요. 그래서 나온 답이 <strong>클라우드</strong>:
|
||||
거대한 데이터센터의 컴퓨터를 <strong>전기처럼 쓴 만큼만 빌려 쓰는</strong> 방식입니다.
|
||||
자가용(내 서버)을 사는 대신 필요할 때 택시(클라우드)를 부르는 거죠.
|
||||
</p>
|
||||
<Code>{CODE_CLOUD}</Code>
|
||||
<p>
|
||||
위 그림은 비유가 아니라 <strong>우리 회사의 실제 구성</strong>이에요. 여러분이 지금
|
||||
보고 있는 이 페이지도 AWS 서울 리전의 EC2 서버에서, Docker 컨테이너에 담긴 채,
|
||||
Caddy를 거쳐 배달된 겁니다. 클라우드는 남의 이야기가 아니라
|
||||
<strong> 오늘 우리가 서 있는 바닥</strong>이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 터미널에서 <span className="icode">nslookup edu.awesomedevapp.com</span>을
|
||||
쳐 보세요. 나오는 IP가 바로 AWS 서울 데이터센터 어딘가에 있는 우리 EC2 서버의 주소예요.
|
||||
"클라우드"라는 뜬구름 같은 말의 정체가 사실은 <strong>주소가 있는 진짜 컴퓨터</strong>라는 걸
|
||||
확인하는 순간입니다. (명령이 낯설다면 네트워크 코스 5섹션 복습!)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="그리고 AI 시대" sub="'어떻게'를 시키던 개발에서 '무엇을'을 말하는 개발로">
|
||||
<p>
|
||||
컴퓨터에게 일을 시키는 방법의 역사를 다시 볼까요? 배선 꽂기(에니악) → 프로그래밍
|
||||
언어(PC) → 클릭과 터치(GUI·스마트폰) — 점점 <strong>사람 쪽으로</strong> 다가왔어요.
|
||||
그리고 지금, 마침내 <strong>사람의 말</strong>이 통하기 시작했습니다.
|
||||
</p>
|
||||
<Code>{CODE_AI}</Code>
|
||||
<p>
|
||||
그럼 개발 공부는 필요 없어지는 걸까요? 정반대예요. AI는 그럴듯하지만 틀린 답도
|
||||
자신 있게 내놓거든요. 운전기사가 생겨도 <strong>목적지와 길을 아는 사람</strong>이
|
||||
차주여야 하듯, AI가 짠 코드를 읽고 "이거 틀렸는데?"라고 말할 수 있는 사람 —
|
||||
<strong> 기초가 탄탄한 개발자</strong>의 가치는 오히려 올라갑니다. 여러분이 이 학습
|
||||
센터에서 네트워크·DB·코딩 기초를 배우는 이유가 바로 그거예요.
|
||||
</p>
|
||||
<p>일곱 시대를 한 장으로 정리하면 이렇습니다.</p>
|
||||
<Code>{CODE_TIMELINE}</Code>
|
||||
<div className="warn">
|
||||
<b>주의 — 회사에서 AI 도구를 쓸 때</b> 편하다고 아무 코드나 AI 서비스에 붙여 넣으면
|
||||
안 돼요. 회사 소스 코드는 계약·보안상 외부 반출에 제약이 있을 수 있으니,
|
||||
<strong> 반드시 멘토에게 먼저 확인</strong>하고 회사가 허용한 도구만 사용하세요.
|
||||
도구는 바뀌어도 이 원칙은 안 바뀝니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🕰️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
교실만 한 에니악에서 출발해 트랜지스터의 축소 마법, 책상 위 PC, 세상을 이은
|
||||
웹, 주머니 속 스마트폰, 우리 서버가 사는 클라우드, 그리고 말이 통하는 AI까지 —
|
||||
<strong> "작아지고, 연결되고, 똑똑해진"</strong> 80년을 완주했어요.
|
||||
이제 그 역사 위에서 직접 만들 차례입니다. 데이터가 달리는 길이 궁금하면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link>로, 그 길 위의
|
||||
코드를 짜고 싶으면 <Link to="/learn/coding"><strong>코딩 기초</strong></Link>로
|
||||
이어서 가 보세요. 아침수업 "프로그램의 변화" 노트와 이 코스의 타임라인을
|
||||
나란히 놓고 복습하면 금상첨화!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
379
frontend/src/pages/courses/HomeNetworkPage.jsx
Normal file
379
frontend/src/pages/courses/HomeNetworkPage.jsx
Normal file
@ -0,0 +1,379 @@
|
||||
// 이 파일이 하는 일: "공유기와 홈 네트워크 실전" 코스 — 공유기 관리자 페이지에 직접 들어가 보는 것부터
|
||||
// WiFi 주파수 선택, 채널 간섭, 포트포워딩, UPnP·DMZ, 보안 설정, "인터넷 느릴 때" 점검 순서까지
|
||||
// 7개 섹션으로 안내하는 정적 학습 페이지. ("네트워크의 이해" 코스의 실전편)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_ADMIN_LOGIN = `공유기 관리자 페이지 들어가기 — 3단계
|
||||
|
||||
1) 내 공유기 주소(게이트웨이) 알아내기 — 터미널에서:
|
||||
ipconfig
|
||||
기본 게이트웨이 . . . : 192.168.0.1 ← 이게 공유기 주소!
|
||||
|
||||
2) 브라우저 주소창에 그 주소를 입력:
|
||||
http://192.168.0.1
|
||||
(제조사에 따라 192.168.1.1, 192.168.219.1 등으로 다를 수 있어요)
|
||||
|
||||
3) 관리자 아이디/비밀번호 입력 → 관리 화면 입장!
|
||||
초기 비번은 공유기 밑면 스티커에 적혀 있는 경우가 많아요.
|
||||
|
||||
→ 여기서 볼 수 있는 것: 연결된 기기 목록, DHCP가 나눠준 IP,
|
||||
WiFi 이름(SSID)·비밀번호, 포트포워딩 설정 전부!`;
|
||||
|
||||
const CODE_BAND_COMPARE = `2.4GHz vs 5GHz — 저음 스피커와 고음 스피커
|
||||
|
||||
2.4GHz 5GHz
|
||||
──────────────────────────────────────────────
|
||||
속도 느림 빠름 (몇 배 차이)
|
||||
도달 거리 멀리 감 가까움
|
||||
벽 통과 잘 뚫음 잘 막힘
|
||||
간섭 많음 (전자레인지, 적음
|
||||
블루투스도 이 대역)
|
||||
|
||||
→ 낮은 주파수 = 낮은 음처럼 벽 너머까지 웅웅 울려 퍼지고,
|
||||
높은 주파수 = 높은 음처럼 또렷하지만 문 닫으면 잘 안 들려요.
|
||||
|
||||
선택 기준:
|
||||
공유기와 같은 방·거실 → 5GHz (SSID에 "_5G"가 붙은 쪽)
|
||||
방 2~3개 건너·먼 방 → 2.4GHz
|
||||
잘 모르겠으면 → 둘 다 잡히는 곳에서는 5GHz 우선!`;
|
||||
|
||||
const CODE_CHANNEL = `채널 간섭 — 같은 주파수 안의 '차선' 나누기
|
||||
|
||||
2.4GHz 대역은 채널 1~13번이 있지만, 서로 겹치는 구조라
|
||||
실제로 안 겹치는 조합은 1 · 6 · 11 세 개뿐이에요.
|
||||
|
||||
채널: 1 2 3 4 5 6 7 8 9 10 11 12 13
|
||||
├──겹침──┤
|
||||
├──겹침──┤
|
||||
안전: 1───────────6───────────11
|
||||
|
||||
아파트라면? 윗집·옆집 공유기가 전부 6번 채널이면
|
||||
왕복 2차선 도로에 트럭 수십 대가 몰린 상황 — 서로 양보하느라 다 느려져요.
|
||||
|
||||
해결:
|
||||
1) 관리자 페이지 → 무선 설정 → 채널을 "자동"으로 (요즘 공유기는 알아서 회피)
|
||||
2) 수동으로 바꾼다면 1·6·11 중 이웃이 덜 쓰는 번호로
|
||||
3) 5GHz는 채널이 훨씬 넓고 많아 간섭 걱정이 적어요 — 이것도 5GHz를 권하는 이유!`;
|
||||
|
||||
const CODE_PORT_FORWARD = `포트포워딩 — 경비실(공유기)에 배달 규칙 등록하기
|
||||
|
||||
문제 상황: 내 PC(192.168.0.5)에서 Spring Boot 서버를 8080 포트로 켰다.
|
||||
친구가 우리 집 공인 IP로 접속하면... 공유기까지만 오고 끝!
|
||||
|
||||
친구 ──> [공인 IP]:8080 ──> [공유기] "8080? 누구 건지 모르는데?" ✗
|
||||
│
|
||||
192.168.0.5 (내 PC, Spring Boot :8080)
|
||||
|
||||
경비실(NAT)은 '안에서 나간 요청의 답장'만 배달할 줄 알아요.
|
||||
밖에서 먼저 들어온 손님은 몇 호로 보낼지 규칙이 없으면 돌려보냅니다.
|
||||
|
||||
포트포워딩 = 그 규칙을 등록하는 것:
|
||||
|
||||
"외부 8080 포트로 오는 손님은 → 192.168.0.5의 8080호로 안내해 줘"
|
||||
|
||||
외부 포트 내부 IP 내부 포트 프로토콜
|
||||
8080 → 192.168.0.5 8080 TCP
|
||||
|
||||
등록 후:
|
||||
친구 ──> [공인 IP]:8080 ──> [공유기] "아, 302호(내 PC)!" ──> Spring Boot ✓`;
|
||||
|
||||
const CODE_UPNP_DMZ = `UPnP와 DMZ — 편한 만큼 위험한 두 가지
|
||||
|
||||
UPnP (Universal Plug and Play)
|
||||
프로그램이 공유기에게 "저한테 포트 하나 열어 주세요" 하고
|
||||
스스로 포트포워딩을 등록하는 기능. 게임기·화상통화가 이걸 써요.
|
||||
장점: 설정이 자동 / 위험: 악성 프로그램도 몰래 문을 열 수 있음
|
||||
|
||||
DMZ
|
||||
"밖에서 오는 모든 요청을 전부 이 기기로 보내"라는 극단적 설정.
|
||||
포트 하나가 아니라 현관문·창문·베란다를 전부 열어 두는 것과 같아요.
|
||||
|
||||
권장 순서 (좁게 열수록 안전):
|
||||
1순위 필요한 포트만 포트포워딩 ← 방 하나만 열기
|
||||
2순위 UPnP (신뢰하는 기기만 쓸 때) ← 자동문
|
||||
최후 DMZ ← 사실상 쓰지 말 것.
|
||||
"귀찮아서 DMZ"는 집 비밀번호를 현관에 써 붙이는 것과 같아요.`;
|
||||
|
||||
const CODE_SECURITY = `공유기 보안 체크리스트 — 위에서부터 순서대로!
|
||||
|
||||
□ 1. 관리자 비밀번호 변경
|
||||
초기 비번(admin/admin 같은)은 검색하면 다 나와요.
|
||||
같은 WiFi에 접속한 누구든 공유기를 통째로 조종할 수 있게 됩니다.
|
||||
|
||||
□ 2. WiFi 암호화 방식 = WPA3 (없으면 WPA2)
|
||||
WEP·"열림(Open)"은 금지 — 지나가던 사람도 통신 내용을 엿볼 수 있어요.
|
||||
WPA3는 최신 자물쇠, WPA2는 아직 튼튼한 자물쇠, WEP는 부서진 자물쇠.
|
||||
|
||||
□ 3. WiFi 비밀번호는 길고 엉뚱하게
|
||||
생일·전화번호 ✗ → 긴 문장형 ○ (예: 단어 3~4개 조합)
|
||||
|
||||
□ 4. 원격 관리(외부에서 관리자 페이지 접속) 끄기
|
||||
켜 두면 전 세계 누구나 우리 집 공유기 로그인 화면을 두드려 볼 수 있어요.
|
||||
|
||||
□ 5. 안 쓰는 포트포워딩·UPnP·DMZ 정리
|
||||
실습 끝난 규칙은 바로 지우기 — 열어 둔 문은 언젠가 누가 두드립니다.
|
||||
|
||||
□ 6. 펌웨어 업데이트
|
||||
공유기도 작은 컴퓨터라 보안 패치가 나와요. 관리자 페이지에서 확인.`;
|
||||
|
||||
const CODE_SLOW_CHECK = `"집 인터넷이 느려요!" — 범인 찾기 순서 (안쪽 → 바깥쪽)
|
||||
|
||||
① 내 기기 문제인가?
|
||||
다른 기기(폰·노트북)로 같은 사이트 접속.
|
||||
→ 다른 기기는 빠르다? 범인은 내 PC (재부팅·백그라운드 다운로드 확인)
|
||||
|
||||
② WiFi 문제인가?
|
||||
랜선으로 직접 연결해서 속도 비교.
|
||||
→ 유선은 빠르다? 범인은 무선 구간 (거리·채널 간섭·2.4/5GHz 선택)
|
||||
|
||||
③ 공유기 문제인가?
|
||||
공유기 전원을 뽑고 10초 뒤 다시 연결 (재부팅).
|
||||
관리자 페이지에서 연결 기기 수 확인 — 모르는 기기가 있다면 비번 유출!
|
||||
→ 재부팅 후 빨라졌다? 공유기 과부하였던 것
|
||||
|
||||
④ 회선(통신사) 문제인가?
|
||||
속도 측정 사이트에서 측정 → 가입한 상품 속도(예: 500Mbps)와 비교.
|
||||
ping 8.8.8.8 -t 로 응답 시간이 널뛰거나 손실이 나오는지 관찰.
|
||||
→ 유선 직결인데도 한참 느리다? 통신사에 문의할 차례
|
||||
|
||||
핵심: 안쪽(내 기기)부터 바깥쪽(통신사)으로 한 칸씩!
|
||||
서버 장애 대응에서 배울 "원인 격리"와 완전히 같은 사고방식이에요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '관리자 페이지 입장' },
|
||||
{ n: 2, label: '2.4G vs 5G' },
|
||||
{ n: 3, label: '채널 간섭' },
|
||||
{ n: 4, label: '포트포워딩' },
|
||||
{ n: 5, label: 'UPnP와 DMZ' },
|
||||
{ n: 6, label: '공유기 보안' },
|
||||
{ n: 7, label: '느릴 때 점검 순서' },
|
||||
];
|
||||
|
||||
export default function HomeNetworkPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 무엇을 다루는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>공유기와 홈 네트워크 실전<br />— 우리 집 네트워크의 관리자 되기</h1>
|
||||
<p>
|
||||
매일 쓰는 공유기, 사실은 우리 집의 작은 서버실이에요. 관리자 페이지에 직접
|
||||
들어가서 WiFi를 튜닝하고, 내 PC를 바깥에 열어 보고(포트포워딩), 보안까지 잠그는
|
||||
<strong> 실전 코스</strong>입니다. 집에 공유기만 있으면 전부 직접 해볼 수 있어요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">직접 해보기 4개</span>
|
||||
<span className="chip">준비물: 집 공유기 + 내 PC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="공유기 관리자 페이지 들어가기" sub="192.168.x.1 — 우리 집 서버실의 문 열기">
|
||||
<p>
|
||||
공유기는 그냥 인터넷 나눠 주는 상자가 아니라, 관리 화면을 가진
|
||||
<strong> 작은 컴퓨터</strong>예요. 그 관리 화면의 주소가 바로
|
||||
<span className="icode">192.168.0.1</span> 같은 <strong>게이트웨이 주소</strong>입니다.
|
||||
"네트워크의 이해" 코스에서 <span className="icode">ipconfig</span>로 봤던
|
||||
<strong> 기본 게이트웨이</strong> — 그게 공유기의 집 주소였던 거죠.
|
||||
</p>
|
||||
<Code>{CODE_ADMIN_LOGIN}</Code>
|
||||
<p>
|
||||
입장하면 학교 교무실 상황판 같은 화면이 나와요. 지금 우리 집에 어떤 기기가
|
||||
몇 대 붙어 있고, 각자 어떤 IP를 받았는지(DHCP), WiFi 이름과 암호는 뭔지 —
|
||||
이 코스의 나머지 섹션은 전부 <strong>이 화면 안에서</strong> 진행됩니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>학교·회사 공유기는 손대지 마세요!</b> 관리자 페이지 실습은 반드시
|
||||
<strong> 우리 집 공유기</strong>로만 하세요. 남의 네트워크 장비 설정을 건드리는 건
|
||||
장난이 아니라 사고입니다. (어썸데브 사무실 공유기도 인프라 담당자 것!)
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 집에서 <span className="icode">ipconfig</span>로 게이트웨이
|
||||
주소를 찾아 브라우저로 접속해 보세요. 로그인했다면 <strong>연결된 기기 목록</strong>을
|
||||
열어 우리 집 기기 수를 세어 보세요 — 폰, 노트북, TV, 심지어 냉장고까지.
|
||||
예상보다 많아서 놀랄 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="WiFi 2.4GHz vs 5GHz" sub="멀리 가는 저음, 빠르지만 여린 고음">
|
||||
<p>
|
||||
공유기 WiFi 목록을 보면 같은 이름에 <span className="icode">_5G</span>만 붙은 게
|
||||
두 개 있죠? 같은 공유기가 <strong>두 개의 주파수 대역</strong>으로 방송 중인 거예요.
|
||||
스피커에 비유하면 2.4GHz는 <strong>저음(우퍼)</strong> — 벽 너머까지 울려 퍼지지만
|
||||
정보량이 적고, 5GHz는 <strong>고음(트위터)</strong> — 또렷하고 빠르지만 문 하나만
|
||||
닫아도 확 줄어들어요.
|
||||
</p>
|
||||
<Code>{CODE_BAND_COMPARE}</Code>
|
||||
<p>
|
||||
그래서 정답은 "무조건 5G"가 아니라 <strong>"내 위치에 따라"</strong>예요.
|
||||
공유기 옆에서 화상수업을 듣는다면 5GHz, 공유기에서 방 두 개 건너에 있다면
|
||||
신호가 약한 5GHz보다 안정적인 2.4GHz가 오히려 빠를 수 있습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 폰으로 속도 측정 사이트를 열고, 공유기 <strong>바로 옆</strong>과
|
||||
<strong> 가장 먼 방</strong>에서 각각 2.4GHz·5GHz에 붙어 총 4번 측정해 보세요.
|
||||
"가까우면 5G 압승, 멀어지면 역전"이 실제 숫자로 보이면 이 섹션은 완전히 이해한 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="채널 간섭" sub="아파트에서 WiFi가 유독 느린 이유">
|
||||
<p>
|
||||
같은 2.4GHz 대역 안에서도 <strong>채널</strong>이라는 세부 차선이 나뉘어 있어요.
|
||||
문제는 옆집·윗집 공유기도 같은 하늘(전파 대역)을 쓴다는 것. 모두가 같은 채널에
|
||||
몰리면, 같은 주파수의 무전기 여러 대가 동시에 말하는 것처럼 서로를 기다리느라
|
||||
다 같이 느려집니다.
|
||||
</p>
|
||||
<Code>{CODE_CHANNEL}</Code>
|
||||
<p>
|
||||
다행히 요즘 공유기는 채널을 <strong>자동</strong>으로 두면 주변을 스캔해서
|
||||
한산한 채널로 옮겨 가요. 관리자 페이지의 무선 설정에서 지금 몇 번 채널을 쓰는지
|
||||
확인해 보고, 수동 고정돼 있다면 자동으로 바꿔 주는 것만으로도 개선되는 경우가 많습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>비유 하나 더</b> 2.4GHz의 1·6·11 채널은 왕복 6차선 도로에서 실제로는
|
||||
<strong> 안 부딪히는 차선이 3개뿐</strong>인 상황이에요. 5GHz로 가면 차선이
|
||||
수십 개인 새 고속도로 — 섹션 2에서 5GHz를 권한 숨은 이유이기도 해요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="포트포워딩 — 내 PC를 서버로" sub="경비실에 '이 손님은 302호로' 규칙 등록하기">
|
||||
<p>
|
||||
"네트워크의 이해"에서 배운 NAT를 기억하나요? 공유기는 아파트 <strong>경비실</strong>이라
|
||||
안에서 나간 요청의 답장은 잘 배달하지만, <strong>밖에서 먼저 찾아온 손님</strong>은
|
||||
몇 호로 보낼지 몰라 돌려보냅니다. 그래서 내 PC에서 Spring Boot 서버를 켜도
|
||||
친구는 접속할 수 없어요. 이 문을 열어 주는 규칙이 <strong>포트포워딩</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_PORT_FORWARD}</Code>
|
||||
<p>
|
||||
포트포워딩이 필요한 순간들: 내 PC의 개발 서버를 친구에게 잠깐 보여줄 때,
|
||||
게임 방을 직접 열 때, 집 PC에 원격 접속할 때. 참고로 우리 학습 플랫폼이 도는
|
||||
AWS EC2는 공유기 대신 <strong>보안 그룹</strong>이라는 방화벽으로 "어떤 포트를
|
||||
열지"를 정해요 — 개념은 완전히 같습니다. 집에서 익힌 감각이 서버 운영에서 그대로 쓰여요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 관리자 페이지에서 <strong>포트포워딩 메뉴를 찾아 규칙 화면까지만</strong>
|
||||
들어가 보세요(NAT 설정, 고급 설정 등의 이름). 외부 포트·내부 IP·내부 포트 입력칸이
|
||||
위 그림과 똑같이 생겼는지 확인! 실제 규칙 추가는 섹션 6의 보안 설정을 마친 뒤,
|
||||
실습이 끝나면 <strong>반드시 삭제</strong>하는 조건으로만 해보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="UPnP와 DMZ 한 입" sub="자동문과 '문 전부 열기' — 편리함의 값은 보안">
|
||||
<p>
|
||||
포트포워딩을 매번 손으로 등록하는 게 귀찮아서 나온 것들이 있어요.
|
||||
<strong> UPnP</strong>는 프로그램이 공유기에 스스로 "포트 열어 주세요" 요청하는
|
||||
자동문이고, <strong>DMZ</strong>는 아예 한 기기로 모든 문을 다 열어 버리는
|
||||
극단적인 방법입니다.
|
||||
</p>
|
||||
<Code>{CODE_UPNP_DMZ}</Code>
|
||||
<div className="warn">
|
||||
<b>DMZ의 함정</b> 인터넷 검색에서 "포트포워딩 안 되면 DMZ 하세요"라는 글을 자주
|
||||
보게 될 거예요. 되긴 됩니다 — 문을 전부 열었으니까요. 하지만 그 순간 그 PC는
|
||||
인터넷의 모든 자동 공격 스캐너에 그대로 노출됩니다. <strong>문제를 푸는 것과
|
||||
문을 다 여는 것은 달라요.</strong> 필요한 포트 하나만 정확히 여는 습관을 들이세요.
|
||||
</div>
|
||||
<p>
|
||||
기억할 원칙은 하나예요. <strong>"가장 좁게 열 수 있는 문을 열어라."</strong>
|
||||
나중에 배울 서버 보안(EC2 보안 그룹, 방화벽)에서도 똑같은 원칙이
|
||||
<strong> 최소 권한 원칙</strong>이라는 이름으로 다시 등장합니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="공유기 보안 설정" sub="관리자 비번과 WPA3 — 우리 집 네트워크의 자물쇠">
|
||||
<p>
|
||||
공유기를 장악당하면 그 집의 <strong>모든 인터넷 통신이 남의 손을 거쳐</strong> 나가게
|
||||
돼요. 가짜 사이트로 몰래 데려가거나, 어떤 사이트에 접속하는지 전부 지켜볼 수도 있죠.
|
||||
그런데도 많은 집이 공유기를 <strong>초기 비밀번호 그대로</strong> 씁니다.
|
||||
아래 체크리스트를 위에서부터 순서대로 점검해 보세요.
|
||||
</p>
|
||||
<Code>{CODE_SECURITY}</Code>
|
||||
<p>
|
||||
<strong>WPA3</strong>는 WiFi 전파 자체를 암호화하는 최신 방식이에요. WiFi는
|
||||
전파라서 비밀번호가 없으면 같은 공간의 누구나 오가는 내용을 주울 수 있는데,
|
||||
WPA2/WPA3는 그 내용을 자물쇠로 잠가 줍니다. 오래된 <span className="icode">WEP</span>는
|
||||
이미 몇 분 만에 뚫리는 부서진 자물쇠라 절대 쓰면 안 돼요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 집 공유기에서 두 가지를 확인하고, 가능하면 바로 고쳐 보세요.
|
||||
① 관리자 비밀번호가 초기값이면 <strong>지금 변경</strong> (부모님께 새 비번 공유!)
|
||||
② 무선 설정에서 암호화 방식이 <span className="icode">WPA2</span> 이상인지 확인,
|
||||
<span className="icode">WPA3</span> 항목이 있으면 <span className="icode">WPA2/WPA3 혼용</span>으로.
|
||||
오늘 이 실습 하나로 여러분은 가족의 보안 담당자가 됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습 — 집 인터넷 느릴 때 점검 순서" sub="감이 아니라 순서로 범인을 찾는다">
|
||||
<p>
|
||||
"인터넷 느려!"라는 한마디에는 범인 후보가 최소 넷이에요 — 내 기기, WiFi 구간,
|
||||
공유기, 통신사 회선. 아무거나 찔러 보는 대신, <strong>안쪽에서 바깥쪽으로</strong>
|
||||
한 칸씩 용의자를 지워 나가면 반드시 범인이 나옵니다. 의사가 문진하듯이요.
|
||||
</p>
|
||||
<Code>{CODE_SLOW_CHECK}</Code>
|
||||
<p>
|
||||
이 "한 칸씩 격리해서 지워 나가기"는 개발자의 핵심 근육이에요. 나중에
|
||||
"우리 플랫폼이 안 열려요"라는 문의를 받으면 똑같이 진행합니다 — 브라우저 문제?
|
||||
DNS 문제? Caddy까지는 왔나? Spring Boot가 죽었나? PostgreSQL이 안 붙나?
|
||||
<strong> 집 인터넷 점검과 서버 장애 대응은 같은 사고방식의 두 얼굴</strong>입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 문제가 없어도 <strong>평소 기준값</strong>을 만들어 두세요.
|
||||
① 속도 측정 사이트에서 유선·무선 속도를 각각 재서 메모
|
||||
② <span className="icode">ping 8.8.8.8</span>의 평균 응답 시간(ms) 메모.
|
||||
장애 대응의 반은 "평소엔 어땠는지"를 아는 것 — 서버 모니터링에서도 똑같아요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🏠 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 공유기 관리 화면을 열고(1), 상황에 맞는 주파수와 채널을 고르고(2·3),
|
||||
필요한 문만 정확히 열고(4·5), 자물쇠를 채우고(6), 문제가 생기면 순서대로 범인을
|
||||
찾을 수 있어요(7). 집에서 익힌 이 감각 — 포트 열기, 최소 권한, 원인 격리 — 는
|
||||
서버 운영의 축소판입니다. 아직 안 들었다면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스로 이론 기초를
|
||||
다지고, 멘토에게 <strong>"내 PC 개발 서버를 포트포워딩으로 친구에게 보여주기"</strong>
|
||||
과제를 받아 이번 코스를 실전으로 마무리해 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
407
frontend/src/pages/courses/HttpPage.jsx
Normal file
407
frontend/src/pages/courses/HttpPage.jsx
Normal file
@ -0,0 +1,407 @@
|
||||
// 이 파일이 하는 일: "HTTP와 HTTPS" 코스 — 브라우저와 서버가 나누는 대화(요청/응답)의
|
||||
// 문법부터, 그 대화를 아무도 못 엿보게 봉인하는 HTTPS까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (요청/응답 구조 → 상태코드 → 쿠키와 세션 → HTTPS/TLS → 인증서 → Network 탭 실습 → 정리 퀴즈)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_REQUEST = `브라우저가 서버에 보내는 편지 = HTTP 요청
|
||||
|
||||
GET /api/courses HTTP/1.1 ← ① 시작줄: "무엇을(경로), 어떻게(메서드)"
|
||||
Host: edu.awesomedevapp.com ← ② 헤더: 받는 사람 주소
|
||||
Accept: application/json ← "답장은 JSON으로 주세요"
|
||||
Cookie: SESSION=a1b2c3... ← "저 아까 로그인한 그 사람이에요"
|
||||
← ③ 빈 줄 (헤더 끝 표시)
|
||||
(GET은 보통 바디 없음) ← ④ 바디: 보낼 내용물 (있을 때만)
|
||||
|
||||
서버가 돌려주는 답장 = HTTP 응답
|
||||
|
||||
HTTP/1.1 200 OK ← ① 시작줄: "결과는 이래(상태코드)"
|
||||
Content-Type: application/json ← ② 헤더: "내용물은 JSON이야"
|
||||
Content-Length: 512 ← "길이는 512바이트"
|
||||
|
||||
{"courses":[{"id":1,"title":"네트워크의 이해"}, ...]} ← ④ 바디: 진짜 내용물`;
|
||||
|
||||
const CODE_METHODS = `메서드 = "무엇을 하고 싶은지"를 나타내는 동사
|
||||
|
||||
메서드 뜻 우리 플랫폼 실례
|
||||
──────────────────────────────────────────────────────
|
||||
GET 조회 (읽기) GET /api/courses 코스 목록 보여줘
|
||||
POST 생성 (쓰기) POST /api/auth/login 로그인 정보 보낼게 (바디에!)
|
||||
PUT 수정 (교체) PUT /api/profile 내 프로필 이걸로 바꿔줘
|
||||
DELETE 삭제 DELETE /api/posts/3 3번 글 지워줘
|
||||
|
||||
기억법: 게시판으로 생각하면 쉬워요.
|
||||
글 목록 보기(GET) / 글 쓰기(POST) / 글 고치기(PUT) / 글 지우기(DELETE)
|
||||
→ 데이터를 다루는 네 가지 기본 동작 = CRUD와 정확히 짝이 맞습니다.`;
|
||||
|
||||
const CODE_STATUS = `상태코드 = 서버의 대답 첫마디. 첫 숫자만 봐도 분위기를 알 수 있어요.
|
||||
|
||||
2xx "잘 됐어요" 3xx "다른 데로 가세요" 4xx "네가 잘못했어" 5xx "내(서버)가 잘못했어"
|
||||
|
||||
코드 이름 우리 플랫폼에서 실제로 만나는 상황
|
||||
─────────────────────────────────────────────────────────────
|
||||
200 OK GET /api/courses 성공 — 코스 목록이 바디에 담겨 옴
|
||||
301 Moved Permanently http:// 로 접속 → Caddy가 "https:// 로 오세요" 안내
|
||||
401 Unauthorized 로그인 안 하고 GET /api/me → "누구세요? 먼저 로그인!"
|
||||
403 Forbidden 학생 계정으로 관리자 API 호출 → "누군진 알겠는데 권한 없음"
|
||||
404 Not Found GET /api/coursez (오타!) → "그런 주소 없는데요"
|
||||
500 Internal Server Error 백엔드(Spring Boot) 코드가 터짐 → 서버 로그 볼 시간
|
||||
|
||||
401 vs 403 헷갈림 방지:
|
||||
401 = 학생증 자체가 없음 (로그인부터)
|
||||
403 = 학생증은 있는데 교무실 출입증은 아님 (권한 부족)`;
|
||||
|
||||
const CODE_COOKIE = `쿠키와 세션 — 우리 플랫폼 로그인이 유지되는 원리
|
||||
|
||||
HTTP는 원래 '금붕어 기억력'이에요. 요청 하나가 끝나면 상대가 누군지 다 잊습니다.
|
||||
그래서 이런 순서로 "기억"을 만들어요:
|
||||
|
||||
① 로그인 요청
|
||||
POST /api/auth/login (바디: 아이디+비밀번호)
|
||||
│
|
||||
② 서버(Spring Boot): 확인 후 '번호표' 발급
|
||||
응답 헤더 → Set-Cookie: SESSION=a1b2c3; HttpOnly; Secure
|
||||
(서버는 "번호표 a1b2c3 = 3학년 김미림"을 자기 장부(세션 저장소)에 기록)
|
||||
│
|
||||
③ 이후 모든 요청에 브라우저가 자동으로 번호표 동봉
|
||||
GET /api/me
|
||||
Cookie: SESSION=a1b2c3 ← 우리가 코드 안 짜도 브라우저가 알아서!
|
||||
│
|
||||
④ 서버가 장부 대조: "a1b2c3? 아, 김미림 님" → 내 정보 응답
|
||||
|
||||
쿠키 = 손에 든 번호표(브라우저 보관)
|
||||
세션 = 카운터 뒤의 장부(서버 보관)
|
||||
번호표만 봐선 누군지 모르고, 장부와 맞춰봐야 아는 구조라 비밀번호가 오가지 않아요.`;
|
||||
|
||||
const CODE_COOKIE_FLAGS = `Set-Cookie 뒤에 붙는 옵션들 — 번호표 도난 방지 장치
|
||||
|
||||
HttpOnly 자바스크립트가 쿠키를 못 읽음
|
||||
→ 악성 스크립트가 심어져도 번호표를 훔쳐가기 어려움
|
||||
Secure HTTPS일 때만 전송
|
||||
→ 암호화 안 된 길로는 번호표를 아예 안 내보냄
|
||||
SameSite 다른 사이트에서 시작된 요청엔 안 붙임
|
||||
→ 낚시 사이트가 내 번호표를 슬쩍 쓰는 것(CSRF) 방지
|
||||
|
||||
이 세 줄이 로그인 보안의 기본기예요. 백엔드 과제에서 다시 만나게 됩니다!`;
|
||||
|
||||
const CODE_TLS = `HTTPS = HTTP + 봉투(TLS). 편지 내용은 같고, '봉인'이 추가된 것.
|
||||
|
||||
HTTP = 엽서. 배달부(중간 라우터들)가 마음만 먹으면 내용을 다 읽어요.
|
||||
아이디·비밀번호도 그대로 노출!
|
||||
HTTPS = 밀봉 봉투. 지나가는 누구도 내용을 못 보고, 뜯으면 티가 남.
|
||||
|
||||
TLS 핸드셰이크 — 봉투를 봉인하기 전 나누는 짧은 인사:
|
||||
|
||||
브라우저: "안녕! 나 이런 암호 방식들 쓸 줄 알아" (Client Hello)
|
||||
서 버: "그럼 이걸로 하자. 그리고 이건 내 신분증(인증서)!" (Server Hello)
|
||||
브라우저: (신분증 진짜인지 검사... 통과!)
|
||||
"좋아, 그럼 우리 둘만 아는 비밀 열쇠 만들자" (키 교환)
|
||||
양 쪽: 지금부터 모든 대화를 그 열쇠로 잠가서 주고받음 🔒
|
||||
|
||||
핵심 아이디어:
|
||||
- 신분 확인은 '자물쇠 공개 + 열쇠 비공개' 방식(공개키)으로,
|
||||
- 실제 대화는 둘만 아는 빠른 열쇠(대칭키)로.
|
||||
느리지만 안전한 방법으로 '열쇠 전달'만 하고, 본대화는 빠른 열쇠로 하는 거예요.`;
|
||||
|
||||
const CODE_CERT = `인증서 = 서버의 신분증. 그런데 신분증은 누가 발급해 줄까?
|
||||
|
||||
[브라우저] ──"edu.awesomedevapp.com 맞아?"──> [우리 서버]
|
||||
│
|
||||
"여기 인증서! CA가 보증했어"
|
||||
│
|
||||
[브라우저]: 내 안에 미리 들어 있는 '신뢰 기관(CA) 목록'과 대조
|
||||
→ 목록에 있는 기관의 도장이면 자물쇠 아이콘 🔒 표시
|
||||
→ 아니면 "주의 요함" 빨간 경고!
|
||||
|
||||
우리 플랫폼의 인증서 발급 흐름 (자동!):
|
||||
|
||||
Let's Encrypt(무료 CA) <──"나 이 도메인 진짜 주인이야" 증명──> Caddy(우리 서버)
|
||||
|
||||
Caddy가 알아서:
|
||||
1) Let's Encrypt에 "edu.awesomedevapp.com 인증서 주세요" 신청
|
||||
2) "그 도메인 주인 맞는지 확인할게" 챌린지에 자동 응답
|
||||
3) 발급받은 인증서로 HTTPS 서비스 + 만료(90일) 전에 자동 갱신
|
||||
|
||||
예전엔 인증서를 돈 주고 사서 수동 갱신했는데, 깜빡하면 사이트에
|
||||
경고가 떠서 대형 사고였어요. 지금은 Caddy가 다 해줍니다. 고마운 친구!`;
|
||||
|
||||
const CODE_NETWORK_LAB = `실습: 개발자 도구로 우리 API 요청 해부하기 (5분)
|
||||
|
||||
1) 우리 학습 플랫폼에 로그인한 상태로 아무 페이지나 열기
|
||||
2) F12 → Network 탭 → 필터에서 "Fetch/XHR" 선택
|
||||
(이미지·CSS 말고 'API 요청'만 골라 보는 필터예요)
|
||||
3) F5 새로고침 → 목록에서 /api/ 로 시작하는 요청 하나 클릭
|
||||
|
||||
이제 오늘 배운 걸 전부 실물로 확인:
|
||||
|
||||
Headers 탭
|
||||
Request Method: GET ← 섹션 1의 메서드!
|
||||
Status Code: 200 OK ← 섹션 2의 상태코드!
|
||||
Request Headers > Cookie: ← 섹션 3의 번호표! (SESSION=...)
|
||||
Response Headers > Content-Type: application/json
|
||||
|
||||
Response 탭
|
||||
서버가 보낸 바디(JSON 원문)가 그대로 보여요.
|
||||
|
||||
주소창 자물쇠 🔒 클릭 > 인증서
|
||||
발급 대상: edu.awesomedevapp.com
|
||||
발급자: Let's Encrypt ← 섹션 5의 그 CA!
|
||||
유효 기간: 90일짜리인지 확인해 보세요.`;
|
||||
|
||||
const CODE_QUIZ = `셀프 체크 5문항 — 답을 소리 내어 설명할 수 있으면 통과!
|
||||
|
||||
Q1. 로그인 요청은 왜 GET이 아니라 POST일까요?
|
||||
힌트: GET은 보낼 내용을 주소(URL)에 싣는 경우가 많아요.
|
||||
비밀번호가 주소창·기록에 남는다면? (섹션 1)
|
||||
|
||||
Q2. 401과 403의 차이를 학생증 비유로 설명해 보세요. (섹션 2)
|
||||
|
||||
Q3. 서버는 매 요청마다 내가 누군지 어떻게 알까요?
|
||||
"쿠키"와 "세션" 두 단어를 모두 써서 설명해 보세요. (섹션 3)
|
||||
|
||||
Q4. 카페 공용 와이파이에서 http:// 사이트에 로그인하면
|
||||
무엇이 위험할까요? https:// 라면 왜 괜찮을까요? (섹션 4)
|
||||
|
||||
Q5. 브라우저는 처음 보는 서버의 인증서를 뭘 근거로 믿을까요?
|
||||
힌트: 브라우저 안에 미리 들어 있는 것. (섹션 5)
|
||||
|
||||
→ 내가 쓴 답이 맞는지 멘토와 함께 확인해 보세요.
|
||||
"말로 설명하다 막히는 지점"이 진짜 복습 포인트입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '요청과 응답' },
|
||||
{ n: 2, label: '상태코드 읽기' },
|
||||
{ n: 3, label: '쿠키와 세션' },
|
||||
{ n: 4, label: 'HTTPS와 TLS' },
|
||||
{ n: 5, label: '인증서' },
|
||||
{ n: 6, label: 'Network 탭 해부' },
|
||||
{ n: 7, label: '정리 퀴즈' },
|
||||
];
|
||||
|
||||
export default function HttpPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 다루는 대화의 '문법'과 '봉인' */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>HTTP와 HTTPS<br />— 브라우저와 서버의 대화법, 그리고 봉인</h1>
|
||||
<p>
|
||||
브라우저와 서버가 주고받는 편지(요청/응답)의 문법을 배우고, 그 편지를
|
||||
아무도 못 엿보게 봉인하는 HTTPS까지 따라갑니다. 마지막엔 개발자 도구로
|
||||
<strong> 우리 플랫폼의 진짜 API 요청</strong>을 직접 해부해 봐요 —
|
||||
이 페이지를 여러분에게 배달한 바로 그 요청들을요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 퀴즈 5문항</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="HTTP 요청과 응답의 구조" sub="편지의 겉봉(시작줄·헤더)과 내용물(바디)">
|
||||
<p>
|
||||
<Link to="/learn/network">네트워크의 이해</Link> 코스에서 데이터가 달리는
|
||||
<strong> 길</strong>을 배웠다면, 이번엔 그 길 위를 오가는 <strong>편지의 형식</strong>을
|
||||
배울 차례예요. HTTP는 브라우저(클라이언트)와 서버가 주고받는 편지의
|
||||
<strong> 약속된 양식</strong>입니다. 요청도 응답도 구조는 똑같이 세 부분 —
|
||||
<strong> 시작줄, 헤더, 바디</strong>예요.
|
||||
</p>
|
||||
<Code>{CODE_REQUEST}</Code>
|
||||
<p>
|
||||
<strong>헤더</strong>는 편지 겉봉에 적는 메모예요. "받는 사람"(Host),
|
||||
"답장 형식 요청"(Accept), "내가 누군지"(Cookie) 같은 <strong>편지에 대한 정보</strong>가
|
||||
들어가고, <strong>바디</strong>엔 진짜 내용물(로그인 정보, JSON 데이터 등)이 들어갑니다.
|
||||
</p>
|
||||
<p>
|
||||
시작줄 맨 앞의 <strong>메서드</strong>는 "이 편지의 용건"이에요.
|
||||
같은 주소라도 메서드에 따라 서버가 하는 일이 완전히 달라집니다.
|
||||
</p>
|
||||
<Code>{CODE_METHODS}</Code>
|
||||
<div className="tip">
|
||||
<b>우리 코드에서는</b> 프론트(React)에서 <span className="icode">fetch('/api/courses')</span>를
|
||||
쓰면 GET 요청이, <span className="icode">{'fetch(url, { method: "POST", body: ... })'}</span>를
|
||||
쓰면 POST 요청이 나갑니다. 백엔드(Spring Boot)는
|
||||
<span className="icode">@GetMapping</span>·<span className="icode">@PostMapping</span>으로
|
||||
받고요. 프론트와 백엔드가 <strong>같은 HTTP 문법</strong>으로 만나는 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="상태코드 읽기" sub="서버의 대답 첫마디 — 첫 숫자만 봐도 반은 안다">
|
||||
<p>
|
||||
응답의 시작줄엔 항상 세 자리 숫자, <strong>상태코드</strong>가 옵니다.
|
||||
시험 점수처럼 숫자대마다 의미가 정해져 있어서, <strong>첫 숫자만 봐도</strong>
|
||||
성공인지(2), 이사 갔는지(3), 요청한 쪽 잘못인지(4), 서버 잘못인지(5)를 알 수 있어요.
|
||||
</p>
|
||||
<Code>{CODE_STATUS}</Code>
|
||||
<div className="warn">
|
||||
<b>디버깅 황금률</b> — API가 안 될 때 첫 질문은 언제나 "상태코드가 뭐야?"입니다.
|
||||
<strong> 4xx면 내 요청부터 의심</strong>(주소 오타? 로그인 했나? 바디 형식 맞나?),
|
||||
<strong> 5xx면 서버 로그부터 확인</strong>. 이 습관 하나로 디버깅 시간이 반으로 줄어요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 브라우저 주소창에
|
||||
<span className="icode"> https://edu.awesomedevapp.com/api/이런주소는없어요 </span>
|
||||
처럼 일부러 엉터리 경로를 쳐 보세요. <span className="kbd">F12</span> →
|
||||
<span className="kbd"> Network</span> 탭에서 방금 요청의 상태가
|
||||
<strong> 404</strong>로 빨갛게 찍히는 걸 확인! 로그아웃 상태로 내 정보 API를
|
||||
호출하면 <strong>401</strong>도 만날 수 있어요. 에러 코드를 "일부러" 만들어 보면
|
||||
실전에서 만났을 때 하나도 안 무섭습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="쿠키와 세션" sub="금붕어 기억력의 HTTP에게 '단골 손님'을 알아보게 하는 법">
|
||||
<p>
|
||||
HTTP의 중요한 성격 하나 — <strong>무상태(stateless)</strong>. 요청 하나를 처리하고
|
||||
나면 서버는 그 손님을 <strong>깨끗이 잊어버려요</strong>. 그런데 우리 플랫폼은
|
||||
로그인하면 페이지를 옮겨 다녀도 계속 "김미림 님"으로 알아보죠? 그 비밀이
|
||||
<strong> 쿠키와 세션</strong>, 카페의 <strong>진동벨(번호표)과 주문 장부</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_COOKIE}</Code>
|
||||
<p>
|
||||
핵심은 <strong>역할 분담</strong>이에요. 브라우저는 번호표(쿠키)를 들고 다니며
|
||||
매 요청에 자동으로 내밀고, 서버는 장부(세션)에서 "이 번호표 = 이 사람"을 대조합니다.
|
||||
비밀번호는 <strong>로그인 그 한 번</strong>만 전송되고, 이후엔 번호표만 오가요.
|
||||
</p>
|
||||
<Code>{CODE_COOKIE_FLAGS}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 우리 플랫폼에 로그인한 뒤 <span className="kbd">F12</span> →
|
||||
<span className="kbd"> Application</span> 탭(파이어폭스는 Storage) → 왼쪽에서
|
||||
<strong> Cookies</strong>를 열어 보세요. 내 번호표(세션 쿠키)가 실제로 저장돼 있어요.
|
||||
이름과 <strong>HttpOnly·Secure 체크 표시</strong>를 확인해 보고 —
|
||||
그 쿠키를 <strong>삭제</strong>한 뒤 새로고침하면? 번호표를 잃어버렸으니
|
||||
로그아웃 상태가 됩니다. 방금 로그인 유지의 원리를 직접 부숴서(!) 증명한 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="HTTPS와 TLS 핸드셰이크" sub="엽서를 봉투에 넣기 — 그리고 봉인 전의 짧은 인사">
|
||||
<p>
|
||||
HTTP 편지는 사실 <strong>엽서</strong>예요. 내 PC에서 서버까지 가는 길에
|
||||
수많은 중간 지점(공유기, 통신사 장비...)을 지나는데, 엽서니까
|
||||
<strong> 지나가는 누구나 내용을 읽을 수 있어요</strong>. 로그인 요청이라면
|
||||
아이디와 비밀번호가 그대로 보인다는 뜻이죠. 그래서 편지를
|
||||
<strong> 밀봉 봉투</strong>에 넣기로 한 것이 <strong>HTTPS</strong>,
|
||||
그 봉투 기술의 이름이 <strong>TLS</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_TLS}</Code>
|
||||
<p>
|
||||
이 인사(핸드셰이크)가 해결하는 문제는 두 가지예요.
|
||||
하나, <strong>"너 진짜 그 서버 맞아?"</strong> — 인증서로 신분 확인(다음 섹션!).
|
||||
둘, <strong>"열쇠를 어떻게 안전하게 나눠 갖지?"</strong> — 도청당하는 길 위에서
|
||||
둘만 아는 열쇠를 만드는 수학적 마법(공개키 암호)으로요. 인사가 끝나면
|
||||
그 열쇠로 잠근 대화만 오가서, 중간에서 훔쳐봐도 <strong>암호문 덩어리</strong>만 보입니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>그래서 공용 와이파이가 무서운 거예요</b> — 카페 와이파이의 주인은 그 길을 지나는
|
||||
트래픽을 들여다볼 수 있어요. <span className="icode">http://</span> 사이트에서 입력한 건
|
||||
엽서처럼 노출되지만, <span className="icode">https://</span>라면 봉투 속이라 안전합니다.
|
||||
우리 플랫폼이 항상 자물쇠(🔒)를 달고 있는 이유, 그리고 섹션 3의 쿠키에
|
||||
<strong> Secure</strong> 옵션을 붙이는 이유가 바로 이거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="인증서와 Let's Encrypt" sub="신분증은 아무나 발급하면 안 되니까">
|
||||
<p>
|
||||
봉투만으로는 부족해요. <strong>가짜 서버</strong>가 "내가 edu.awesomedevapp.com이야"
|
||||
라고 속이면서 봉투 대화를 걸어올 수도 있으니까요. 그래서 서버는
|
||||
<strong> 인증서</strong>라는 신분증을 내밀고, 그 신분증은 아무나 못 만들고
|
||||
<strong> CA(인증 기관)</strong>라는 공인된 발급처의 도장이 있어야 합니다.
|
||||
주민등록증을 문방구가 아니라 주민센터에서 발급하는 것과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_CERT}</Code>
|
||||
<p>
|
||||
우리 플랫폼의 인증서는 <strong>Let's Encrypt</strong>라는 무료 CA에서 받아요.
|
||||
더 좋은 건, 서버 현관을 지키는 <strong>Caddy</strong>가 발급 신청부터
|
||||
<strong> 90일마다의 갱신까지 전부 자동</strong>으로 처리한다는 것.
|
||||
여러분이 나중에 개인 프로젝트를 EC2에 올릴 때도 같은 방법으로 자물쇠를 달 수 있습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>미리 보기</b> 주소창의 자물쇠 아이콘을 클릭하면 <strong>인증서 보기</strong> 메뉴가
|
||||
있어요. 발급자(Issuer)에 Let's Encrypt가 적혀 있는지, 유효 기간이 90일짜리인지 —
|
||||
다음 섹션의 실습에서 함께 확인해 볼 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="Network 탭으로 우리 API 해부하기" sub="오늘 배운 전부를 실물로 — 종합 실습">
|
||||
<p>
|
||||
이제 이론은 끝. <strong>이 페이지를 여러분에게 배달한 진짜 요청들</strong>을 열어서
|
||||
섹션 1~5에서 배운 걸 전부 눈으로 확인할 시간이에요. 개발자 도구의
|
||||
<strong> Network 탭</strong>은 앞으로 여러분이 프론트·백엔드를 만들 때
|
||||
<strong> 매일 열게 될 창</strong>이니, 오늘 손에 익혀 두세요.
|
||||
</p>
|
||||
<Code>{CODE_NETWORK_LAB}</Code>
|
||||
<p>
|
||||
여기서 본 <strong>Request Headers / Response Headers / Response 바디</strong>가
|
||||
섹션 1의 편지 구조 그대로고, <strong>Status Code</strong>가 섹션 2,
|
||||
<strong> Cookie 헤더</strong>가 섹션 3, <strong>자물쇠와 인증서</strong>가
|
||||
섹션 4~5입니다. 한 화면에 이 코스 전체가 들어 있는 셈이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>한 걸음 더</b> Network 탭에서 요청을 <strong>우클릭 → Copy → Copy as cURL</strong>을
|
||||
해 보세요. 브라우저가 보낸 요청을 터미널에서 그대로 재현할 수 있는 명령어가 복사됩니다.
|
||||
메서드·헤더·쿠키가 어떻게 명령어 옵션으로 바뀌는지 구경해 보면, "브라우저도 결국
|
||||
HTTP 편지를 조립해서 보내는 프로그램일 뿐"이라는 게 실감 나요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="정리 퀴즈 — 셀프 체크 5문항" sub="설명할 수 있어야 아는 것">
|
||||
<Code>{CODE_QUIZ}</Code>
|
||||
<div className="tip">
|
||||
<b>공부 팁</b> 답을 종이에 쓰지 말고 <strong>옆 사람에게 말로 설명</strong>해 보세요.
|
||||
엽서·봉투·번호표·장부 — 오늘 나온 비유로 설명할 수 있으면 진짜 이해한 거예요.
|
||||
막히는 문항은 해당 섹션으로 돌아가 실습을 한 번 더!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🔒 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 브라우저와 서버가 나누는 대화의 <strong>문법(요청/응답)</strong>과
|
||||
<strong> 봉인(HTTPS)</strong>을 모두 알게 됐어요. 데이터가 달리는 길이 궁금하면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link>로,
|
||||
이 편지에 실려 오는 화면(HTML·JS)이 어떻게 만들어지는지 궁금하면{' '}
|
||||
<Link to="/learn/coding"><strong>코딩 기초</strong></Link>로 이어가 보세요.
|
||||
그리고 다음 과제에서 여러분이 직접 만들 API가 — 바로 오늘 해부한 그 편지들을
|
||||
보내는 쪽이 됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
369
frontend/src/pages/courses/IconsImagesPage.jsx
Normal file
369
frontend/src/pages/courses/IconsImagesPage.jsx
Normal file
@ -0,0 +1,369 @@
|
||||
// 이 파일이 하는 일: "아이콘과 이미지" 코스 — 비트맵과 벡터의 차이에서 출발해
|
||||
// 해상도(@2x), 좋은 아이콘의 조건, 저작권, 이미지 최적화, 파비콘·OG 이미지까지
|
||||
// 7개 섹션으로 안내하는 정적 학습 페이지. (마지막 섹션은 SVG를 메모장으로 여는 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_BITMAP_VS_VECTOR = `비트맵 vs 벡터 — 같은 그림, 완전히 다른 저장 방식
|
||||
|
||||
비트맵 (PNG · JPG) 벡터 (SVG)
|
||||
──────────────────────── ────────────────────────
|
||||
"픽셀 색을 전부 기록" "그리는 법을 기록"
|
||||
1번 칸은 빨강, 2번 칸은 파랑... "중심 (50,50)에 반지름 40 원을 그려"
|
||||
확대하면 → 칸이 커져서 계단현상 확대하면 → 다시 계산해 그림 → 항상 매끈
|
||||
사진에 최적 (색이 수백만 가지) 아이콘·로고에 최적 (도형 몇 개)
|
||||
|
||||
레고 모자이크 vs 설계도 비유:
|
||||
비트맵 = 레고 브릭으로 만든 그림. 가까이서 보면 브릭이 다 보여요.
|
||||
벡터 = "여기에 원, 저기에 선" 설계도. 어떤 크기로 지어도 매끈해요.`;
|
||||
|
||||
const CODE_FORMAT_TABLE = `언제 어떤 포맷? — 실무 선택 기준표
|
||||
|
||||
포맷 종류 투명배경 최적 용도
|
||||
─────────────────────────────────────────────
|
||||
JPG 비트맵 ✗ 사진 (풍경·인물, 색이 많은 이미지)
|
||||
PNG 비트맵 ○ 스크린샷, 투명 배경이 필요한 비트맵
|
||||
SVG 벡터 ○ 아이콘, 로고, 단순 일러스트
|
||||
WebP 비트맵 ○ JPG·PNG 대체 — 같은 품질에 더 작음
|
||||
|
||||
한 줄 요약:
|
||||
사진이면 JPG(또는 WebP), 아이콘·로고면 SVG,
|
||||
"투명 배경 + 비트맵"이면 PNG.`;
|
||||
|
||||
const CODE_RETINA = `해상도와 @2x — 화면 픽셀 밀도 이야기
|
||||
|
||||
일반 모니터: CSS의 1px = 실제 화소 1개
|
||||
고해상도 폰: CSS의 1px = 실제 화소 2개(@2x)~3개(@3x)
|
||||
|
||||
100×100으로 보여줄 이미지를 준비한다면?
|
||||
|
||||
icon.png (100×100) → 일반 화면용
|
||||
icon@2x.png (200×200) → 고해상도 화면용 (2배 크게 만들어 축소 표시)
|
||||
|
||||
100×100짜리를 고해상도 폰에서 그대로 쓰면?
|
||||
→ 화소 4개가 픽셀 1개의 색을 나눠 가져야 해서 뿌옇게 보여요.
|
||||
|
||||
그런데 SVG라면? @2x, @3x 전부 필요 없음!
|
||||
어차피 "그리는 법"이라 어떤 밀도에서도 다시 매끈하게 그려집니다.
|
||||
아이콘에 SVG를 쓰는 가장 실용적인 이유예요.`;
|
||||
|
||||
const CODE_ICON_RULES = `좋은 아이콘의 3가지 조건 — 단순 · 일관 · 의미
|
||||
|
||||
1) 단순함
|
||||
16~24px 크기에서도 알아볼 수 있어야 한다.
|
||||
→ 디테일이 많으면 작아졌을 때 뭉개진 점이 된다.
|
||||
|
||||
2) 일관성
|
||||
한 화면의 아이콘은 한 가족처럼 보여야 한다.
|
||||
→ 선 굵기(2px이면 전부 2px), 모서리 둥글기, 채움/외곽선
|
||||
스타일을 통일. 한 세트에서 골라 쓰는 게 정답.
|
||||
|
||||
3) 의미 (관습 존중)
|
||||
돋보기 = 검색, 톱니바퀴 = 설정, 집 = 홈, 휴지통 = 삭제.
|
||||
→ 이미 전 세계가 합의한 기호. 창의력을 발휘할 곳이 아니다.
|
||||
"예쁜데 무슨 뜻인지 모를 아이콘"은 실패한 아이콘.`;
|
||||
|
||||
const CODE_OPTIMIZE = `이미지 최적화 — 용량이 곧 속도
|
||||
|
||||
같은 사진, 처리에 따른 용량 차이 (예시)
|
||||
|
||||
원본 (폰 카메라, 4000×3000 JPG) ...... 약 5 MB
|
||||
화면 크기에 맞게 리사이즈 (1200px) ...... 약 400 KB
|
||||
WebP로 변환 + 압축 ...... 약 150 KB
|
||||
|
||||
→ 30배 이상 차이! 페이지에 사진이 10장이면 50MB vs 1.5MB.
|
||||
|
||||
기억할 순서:
|
||||
1) 리사이즈 — 보여줄 크기보다 큰 이미지를 올리지 않는다
|
||||
(프로필 사진 영역이 200px인데 4000px 원본을 올리면 낭비)
|
||||
2) 포맷 선택 — 사진은 JPG/WebP, 아이콘은 SVG
|
||||
3) 압축 — 압축 도구를 거치면 눈으로 구분 안 되는 수준에서 확 줄어듦
|
||||
4) 확인 — F12 → Network 탭에서 실제 전송 용량 확인`;
|
||||
|
||||
const CODE_FAVICON_OG = `파비콘과 OG 이미지 — 우리 서비스의 '얼굴' 두 장
|
||||
|
||||
파비콘 (favicon)
|
||||
브라우저 탭 왼쪽의 작은 아이콘 (16~32px).
|
||||
<head> 안에서 선언:
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
→ 탭이 수십 개 열려 있을 때 우리 서비스를 찾게 해주는 이름표.
|
||||
|
||||
OG 이미지 (Open Graph)
|
||||
링크를 카톡·메신저에 공유했을 때 뜨는 미리보기 카드 이미지.
|
||||
<meta property="og:image" content="https://.../og.png" />
|
||||
<meta property="og:title" content="어썸데브 학습 센터" />
|
||||
→ 권장 1200×630. 이게 없으면 링크가 밋밋한 글자로만 공유돼요.
|
||||
|
||||
둘 다 "코드 한 줄 + 이미지 한 장"이지만,
|
||||
서비스의 첫인상을 결정하는 디테일입니다.`;
|
||||
|
||||
const CODE_SVG_SOURCE = `메모장으로 연 SVG의 실제 내용 — 이미지가 아니라 '코드'!
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
|
||||
읽어 보면:
|
||||
circle → 중심 (11,11), 반지름 8짜리 원
|
||||
line → (21,21)에서 (16.65,16.65)까지 직선
|
||||
= 원 하나 + 짧은 선 하나 = 돋보기(검색) 아이콘!
|
||||
|
||||
stroke-width="2"를 "4"로 바꿔 저장하고 브라우저로 열면
|
||||
선이 굵어져요. 그림을 '수정'한 게 아니라 '코드를 고친' 겁니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '비트맵 vs 벡터' },
|
||||
{ n: 2, label: '해상도와 @2x' },
|
||||
{ n: 3, label: '좋은 아이콘의 조건' },
|
||||
{ n: 4, label: '무료 소스와 저작권' },
|
||||
{ n: 5, label: '이미지 최적화' },
|
||||
{ n: 6, label: '파비콘 · OG 이미지' },
|
||||
{ n: 7, label: '실습: SVG 열어보기' },
|
||||
];
|
||||
|
||||
export default function IconsImagesPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 다루는 범위 소개 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>아이콘과 이미지<br />— 화면 위의 그림, 제대로 다루기</h1>
|
||||
<p>
|
||||
웹 화면의 절반은 그림이에요 — 아이콘, 사진, 로고, 미리보기 이미지까지.
|
||||
이 코스에서는 그림 파일의 두 가지 종류부터 저작권, 최적화, 그리고
|
||||
<strong> SVG가 사실은 코드라는 것</strong>을 직접 눈으로 확인하는 실습까지 다룹니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 3개</span>
|
||||
<span className="chip">준비물: 내 PC와 메모장</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="비트맵(PNG·JPG) vs 벡터(SVG)" sub="확대해 보면 정체가 드러난다">
|
||||
<p>
|
||||
컴퓨터가 그림을 저장하는 방식은 딱 두 가지예요.
|
||||
<strong> 비트맵</strong>은 그림을 아주 작은 칸(픽셀)으로 나눠서
|
||||
<strong> 칸마다 무슨 색인지 전부 기록</strong>하는 방식이고,
|
||||
<strong> 벡터</strong>는 색을 기록하는 대신
|
||||
<strong> "어떻게 그리는지"라는 설계도</strong>를 기록하는 방식입니다.
|
||||
</p>
|
||||
<Code>{CODE_BITMAP_VS_VECTOR}</Code>
|
||||
<p>
|
||||
구분법은 간단해요 — <strong>확대해 보면</strong> 됩니다. 비트맵은 확대할수록
|
||||
네모 칸(픽셀)이 드러나면서 계단처럼 깨지고, 벡터는 아무리 확대해도 매끈해요.
|
||||
설계도는 크게 지어도 설계도니까요.
|
||||
</p>
|
||||
<Code>{CODE_FORMAT_TABLE}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 학습 플랫폼을 연 상태에서
|
||||
<span className="kbd">Ctrl</span> + <span className="kbd">+</span> 를 여러 번 눌러
|
||||
화면을 300~400%로 확대해 보세요. 메뉴의 아이콘(SVG)은 여전히 매끈한데,
|
||||
사진(비트맵)은 슬슬 뭉개지기 시작할 거예요. 다 보고 나면
|
||||
<span className="kbd">Ctrl</span> + <span className="kbd">0</span> 으로 원래 크기 복귀!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="해상도와 @2x" sub="같은 1px인데 폰에서는 왜 4배 촘촘할까">
|
||||
<p>
|
||||
요즘 스마트폰 화면은 같은 면적에 화소를 훨씬 촘촘하게 박아 넣어요.
|
||||
그래서 코드에서 말하는 <span className="icode">1px</span>이 실제로는
|
||||
화소 <strong>2개(@2x)</strong> 또는 <strong>3개(@3x)</strong>로 그려집니다.
|
||||
도화지 한 칸을 더 잘게 쪼개 색칠하는 셈이라 글씨와 그림이 훨씬 선명하죠.
|
||||
</p>
|
||||
<Code>{CODE_RETINA}</Code>
|
||||
<p>
|
||||
그래서 비트맵 이미지는 <strong>보여줄 크기의 2배로 만들어 두는</strong> 관습이
|
||||
생겼어요 — 파일명 뒤의 <span className="icode">@2x</span>가 그 표시입니다.
|
||||
디자이너에게 아이콘을 받을 때 "@2x로 주세요"라는 말이 오가는 이유예요.
|
||||
그리고 이 번거로움을 한 방에 없애는 게 벡터 — <strong>SVG는 애초에 설계도라
|
||||
어떤 화면 밀도에서도 스스로 매끈하게 그려집니다.</strong>
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>비유 한 스푼</b> 비트맵 @1x를 고해상도 화면에 쓰는 건, A4로 뽑은 포스터를
|
||||
현수막 크기로 확대 복사하는 것과 같아요. 원본이 작으면 아무리 좋은 화면도
|
||||
없는 정보를 만들어 내지 못합니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="좋은 아이콘의 조건" sub="단순하게, 일관되게, 뜻이 통하게">
|
||||
<p>
|
||||
아이콘은 <strong>글자 없이 뜻을 전하는 그림 언어</strong>예요. 화장실 표지판이나
|
||||
비상구 픽토그램처럼, 처음 보는 사람도 0.5초 안에 알아봐야 합니다.
|
||||
잘 만든 아이콘에는 세 가지 공통점이 있어요.
|
||||
</p>
|
||||
<Code>{CODE_ICON_RULES}</Code>
|
||||
<p>
|
||||
특히 <strong>일관성</strong>이 초보가 가장 많이 놓치는 부분이에요. 이 사이트
|
||||
저 사이트에서 아이콘을 하나씩 주워 오면, 선 굵기와 스타일이 제각각이라
|
||||
화면이 어수선해집니다. 교복 입은 반에 혼자 사복 입고 온 느낌이랄까요.
|
||||
<strong> 반드시 한 아이콘 세트 안에서 골라 쓰세요.</strong>
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 학습 플랫폼의 메뉴 아이콘들을 관찰해 보세요.
|
||||
선 굵기가 같은지, 전부 외곽선(라인) 스타일로 통일돼 있는지,
|
||||
돋보기·집·톱니바퀴가 관습대로 쓰였는지 — 배운 3가지 조건으로 채점해 보는 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="무료 소스와 저작권" sub="'무료'와 '마음대로'는 다른 말이다">
|
||||
<p>
|
||||
아이콘과 사진을 전부 직접 그릴 수는 없으니, 실무에서는 무료 아이콘 세트와
|
||||
무료 이미지 사이트를 많이 활용해요. 오픈소스 아이콘 세트, 무료 스톡 사진
|
||||
사이트처럼 좋은 소스가 많습니다. <strong>단, 다운로드 버튼이 있다고 해서
|
||||
마음대로 써도 된다는 뜻은 절대 아니에요.</strong>
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>이것만은 꼭! — 라이선스를 확인하지 않고 쓰면 회사가 위험해집니다</b>
|
||||
<p>
|
||||
인터넷의 모든 이미지에는 <strong>저작권</strong>이 있어요. 검색해서 나온 이미지를
|
||||
회사 서비스에 그냥 쓰면 <strong>저작권 침해</strong>이고, 손해배상 청구로
|
||||
이어질 수 있습니다. "학생이라서", "몰랐어서"는 면책 사유가 아니에요.
|
||||
쓰기 전에 반드시 확인하세요.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li><strong>상업적 이용 가능?</strong> — 우리 회사 서비스에 쓰는 건 전부 상업적 이용이에요. "개인/비상업용 무료"는 회사에서 못 씁니다.</li>
|
||||
<li><strong>출처 표기(저작자 표시) 필요?</strong> — CC BY 같은 라이선스는 무료지만 저작자 이름을 표기해야 해요. 조건을 지켜야 무료입니다.</li>
|
||||
<li><strong>수정 가능?</strong> — 색을 바꾸거나 잘라 쓰는 것도 '수정'이에요. 수정 금지(ND) 라이선스는 원본 그대로만 써야 합니다.</li>
|
||||
</ol>
|
||||
<p>
|
||||
<strong>CC(Creative Commons)</strong>는 "이 조건만 지키면 써도 좋다"는 약속
|
||||
표시예요. CC0는 조건 없음, CC BY는 저작자 표시, BY-NC는 비상업만…
|
||||
글자가 늘수록 조건이 늘어난다고 기억하세요. 조금이라도 애매하면
|
||||
<strong> 혼자 판단하지 말고 멘토에게 물어보는 것</strong> — 그게 프로의 습관입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>안전한 기본기</b> 라이선스가 명확한 무료 아이콘 세트 하나, 상업적 이용이
|
||||
허용된 무료 이미지 사이트 한두 곳을 '내 단골'로 정해 두세요. 매번 검색으로
|
||||
아무 이미지나 줍는 것보다 훨씬 빠르고, 훨씬 안전합니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="이미지 최적화" sub="용량이 곧 속도, 속도가 곧 사용자 경험">
|
||||
<p>
|
||||
웹페이지가 느린 원인 1순위는 거의 항상 <strong>이미지 용량</strong>이에요.
|
||||
코드(HTML·JS)는 보통 수십~수백 KB인데, 최적화 안 된 사진 한 장이
|
||||
5MB를 넘기도 하거든요. 책가방에 교과서 대신 아령을 넣고 다니는 셈이죠.
|
||||
</p>
|
||||
<Code>{CODE_OPTIMIZE}</Code>
|
||||
<p>
|
||||
특히 <strong>리사이즈</strong>가 핵심이에요. 화면에 200px로 보일 프로필 사진에
|
||||
4000px 원본을 올리면, 사용자는 쓰지도 않을 픽셀 수백만 개를 다운로드하는
|
||||
거예요. 서버(AWS EC2)의 트래픽 비용도 그만큼 늘어나고요 —
|
||||
<strong> 최적화는 사용자에게도, 회사 지갑에도 좋은 일</strong>입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 학습 플랫폼에서 <span className="kbd">F12</span> →
|
||||
<span className="kbd">Network</span> 탭 → 필터에서 <span className="icode">Img</span>를
|
||||
선택하고 <span className="kbd">F5</span> 새로고침. 이미지마다 전송 용량(Size)이
|
||||
보여요. 가장 무거운 이미지는 몇 KB인가요? SVG 아이콘들이 얼마나 가벼운지도
|
||||
비교해 보세요 — 보통 1KB 안팎입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="파비콘과 OG 이미지" sub="탭 속 이름표와 공유 미리보기 카드">
|
||||
<p>
|
||||
서비스에는 화면 속 그림 말고도 중요한 이미지가 두 장 더 있어요.
|
||||
하나는 브라우저 <strong>탭에 뜨는 작은 아이콘(파비콘)</strong>, 다른 하나는
|
||||
링크를 메신저에 공유했을 때 뜨는 <strong>미리보기 카드(OG 이미지)</strong>입니다.
|
||||
둘 다 서비스의 명함 같은 존재예요.
|
||||
</p>
|
||||
<Code>{CODE_FAVICON_OG}</Code>
|
||||
<p>
|
||||
파비콘이 없으면 탭에 밋밋한 기본 아이콘이 뜨고, OG 이미지가 없으면 링크
|
||||
공유가 글자만 덜렁 있는 초라한 모습이 돼요. 기능에는 지장이 없지만
|
||||
<strong> "관리 안 된 서비스"라는 인상</strong>을 줍니다. 작지만 완성도를
|
||||
가르는 디테일이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 학습 플랫폼과 Gitea(<span className="icode">edu.awesomedevapp.com</span>)의
|
||||
탭 아이콘을 비교해 보세요. 그리고 아무 페이지에서 <span className="kbd">F12</span> →
|
||||
<span className="kbd">Elements</span> 탭에서 <span className="icode"><head></span>를
|
||||
펼쳐 <span className="icode">link rel="icon"</span>과
|
||||
<span className="icode">og:image</span> 태그를 찾아보세요. 방금 배운 두 장의 얼굴이
|
||||
코드로는 이렇게 선언돼 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습: SVG를 메모장으로 열어 보기" sub="이미지인 줄 알았는데, 코드였다">
|
||||
<p>
|
||||
이 코스의 하이라이트 실습이에요. SVG 파일을 <strong>이미지 뷰어가 아니라
|
||||
메모장으로</strong> 열어 보면, 그림이 아니라 <strong>글자(코드)</strong>가
|
||||
나타납니다. 벡터가 "그리는 법의 기록"이라는 말을 눈으로 확인하는 순간이죠.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>브라우저에서 아무 사이트의 SVG 아이콘에 우클릭 → <strong>"이미지를 다른 이름으로 저장"</strong>으로 <span className="icode">.svg</span> 파일을 바탕화면에 저장해요. (플랫폼 프로젝트를 받았다면 <span className="icode">frontend/src/assets</span> 폴더 안의 SVG를 써도 좋아요.)</li>
|
||||
<li>저장한 파일에 우클릭 → <strong>연결 프로그램 → 메모장</strong>으로 열어요.</li>
|
||||
<li>그림 대신 <span className="icode"><svg></span>로 시작하는 코드가 보이면 성공!</li>
|
||||
<li>같은 파일을 이번엔 브라우저로 열어 보세요(파일을 브라우저 창에 끌어다 놓기). 똑같은 내용이 그림으로 그려집니다.</li>
|
||||
</ol>
|
||||
<Code>{CODE_SVG_SOURCE}</Code>
|
||||
<p>
|
||||
한 발 더 — 메모장에서 <span className="icode">stroke-width</span> 숫자나
|
||||
색 값을 바꿔 저장한 뒤 브라우저를 새로고침해 보세요. <strong>포토샵 없이
|
||||
텍스트 편집만으로 그림이 바뀝니다.</strong> 그래서 SVG는 개발자가 코드로
|
||||
색을 입히고(<span className="icode">currentColor</span>), CSS로 애니메이션까지
|
||||
걸 수 있는 특별한 이미지예요. React 컴포넌트 안에 직접 넣을 수도 있고요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>정리 한 줄</b> 비트맵은 "완성된 그림"이라 편집 도구가 필요하지만,
|
||||
SVG는 "그림을 그리는 코드"라 우리가 배우는 개발 도구로 다룰 수 있어요.
|
||||
아이콘의 세계에서 SVG가 표준이 된 이유입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🎨 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 그림 파일의 두 종류(비트맵·벡터), @2x가 필요한 이유, 좋은 아이콘의
|
||||
3조건, <strong>저작권 확인 습관</strong>, 최적화 순서, 파비콘·OG까지 —
|
||||
화면 위의 그림을 다루는 기본기를 갖췄어요. 다음은 이 그림들이 놓일 화면
|
||||
전체의 배치를 배울 차례 — <Link to="/learn/design"><strong>디자인 기초</strong></Link> 코스에서
|
||||
여백과 정렬을 이어서 배우고, <Link to="/learn/coding"><strong>코딩 기초</strong></Link>에서
|
||||
SVG를 컴포넌트로 다루는 법까지 연결해 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
422
frontend/src/pages/courses/IpDeepPage.jsx
Normal file
422
frontend/src/pages/courses/IpDeepPage.jsx
Normal file
@ -0,0 +1,422 @@
|
||||
// 이 파일이 하는 일: "IP 주소 깊이 보기" 코스 — IPv4 주소의 생김새와 고갈 문제에서 출발해
|
||||
// 사설 대역, 서브넷마스크, 게이트웨이, DHCP, IPv6, 그리고 우리 EC2의 탄력적 IP까지
|
||||
// 8개 섹션으로 파고드는 정적 학습 페이지.
|
||||
// (IPv4 구조 → 사설 대역 → 서브넷마스크 → 게이트웨이 → DHCP → IPv6 → ipconfig /all 해부 → 고정 vs 유동)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_IPV4_STRUCT = `IPv4 주소 = 32비트를 8비트씩 4칸으로 끊어 읽은 것
|
||||
|
||||
192 . 168 . 0 . 5 ← 사람이 읽는 표기 (점으로 구분)
|
||||
┌────────┬────────┬────────┬────────┐
|
||||
│11000000│10101000│00000000│00000101│ ← 컴퓨터가 보는 실제 모습
|
||||
└────────┴────────┴────────┴────────┘
|
||||
8비트 8비트 8비트 8비트 = 총 32비트
|
||||
|
||||
한 칸(옥텟)은 8비트 → 0~255 (2^8 = 256가지)
|
||||
전체 조합 = 2^32 ≈ 약 43억 개
|
||||
|
||||
43억이면 많아 보이지만... 지구 인구가 이미 80억이 넘고,
|
||||
1인당 폰·노트북·태블릿·IoT 기기까지 생각하면 턱없이 부족해요.
|
||||
→ 이게 바로 "IPv4 고갈 문제"입니다. (해결책은 섹션 2와 6에서!)`;
|
||||
|
||||
const CODE_PRIVATE_RANGES = `사설 IP 3대 대역 — 전 세계 누구나 '집 안에서만' 마음껏 쓰는 주소
|
||||
|
||||
대역 크기 암기법
|
||||
──────────────────────────────────────────────────────────
|
||||
10.0.0.0 ~ 10.255.255.255 약 1,678만 개 "10 하나만 외우면 끝"
|
||||
172.16.0.0 ~ 172.31.255.255 약 104만 개 "172의 16~31층만"
|
||||
192.168.0.0 ~ 192.168.255.255 약 6.5만 개 "집 공유기의 국민 주소"
|
||||
|
||||
암기 요령:
|
||||
· 10.x.x.x → 제일 크다. 큰 회사·클라우드가 즐겨 씀 (우리 AWS VPC도!)
|
||||
· 172.16~31.x.x → 어중간해서 제일 헷갈림. "16부터 31까지 딱 16개 층"
|
||||
· 192.168.x.x → 집·학교 공유기 단골. ipconfig 치면 십중팔구 이 녀석
|
||||
|
||||
이 주소들은 인터넷(바깥세상)에서는 라우터가 아예 안 받아 줍니다.
|
||||
그래서 옆집도 우리 집도 똑같이 192.168.0.5를 써도 충돌이 없어요 —
|
||||
아파트마다 302호가 있어도 괜찮은 것과 같죠. 덕분에 43억 개뿐인
|
||||
IPv4를 전 세계가 '재활용'하며 버티고 있는 겁니다. (NAT의 마법)`;
|
||||
|
||||
const CODE_SUBNET = `서브넷마스크 = "주소에서 어디까지가 단지 이름이고, 어디부터가 호수인가"
|
||||
|
||||
주소: 192.168.0.5
|
||||
마스크: 255.255.255.0 ← /24 라고도 써요 (앞에서부터 1이 24개)
|
||||
|
||||
┌─ 네트워크 부분 (아파트 단지 이름) ─┐┌ 호스트 부분 ┐
|
||||
주소(2진수): 11000000.10101000.00000000 . 00000101
|
||||
마스크: 11111111.11111111.11111111 . 00000000
|
||||
└────────── 1이 24개 = /24 ──────┘└─ 남은 8비트 ┘
|
||||
|
||||
해석: "192.168.0 까지가 우리 단지 이름, 마지막 칸이 각 세대 호수"
|
||||
→ 호수로 쓸 수 있는 번호: 2^8 = 256개
|
||||
그중 .0(단지 자체를 가리키는 이름)과 .255(단지 전체 방송용)를 빼면
|
||||
실제 입주 가능한 세대 = 254개
|
||||
|
||||
같은 단지(같은 네트워크) 주민끼리는 경비실(게이트웨이)을 안 거치고
|
||||
바로 대화할 수 있고, 단지가 다르면 반드시 경비실을 거쳐야 해요.
|
||||
서브넷마스크는 그 '단지 경계선'을 긋는 자입니다.`;
|
||||
|
||||
const CODE_GATEWAY = `게이트웨이 = 우리 단지의 유일한 정문(경비실)
|
||||
|
||||
[내 PC 192.168.0.5] 목적지: 8.8.8.8 (다른 단지!)
|
||||
│
|
||||
│ "이 주소, 우리 단지(192.168.0.x)가 아니네?
|
||||
│ → 그럼 무조건 정문으로 보내자"
|
||||
▼
|
||||
[게이트웨이 192.168.0.1] ← 공유기. 보통 단지의 1호를 차지해요
|
||||
│
|
||||
▼
|
||||
바깥 인터넷으로...
|
||||
|
||||
PC의 판단 로직 (매 패킷마다!):
|
||||
1) 목적지 IP를 서브넷마스크로 잘라 본다
|
||||
2) 우리 단지 주소와 같으면 → 직접 전달 (경비실 안 거침)
|
||||
3) 다르면 → 게이트웨이(정문)로 던진다. "나머진 알아서 부탁해!"
|
||||
|
||||
게이트웨이 주소를 잘못 설정하면? 단지 안 친구와는 대화가 되는데
|
||||
인터넷만 안 되는, 그 유명한 "내부망은 되는데 외부가 안 돼요" 상태가 됩니다.`;
|
||||
|
||||
const CODE_DHCP = `DHCP 임대 4단계 — 새 기기가 IP를 '빌리는' 과정 (DORA로 외워요)
|
||||
|
||||
내 노트북 공유기(DHCP 서버)
|
||||
│ │
|
||||
│── ① Discover ─────────────────────────▶│
|
||||
│ "여기 DHCP 서버 계세요? 저 IP 필요해요!" (단지 전체에 방송)
|
||||
│ │
|
||||
│◀───────────────────────── ② Offer ──── │
|
||||
│ "저요! 192.168.0.7 어때요? 24시간 빌려드림"
|
||||
│ │
|
||||
│── ③ Request ──────────────────────────▶│
|
||||
│ "좋아요, 그 주소 저 주세요!" │
|
||||
│ │
|
||||
│◀──────────────────── ④ Acknowledge ─── │
|
||||
│ "확정! 192.168.0.7 / 마스크 255.255.255.0
|
||||
│ 게이트웨이 192.168.0.1 / DNS는 이거 쓰세요. 임대기간 24시간"
|
||||
|
||||
포인트: IP만 주는 게 아니라 마스크·게이트웨이·DNS까지
|
||||
'네트워크 생활 풀세트'를 한 번에 배정해 줘요.
|
||||
'임대(lease)'라는 말 그대로 기간이 있어서, 절반쯤 지나면
|
||||
기기가 알아서 "연장할게요" 하고 갱신합니다. 전세 재계약처럼요.`;
|
||||
|
||||
const CODE_IPV6 = `IPv6 — 주소가 모자라면? 자릿수를 늘리면 되지!
|
||||
|
||||
IPv4: 32비트 → 약 43억 개 (4.3 × 10^9)
|
||||
IPv6: 128비트 → 약 3.4 × 10^38 개
|
||||
(지구 모든 모래알에 주소를 붙여도 남는 수준)
|
||||
|
||||
생김새: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
└ 16진수 4자리 × 8덩어리, 콜론(:)으로 구분
|
||||
|
||||
줄여 쓰기 규칙:
|
||||
· 각 덩어리 앞의 0 생략: 0db8 → db8
|
||||
· 연속된 0000 덩어리는 :: 로 한 번만 축약
|
||||
2001:0db8:0000:0000:0000:0000:0000:0001
|
||||
→ 2001:db8::1 (훨씬 낫죠?)
|
||||
|
||||
이미 여러분 PC에도 IPv6 주소가 조용히 붙어 있어요 (다음 섹션에서 확인!).
|
||||
다만 전 세계 장비가 다 바뀌는 데 시간이 걸려서, 지금은
|
||||
IPv4(+NAT)와 IPv6가 나란히 달리는 과도기입니다.`;
|
||||
|
||||
const CODE_IPCONFIG_ALL = `# Windows 터미널(cmd/PowerShell)에서:
|
||||
ipconfig /all
|
||||
|
||||
# 주요 줄 해부 — 이 코스에서 배운 게 전부 여기 있어요:
|
||||
|
||||
이더넷 어댑터 이더넷: (또는 Wi-Fi)
|
||||
물리적 주소 . . . : A1-B2-C3-D4-E5-F6 ← MAC 주소. 랜카드의 '주민번호'
|
||||
DHCP 사용 . . . . : 예 ← 섹션 5! IP를 빌려 쓰는 중
|
||||
IPv6 주소 . . . . : fe80::... ← 섹션 6! 이미 갖고 있었죠?
|
||||
IPv4 주소 . . . . : 192.168.0.5 ← 섹션 2! 사설 대역
|
||||
서브넷 마스크 . . : 255.255.255.0 ← 섹션 3! /24, 우리 단지 경계
|
||||
기본 게이트웨이 . : 192.168.0.1 ← 섹션 4! 단지 정문
|
||||
DHCP 서버 . . . . : 192.168.0.1 ← IP를 빌려준 임대인 = 공유기
|
||||
임대 시작 날짜 . .: 2026-07-16 오전... ← 섹션 5의 '임대' 그 자체!
|
||||
임대 만료 날짜 . .: 2026-07-17 오전... ← 24시간짜리 계약이네요
|
||||
DNS 서버 . . . . .: ... ← DHCP가 풀세트로 준 전화번호부
|
||||
|
||||
# 게이트웨이와 DHCP 서버 주소가 같죠? 집 공유기 한 대가
|
||||
# 정문 경비(게이트웨이) + 임대인(DHCP)을 겸직하고 있기 때문입니다.`;
|
||||
|
||||
const CODE_ELASTIC_IP = `고정 IP vs 유동 IP — 그리고 우리 EC2의 '탄력적 IP'
|
||||
|
||||
유동(동적) IP: DHCP가 그때그때 빌려줌. 만료·재부팅 때 바뀔 수 있음
|
||||
고정(정적) IP: 손으로 못 박아 둠. 절대 안 바뀜 — 서버에겐 필수!
|
||||
|
||||
왜 서버는 고정이어야 할까?
|
||||
→ 도메인(edu.awesomedevapp.com)이 DNS에 "이 IP로 가"라고
|
||||
등록돼 있는데, 서버 IP가 바뀌면 전화번호부가 통째로 틀려져요.
|
||||
가게가 매주 이사 다니면 단골이 못 찾아오는 것과 같죠.
|
||||
|
||||
그런데 AWS EC2는 기본이 유동이에요:
|
||||
EC2 인스턴스를 중지(stop)했다 시작(start)하면 공인 IP가 바뀜!
|
||||
|
||||
그래서 쓰는 게 '탄력적 IP(Elastic IP)':
|
||||
[고정 공인 IP 하나를 내 계정 명의로 예약] ──연결──▶ [서울 EC2 인스턴스]
|
||||
· 인스턴스를 껐다 켜도 이 IP는 그대로
|
||||
· 서버를 새 인스턴스로 갈아타도, IP만 옮겨 붙이면 DNS는 손댈 필요 없음
|
||||
· 가게 건물을 옮겨도 '전화번호(대표 IP)'는 그대로 가져가는 셈
|
||||
|
||||
우리 플랫폼이 언제나 같은 주소로 열리는 건,
|
||||
서울 EC2에 탄력적 IP가 못 박혀 있고 → DNS가 그 IP를 가리키고 →
|
||||
Caddy가 443 포트에서 손님을 맞이하기 때문입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'IPv4 구조와 고갈' },
|
||||
{ n: 2, label: '사설 대역 암기법' },
|
||||
{ n: 3, label: '서브넷마스크' },
|
||||
{ n: 4, label: '게이트웨이' },
|
||||
{ n: 5, label: 'DHCP 임대' },
|
||||
{ n: 6, label: 'IPv6 맛보기' },
|
||||
{ n: 7, label: 'ipconfig /all 해부' },
|
||||
{ n: 8, label: '고정 vs 유동 IP' },
|
||||
];
|
||||
|
||||
export default function IpDeepPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 무엇을 얼마나 깊이 파는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>IP 주소 깊이 보기<br />— 숫자 네 칸에 숨은 세계</h1>
|
||||
<p>
|
||||
"네트워크의 이해"에서 스치듯 지나간 <span className="icode">192.168.0.5</span> 같은
|
||||
숫자를 이번엔 비트 단위까지 뜯어봅니다. 사설 대역 암기법부터 서브넷마스크,
|
||||
DHCP 임대, 그리고 우리 서울 EC2에 못 박힌 탄력적 IP까지 —
|
||||
숫자 네 칸을 읽을 줄 알면 네트워크 장애의 절반은 눈으로 진단할 수 있어요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 이상</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="IPv4의 구조와 고갈 문제" sub="점 세 개로 나뉜 32비트, 그리고 43억의 한계">
|
||||
<p>
|
||||
IP 주소는 사실 <strong>숫자 네 개가 아니라 비트 32개</strong>예요. 컴퓨터는
|
||||
0과 1로 된 32자리 숫자 하나로 보는데, 사람이 읽기 힘드니까 8비트씩 네 칸으로
|
||||
끊고 점을 찍어 표기하는 것뿐이죠. 한 칸이 8비트라서 각 칸은
|
||||
<strong> 0~255</strong>까지만 가능해요 — <span className="icode">192.168.0.300</span> 같은
|
||||
주소가 존재할 수 없는 이유입니다.
|
||||
</p>
|
||||
<Code>{CODE_IPV4_STRUCT}</Code>
|
||||
<p>
|
||||
1980년대에 설계할 땐 43억 개면 영원히 충분해 보였어요. 하지만 지금은 한 사람이
|
||||
폰·노트북·워치까지 기기를 여러 대 들고 다니는 시대 — 주소가 진작에 바닥났습니다.
|
||||
인류는 두 가지 답을 냈어요. 하나는 <strong>사설 IP + NAT로 주소를 재활용</strong>하기(섹션 2),
|
||||
다른 하나는 <strong>자릿수를 왕창 늘린 새 체계, IPv6</strong>(섹션 6)입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>비유로 기억하기</b> IPv4는 전 세계에 43억 장만 발행된 <strong>한정판 번호표</strong>예요.
|
||||
번호표가 모자라자 "건물 안에서는 자체 번호표(사설 IP)를 쓰고, 밖에 나갈 때만
|
||||
건물 대표 번호표(공인 IP)를 빌려 쓰자"는 아이디어가 나온 거죠.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="사설 대역 3형제 암기법" sub="10 · 172.16 · 192.168 — 시험에도 실무에도 평생 나오는 숫자">
|
||||
<p>
|
||||
국제 규약(RFC 1918)은 IPv4 주소 중 <strong>딱 세 구역</strong>을 떼어
|
||||
"이건 누구나 내부에서만 자유롭게 써라"고 정해 뒀어요. 이 세 대역은 면접에서도,
|
||||
서버 설정에서도, 장애 대응에서도 끝없이 마주치니 지금 확실히 외워 둡시다.
|
||||
</p>
|
||||
<Code>{CODE_PRIVATE_RANGES}</Code>
|
||||
<p>
|
||||
실무 감각 하나: 터미널에서 본 IP가 <span className="icode">10.x</span>나
|
||||
<span className="icode">192.168.x</span>면 "아, 내부망 주소구나 — 바깥에서는 이 주소로
|
||||
못 들어오겠네"가 <strong>반사적으로</strong> 떠올라야 해요. 우리 회사 AWS도
|
||||
VPC 내부는 <span className="icode">10.x.x.x</span> 사설 대역을 쓰고, 바깥과의 접점에만
|
||||
공인 IP를 붙입니다(섹션 8에서 자세히!).
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>제일 많이 틀리는 함정</b> — 172 대역은 <span className="icode">172.16.x.x</span>부터
|
||||
<span className="icode">172.31.x.x</span>까지 <strong>만</strong> 사설이에요.
|
||||
<span className="icode">172.15.x.x</span>나 <span className="icode">172.32.x.x</span>는
|
||||
남의 공인 IP입니다. "172는 16층부터 31층까지만 우리 것"으로 기억하세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="서브넷마스크 — 아파트 단지의 경계선" sub="/24가 대체 무슨 뜻일까">
|
||||
<p>
|
||||
IP 주소 32비트는 사실 <strong>두 부분의 합체</strong>예요. 앞부분은
|
||||
<strong> 네트워크 주소</strong>(어느 아파트 단지인지), 뒷부분은
|
||||
<strong> 호스트 주소</strong>(그 단지의 몇 호인지). 그런데 어디까지가 단지 이름이고
|
||||
어디부터가 호수인지는 주소만 봐선 몰라요 — 그 <strong>경계선을 알려 주는 자</strong>가
|
||||
바로 서브넷마스크입니다.
|
||||
</p>
|
||||
<Code>{CODE_SUBNET}</Code>
|
||||
<p>
|
||||
<span className="icode">255.255.255.0</span>과 <span className="icode">/24</span>는
|
||||
같은 말이에요. 2진수로 펼치면 1이 앞에서부터 24개라서 "슬래시 24"라고 줄여 부르는
|
||||
것뿐이죠. <strong>/24 단지에는 254세대</strong>가 살 수 있고, 만약
|
||||
<span className="icode">/16</span>(255.255.0.0)이라면 호스트 비트가 16개라
|
||||
6만 세대가 넘는 초대형 단지가 됩니다. 회사 네트워크를 설계할 때 "이 팀 서브넷은
|
||||
/24면 충분한가?" 같은 결정이 전부 이 계산에서 나와요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>3초 판별법</b> 두 기기가 <strong>같은 단지</strong>인지 보려면: 서브넷마스크가
|
||||
255인 칸끼리 비교해서 전부 같으면 같은 네트워크! <span className="icode">192.168.0.5</span>와
|
||||
<span className="icode">192.168.0.200</span>은 (/24 기준) 같은 단지,
|
||||
<span className="icode">192.168.1.5</span>는 다른 단지예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="게이트웨이 — 단지의 정문" sub="'우리 단지가 아니면 무조건 정문으로'">
|
||||
<p>
|
||||
PC는 패킷을 보낼 때마다 딱 한 가지를 판단해요 — <strong>"목적지가 우리 단지인가?"</strong>
|
||||
같은 단지면 직접 건네고, 아니면 고민 없이 <strong>게이트웨이</strong>(대개 공유기)에게
|
||||
넘깁니다. 단지 밖 세상의 길은 정문 경비실이 다 알아서 하니까요.
|
||||
</p>
|
||||
<Code>{CODE_GATEWAY}</Code>
|
||||
<p>
|
||||
섹션 3의 서브넷마스크가 왜 중요한지 여기서 드러나요. <strong>마스크가 경계선을 긋고,
|
||||
게이트웨이가 그 경계 밖으로 나가는 유일한 문</strong>이 됩니다. 이 둘 중 하나만
|
||||
잘못 설정돼도 "같은 사무실 프린터는 되는데 인터넷이 안 돼요" 같은,
|
||||
겉보기에 이상한 장애가 생겨요 — 이제 여러분은 원인을 짚을 수 있습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 터미널에서 <span className="icode">ping 192.168.0.1</span>
|
||||
(내 게이트웨이 주소는 섹션 7 실습에서 확인)을 쳐 보세요. 응답이 오면 "정문까지는
|
||||
길이 뚫려 있다"는 뜻이에요. 인터넷이 안 될 때 <strong>정문까지는 되는지</strong>부터
|
||||
확인하는 게 네트워크 장애 진단의 1단계입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="DHCP — IP를 '빌리는' 4단계" sub="Discover, Offer, Request, Acknowledge = DORA">
|
||||
<p>
|
||||
새 기기가 와이파이에 붙는 순간, 우리 눈에 안 보이는 <strong>4단계 임대 계약</strong>이
|
||||
순식간에 체결돼요. 부동산에 비유하면 ① 세입자가 "방 있나요?" 외치고 ② 임대인이
|
||||
"이 방 어때요?" 제안하고 ③ "계약할게요!" ④ "도장 쾅!" — 이 흐름을 첫 글자를 따서
|
||||
<strong> DORA</strong>라고 외웁니다.
|
||||
</p>
|
||||
<Code>{CODE_DHCP}</Code>
|
||||
<p>
|
||||
핵심은 DHCP가 <strong>IP 하나만 주는 게 아니라</strong>는 점이에요. 지금까지 배운
|
||||
서브넷마스크(섹션 3)·게이트웨이(섹션 4)·DNS 서버 주소까지 세트로 배정해 줍니다.
|
||||
우리가 카페 와이파이에 붙자마자 아무 설정 없이 인터넷이 되는 건, 이 계약이
|
||||
1초 안에 자동으로 끝나기 때문이에요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>'임대'라는 말에 담긴 뜻</b> — 기한이 지나면 주소를 반납해요. 그래서 유동 IP는
|
||||
어느 날 바뀌어 있을 수 있죠. 노트북에게는 아무 문제 없지만, <strong>서버가 이러면
|
||||
큰일</strong>납니다(섹션 8에서 이어짐).
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="IPv6 맛보기" sub="주소가 모자라? 그럼 3.4 × 10^38개로 늘리자">
|
||||
<p>
|
||||
섹션 1의 고갈 문제에 대한 정공법이 <strong>IPv6</strong>예요. 32비트를 128비트로
|
||||
늘려서, 주소 개수가 <strong>상상 자체가 안 되는 수준</strong>으로 많아졌습니다.
|
||||
지구의 모래알 전부에 주소를 붙이고도 남아요 — 사설 IP로 아껴 쓸 필요조차 없죠.
|
||||
</p>
|
||||
<Code>{CODE_IPV6}</Code>
|
||||
<p>
|
||||
표기법이 낯설지만 원리는 같아요. 16진수라 <span className="icode">a~f</span>가 섞여
|
||||
있고, 콜론으로 구분하며, 0이 연속되면 <span className="icode">::</span>로 접어 줍니다.
|
||||
국내 통신사들도 이미 IPv6를 함께 제공하고 있어서, 여러분의 폰은 지금도 두 체계를
|
||||
<strong> 동시에</strong> 쓰고 있을 가능성이 높아요. 다음 섹션 실습에서 내 PC의
|
||||
IPv6 주소를 직접 찾아봅시다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습 — ipconfig /all 해부하기" sub="배운 걸 전부 내 PC에서 눈으로 찾기">
|
||||
<p>
|
||||
이번 코스의 하이라이트 실습이에요. <span className="icode">ipconfig</span> 뒤에
|
||||
<span className="icode"> /all</span>을 붙이면 숨어 있던 정보가 전부 나옵니다.
|
||||
출력의 <strong>한 줄 한 줄이 지금까지 배운 섹션과 1:1로 연결</strong>돼요 —
|
||||
아래 해부도와 내 화면을 나란히 놓고 대조해 보세요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> <span className="kbd">Win</span> + <span className="kbd">R</span> →
|
||||
<span className="icode">cmd</span> 입력 → 터미널에서 <span className="icode">ipconfig /all</span> 실행.
|
||||
아래 해부도의 항목을 내 PC에서 <strong>전부</strong> 찾아 체크해 보세요.
|
||||
특히 <strong>임대 시작/만료 날짜</strong> — DHCP 계약서의 기간 조항을 실물로 보는 순간입니다.
|
||||
</div>
|
||||
<Code>{CODE_IPCONFIG_ALL}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 같은 터미널에서 <span className="icode">ipconfig /release</span> →
|
||||
<span className="icode">ipconfig /renew</span>를 차례로 실행해 보세요. 방금 배운
|
||||
<strong> DHCP 임대 계약을 해지했다가 다시 맺는</strong> 과정이에요. 잠깐 인터넷이 끊겼다
|
||||
돌아오고, 대개 같은 IP를 다시 받습니다(공유기가 "단골이니 같은 방 드릴게요" 하는 셈).
|
||||
와이파이 PC라면 연결이 몇 초 출렁일 수 있으니 다운로드 중엔 하지 마세요!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="고정 IP vs 유동 IP — 그리고 우리 EC2" sub="서버의 주소는 왜 못 박아야 하는가">
|
||||
<p>
|
||||
집 PC는 IP가 바뀌어도 아무도 안 곤란해요 — 우리는 항상 <strong>거는 쪽</strong>이니까요.
|
||||
하지만 서버는 <strong>받는 쪽</strong>입니다. 전 세계가 DNS 전화번호부에 적힌 번호로
|
||||
찾아오는데, 그 번호가 수시로 바뀌면 아무도 못 찾아와요. 그래서
|
||||
<strong> 서버의 공인 IP는 반드시 고정</strong>이어야 합니다.
|
||||
</p>
|
||||
<Code>{CODE_ELASTIC_IP}</Code>
|
||||
<p>
|
||||
정리하면 우리 플랫폼의 주소 체계는 3단 구조예요.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li><strong>탄력적 IP</strong> — 서울 EC2에 못 박힌 고정 공인 IP. 인스턴스를 재시작해도 안 바뀜.</li>
|
||||
<li><strong>DNS</strong> — <span className="icode">edu.awesomedevapp.com</span>이 그 IP를 가리키도록 등록.</li>
|
||||
<li><strong>Caddy</strong> — 그 IP의 443 포트에서 HTTPS로 손님을 맞아 Spring Boot로 안내.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③</b> <span className="icode">nslookup edu.awesomedevapp.com</span>을
|
||||
오늘 한 번, 내일 한 번 실행해 보세요. IP가 <strong>이틀 연속 똑같다면</strong> —
|
||||
그게 바로 탄력적 IP가 일하고 있다는 증거예요. 반대로 브라우저에서 "내 아이피"를
|
||||
검색해 나온 <strong>우리 집 공인 IP</strong>는 공유기를 오래 껐다 켜면 바뀔 수도 있는
|
||||
유동 IP입니다. 고정과 유동을 내 손으로 비교해 본 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📍 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 <span className="icode">192.168.0.5/24</span> 같은 표기를 보면 사설 대역,
|
||||
단지 경계, 게이트웨이의 위치까지 한눈에 읽어낼 수 있어요. IP는 네트워크의
|
||||
<strong> 주소</strong>였다면, 다음은 그 주소로 오가는 <strong>대화의 규칙</strong>을
|
||||
배울 차례 — <Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스의
|
||||
DNS·TCP/IP·HTTP 섹션을 복습하고, 멘토에게 <strong>과제</strong>로
|
||||
"우리 EC2의 탄력적 IP와 VPC 사설 대역을 AWS 콘솔에서 찾아 그림으로 그려 오기"를
|
||||
받아 보세요. 콘솔을 눈으로 본 순간, 이 코스의 모든 개념이 실물이 됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
418
frontend/src/pages/courses/JavaBasicsPage.jsx
Normal file
418
frontend/src/pages/courses/JavaBasicsPage.jsx
Normal file
@ -0,0 +1,418 @@
|
||||
// 이 파일이 하는 일: "Java 기초" 코스 — JS만 써 본 사람이 Java의 세계관(타입·클래스·컴파일)을
|
||||
// 이해하고, 우리 백엔드(Spring Boot) 코드를 읽을 수 있게 되는 것을 목표로 하는 정적 학습 페이지.
|
||||
// (JS와의 차이 → 변수와 타입 → 클래스와 객체 → 메서드·생성자 → 접근제어자 → 컬렉션 → 예외 → 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// Java 코드 예제는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 코드 예제 · 비교표 상수들 ──
|
||||
|
||||
const CODE_JS_VS_JAVA = `같은 일을 하는 코드, 두 언어의 말투 차이
|
||||
|
||||
JavaScript (우리 프론트) Java (우리 백엔드)
|
||||
────────────────────────── ─────────────────────────────
|
||||
let age = 17; int age = 17;
|
||||
let name = "지수"; String name = "지수";
|
||||
age = "열일곱"; // 허용됨! age = "열일곱"; // 컴파일 에러!
|
||||
|
||||
→ JS는 변수에 아무거나 담을 수 있지만,
|
||||
Java는 "이 변수엔 정수만!" 하고 미리 선언하고, 어기면 실행 전에 막습니다.`;
|
||||
|
||||
const CODE_COMPILE = `두 언어가 실행되는 방식
|
||||
|
||||
JS: 코드 작성 → 브라우저가 읽으면서 바로 실행 (통역사가 옆에서 실시간 통역)
|
||||
Java: 코드 작성 → 컴파일(.java → .class 번역) → 실행 (책을 통째로 번역 출판)
|
||||
|
||||
컴파일의 좋은 점:
|
||||
- 오타·타입 실수를 "실행하기 전에" 전부 잡아줌
|
||||
- 미리 번역해 두니 실행 속도가 빠름
|
||||
|
||||
그래서 Java 백엔드는 "돌려보니 터지네?"가 아니라
|
||||
"애초에 빌드가 안 되네?"로 실수를 먼저 만납니다. 서버엔 이게 축복이에요.`;
|
||||
|
||||
const CODE_TYPES = `자주 쓰는 타입 3총사 (+ 친구들)
|
||||
|
||||
타입 담는 것 예시
|
||||
────────────────────────────────────────────
|
||||
int 정수 int stock = 30;
|
||||
String 문자열 String title = "Java 기초";
|
||||
boolean 참/거짓 boolean isDone = false;
|
||||
|
||||
double 소수 double price = 1500.5;
|
||||
long 아주 큰 정수 long userId = 9000000001L;
|
||||
|
||||
// 선언 = "타입 + 이름 + 값"
|
||||
int score = 95; // 만들면서 값 넣기
|
||||
String mentor; // 일단 선언만
|
||||
mentor = "선배님"; // 나중에 값 넣기
|
||||
mentor = 100; // ❌ String 자리에 숫자 → 컴파일 에러`;
|
||||
|
||||
const CODE_CLASS = `클래스 = 붕어빵 틀, 객체 = 붕어빵
|
||||
|
||||
// User.java — "회원"이라는 붕어빵 틀
|
||||
public class User {
|
||||
private String username; // 틀에 파인 홈: 아이디 자리
|
||||
private String nickname; // 닉네임 자리
|
||||
private boolean active; // 활성화 여부 자리
|
||||
}
|
||||
|
||||
// 틀(클래스)은 하나, 붕어빵(객체)은 원하는 만큼:
|
||||
User kim = new User(); // 붕어빵 1호
|
||||
User lee = new User(); // 붕어빵 2호 — 틀은 같지만 서로 다른 붕어빵!
|
||||
|
||||
→ new 키워드가 "틀에 반죽 부어서 하나 굽기"입니다.`;
|
||||
|
||||
const CODE_METHOD = `메서드 = 객체가 할 수 있는 행동
|
||||
|
||||
public class User {
|
||||
private boolean active;
|
||||
|
||||
// ① ② ③
|
||||
public boolean isActive() {
|
||||
return active; // ④
|
||||
}
|
||||
|
||||
public void deactivate() { // void = 돌려주는 값 없음
|
||||
this.active = false; // this = "지금 이 붕어빵 자신"
|
||||
}
|
||||
}
|
||||
|
||||
① 공개 범위 ② 반환 타입(무엇을 돌려주나) ③ 이름과 괄호(재료 받는 곳) ④ 결과 반환
|
||||
|
||||
// JS의 function과 비슷하지만, "무엇을 받아서 무엇을 돌려주는지"
|
||||
// 타입을 전부 약속해 두는 게 다릅니다. 읽는 사람이 문서 없이도 알 수 있죠.`;
|
||||
|
||||
const CODE_CONSTRUCTOR = `생성자 = 붕어빵 구울 때 반드시 넣을 재료 목록
|
||||
|
||||
public class User {
|
||||
private String username;
|
||||
private String nickname;
|
||||
|
||||
// 클래스와 이름이 같고, 반환 타입이 없으면 → 생성자!
|
||||
public User(String username, String nickname) {
|
||||
this.username = username; // this.username = 붕어빵의 홈
|
||||
this.nickname = nickname; // username = 방금 받은 재료
|
||||
}
|
||||
}
|
||||
|
||||
User kim = new User("kim01", "김붕어"); // 재료를 줘야 구워짐
|
||||
User bad = new User(); // ❌ 재료 없이는 못 구움 (컴파일 에러)
|
||||
|
||||
→ "아이디 없는 회원"이 아예 만들어질 수 없게 막는 안전장치예요.`;
|
||||
|
||||
const CODE_ACCESS = `접근제어자 = 문의 종류
|
||||
|
||||
public 누구나 열 수 있는 정문
|
||||
private 그 클래스 안에서만 여는 내 방 문
|
||||
|
||||
public class User {
|
||||
private String password; // 밖에서 직접 손대기 금지!
|
||||
|
||||
public boolean checkPassword(String input) {
|
||||
return this.password.equals(input); // 확인은 창구를 통해서만
|
||||
}
|
||||
}
|
||||
|
||||
user.password = "1234"; // ❌ private — 컴파일 에러
|
||||
user.checkPassword("1234"); // ⭕ 공개 창구(메서드)로만 접근
|
||||
|
||||
→ 데이터는 private으로 숨기고, 공개 메서드로만 만지게 하는 습관.
|
||||
은행 금고를 아무나 열게 두지 않고 창구 직원을 거치게 하는 것과 같아요.`;
|
||||
|
||||
const CODE_COLLECTIONS = `List = 번호표 있는 줄서기, Map = 이름표 달린 사물함
|
||||
|
||||
// List — 순서가 있는 목록 (JS의 배열과 비슷)
|
||||
List<String> mentors = new ArrayList<>();
|
||||
mentors.add("김선배"); // [김선배]
|
||||
mentors.add("이선배"); // [김선배, 이선배]
|
||||
mentors.get(0); // "김선배" (0번부터!)
|
||||
mentors.size(); // 2
|
||||
|
||||
// Map — 키(이름표)로 값을 찾는 사물함 (JS의 객체/Map과 비슷)
|
||||
Map<String, Integer> scores = new HashMap<>();
|
||||
scores.put("네트워크", 90); // "네트워크" 칸에 90 넣기
|
||||
scores.put("Java", 85);
|
||||
scores.get("Java"); // 85
|
||||
|
||||
// <String> <String, Integer> — 꺾쇠 안은 "이 통엔 이 타입만 담아요" 약속.
|
||||
// 여기서도 Java의 성격이 보이죠: 통에도 타입 라벨을 붙입니다.`;
|
||||
|
||||
const CODE_EXCEPTION = `예외 처리 = 사고가 나도 서버는 멈추지 않게
|
||||
|
||||
try {
|
||||
int stock = Integer.parseInt(input); // "30" → 30. 그런데 "삼십"이 오면?
|
||||
System.out.println(stock * 2);
|
||||
} catch (NumberFormatException e) {
|
||||
// 숫자로 못 바꾸는 값이 왔을 때 여기로 점프!
|
||||
System.out.println("숫자만 입력해 주세요: " + e.getMessage());
|
||||
}
|
||||
|
||||
try = "일단 해 볼게, 사고 나면 알려줘"
|
||||
catch = "사고 나면 이렇게 수습할게"
|
||||
|
||||
→ 예외 처리가 없으면 이용자 한 명의 이상한 입력에 프로그램 전체가 죽어요.
|
||||
자동차 에어백처럼, 안 터지는 게 최선이지만 반드시 달아 두는 장치입니다.`;
|
||||
|
||||
const CODE_HELLO = `// 1) IntelliJ에서 New Project → Java 선택 → 프로젝트 생성
|
||||
// 2) src 폴더 우클릭 → New → Java Class → 이름 "Hello"
|
||||
// 3) 아래처럼 입력:
|
||||
|
||||
public class Hello {
|
||||
public static void main(String[] args) {
|
||||
String name = "미림";
|
||||
int year = 3;
|
||||
System.out.println("안녕하세요, " + name + " " + year + "학년!");
|
||||
}
|
||||
}
|
||||
|
||||
// 4) main 옆의 ▶ 초록 화살표 클릭 → 아래 콘솔에 출력 확인!
|
||||
// main 메서드는 Java 프로그램의 "정문" — 실행하면 여기부터 시작합니다.
|
||||
|
||||
// 도전: name을 int로 바꿔 보세요. 실행 버튼을 누르기도 전에
|
||||
// IntelliJ가 빨간 줄로 화를 내는 걸 볼 수 있어요. 그게 컴파일 검사입니다.`;
|
||||
|
||||
const CODE_READING = `우리 백엔드 코드 읽기 체크리스트 — 도메인 클래스 하나를 골라서:
|
||||
|
||||
□ public class 클래스이름 → 이 붕어빵 틀의 이름은? (섹션 3)
|
||||
□ private 타입 필드이름; → 홈(필드)이 몇 개? 각각 무슨 타입? (섹션 2·6)
|
||||
□ 클래스이름과 같은 메서드 → 생성자가 재료를 몇 개 요구하나? (섹션 5)
|
||||
□ public ... 메서드( ) { } → 이 객체가 할 수 있는 행동은? (섹션 4)
|
||||
□ List<...> / Map<...> → 다른 객체를 여러 개 품고 있나? (섹션 7)
|
||||
|
||||
→ 다섯 칸이 다 채워지면, 그 클래스가 "무엇을 알고(필드)
|
||||
무엇을 할 수 있는지(메서드)" 한 문장으로 말해 보세요.
|
||||
그게 되면 Java 코드를 '읽을 줄 아는' 겁니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'JS와 뭐가 다를까' },
|
||||
{ n: 2, label: '변수와 타입' },
|
||||
{ n: 3, label: '클래스와 객체' },
|
||||
{ n: 4, label: '메서드' },
|
||||
{ n: 5, label: '생성자' },
|
||||
{ n: 6, label: '접근제어자' },
|
||||
{ n: 7, label: 'List와 Map' },
|
||||
{ n: 8, label: '예외와 실습' },
|
||||
];
|
||||
|
||||
export default function JavaBasicsPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 왜 필요하고 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 소프트웨어</div>
|
||||
<h1>Java 기초<br />— 우리 백엔드의 모국어 배우기</h1>
|
||||
<p>
|
||||
어썸데브의 백엔드(Spring Boot)는 전부 Java로 쓰여 있어요. 이 코스는 JS만 써 본
|
||||
사람이 Java의 세계관 — <strong>타입을 미리 약속하고, 실행 전에 검사한다</strong> —
|
||||
을 이해하고, 우리 서버 코드를 직접 읽어낼 수 있게 되는 것이 목표입니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 90분</span>
|
||||
<span className="chip">실습 2개 (IntelliJ + 코드 읽기)</span>
|
||||
<span className="chip">준비물: IntelliJ IDEA</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="JS와 뭐가 다를까" sub="타입을 미리 선언하고, 실행 전에 번역(컴파일)한다">
|
||||
<p>
|
||||
여러분이 프론트에서 쓰는 JavaScript는 <strong>자유로운 언어</strong>예요. 변수에
|
||||
숫자를 넣었다가 문자열을 넣어도 군말이 없죠. Java는 반대로 <strong>깐깐한
|
||||
언어</strong>입니다 — 변수를 만들 때 "여기엔 정수만 담을 거야" 하고
|
||||
<strong> 타입을 미리 선언</strong>하고, 약속을 어기면 실행조차 못 하게 막아요.
|
||||
</p>
|
||||
<Code>{CODE_JS_VS_JAVA}</Code>
|
||||
<p>
|
||||
두 번째 차이는 <strong>컴파일</strong>이에요. JS는 브라우저가 코드를 읽으면서 바로
|
||||
실행하는 <strong>실시간 통역</strong> 방식이고, Java는 실행 전에 코드를 통째로
|
||||
기계가 이해하는 말로 <strong>번역 출판</strong>하는 방식입니다.
|
||||
</p>
|
||||
<Code>{CODE_COMPILE}</Code>
|
||||
<div className="tip">
|
||||
<b>왜 백엔드는 깐깐한 언어를 쓸까?</b> 프론트에서 버그가 나면 그 사람 화면 하나가
|
||||
이상해지지만, 서버에서 버그가 나면 <strong>모든 이용자</strong>가 동시에 피해를 봐요.
|
||||
그래서 실수를 실행 전에 잡아 주는 깐깐함이 서버 쪽에선 큰 장점이 됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="변수와 타입" sub="int, String, boolean — 라벨 붙은 서랍">
|
||||
<p>
|
||||
Java의 변수는 <strong>라벨이 붙은 서랍</strong>이에요. <span className="icode">int</span> 라벨이
|
||||
붙은 서랍엔 정수만, <span className="icode">String</span> 서랍엔 문자열만 들어갑니다.
|
||||
라벨과 다른 물건을 넣으려 하면 넣는 순간(컴파일 때) 거부당해요.
|
||||
</p>
|
||||
<Code>{CODE_TYPES}</Code>
|
||||
<div className="warn">
|
||||
<b>대소문자 함정</b> — <span className="icode">int</span>·<span className="icode">boolean</span>은
|
||||
소문자로 시작하고, <span className="icode">String</span>은 대문자로 시작해요.
|
||||
<span className="icode">string</span>이라고 쓰면 에러! (String은 기본형이 아니라 클래스라서
|
||||
그래요 — 클래스 이름은 대문자로 시작한다는 규칙, 다음 섹션에서 만납니다.)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="클래스와 객체" sub="붕어빵 틀과 붕어빵">
|
||||
<p>
|
||||
Java 세계의 주인공입니다. <strong>클래스</strong>는 붕어빵 <strong>틀</strong>,
|
||||
<strong> 객체</strong>는 그 틀로 구운 <strong>붕어빵</strong>이에요. 틀에는
|
||||
"머리·꼬리·팥 넣는 자리"처럼 모양(필드)이 파여 있고, 같은 틀로 구워도
|
||||
팥붕·슈붕처럼 <strong>내용물(값)은 붕어빵마다 다를 수 있죠</strong>.
|
||||
</p>
|
||||
<Code>{CODE_CLASS}</Code>
|
||||
<p>
|
||||
멀리 있는 얘기가 아니에요 — 우리 플랫폼의 백엔드에도 <strong>User.java</strong>라는
|
||||
클래스가 실제로 있습니다. 여러분이 이 페이지에 로그인한 순간, 서버 어딘가에서
|
||||
<span className="icode">User</span> 틀로 구워진 "여러분 붕어빵" 객체가
|
||||
만들어져 돌아다니고 있어요. 지금 배우는 문법이 곧 우리 서비스의 실물입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>이름 규칙</b> — 클래스 이름은 <span className="icode">User</span>,
|
||||
<span className="icode">CourseProgress</span>처럼 <strong>대문자로 시작</strong>하고,
|
||||
변수·메서드 이름은 <span className="icode">userName</span>처럼 <strong>소문자로
|
||||
시작</strong>해요(camelCase). 코드만 보고 "아, 이건 클래스구나"를 구분하는 약속입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="메서드" sub="객체가 할 수 있는 '행동'">
|
||||
<p>
|
||||
필드가 객체가 <strong>아는 것</strong>(데이터)이라면, 메서드는 객체가
|
||||
<strong> 할 수 있는 것</strong>(행동)이에요. JS의 함수와 비슷하지만 결정적 차이가
|
||||
하나 있습니다 — <strong>무엇을 받아서(매개변수 타입) 무엇을 돌려주는지(반환 타입)</strong>를
|
||||
전부 코드에 약속해 둔다는 것.
|
||||
</p>
|
||||
<Code>{CODE_METHOD}</Code>
|
||||
<p>
|
||||
이 약속 덕분에 남이 만든 메서드도 <strong>선언부 한 줄만 읽으면</strong> 쓸 수 있어요.
|
||||
"<span className="icode">public boolean isActive()</span> — 아무것도 안 받고,
|
||||
참/거짓을 돌려주는구나" — 자판기 버튼에 가격과 상품이 적혀 있어서 내부 기계를
|
||||
몰라도 쓸 수 있는 것과 같습니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="생성자" sub="붕어빵 구울 때 반드시 넣어야 하는 재료">
|
||||
<p>
|
||||
<span className="icode">new User()</span>로 붕어빵을 구울 때, "이 재료는 꼭 받고
|
||||
시작할게"를 정하는 특별한 메서드가 <strong>생성자</strong>예요. 클래스와
|
||||
<strong> 이름이 똑같고 반환 타입이 없으면</strong> 생성자입니다.
|
||||
</p>
|
||||
<Code>{CODE_CONSTRUCTOR}</Code>
|
||||
<p>
|
||||
생성자의 진짜 역할은 <strong>불량품 방지</strong>예요. 아이디 없는 회원, 제목 없는
|
||||
게시글 같은 "반쯤 만들어진 객체"가 세상에 나오는 것을 태어나는 순간부터 막아 줍니다.
|
||||
급식실에서 식판 없이는 배식 줄에 못 서게 하는 것과 같아요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="접근제어자" sub="private이 기본인 이유 — 금고와 창구">
|
||||
<p>
|
||||
앞의 예제들에서 필드는 전부 <span className="icode">private</span>, 메서드는 대부분
|
||||
<span className="icode"> public</span>이었죠. 우연이 아니라 <strong>Java 세계의
|
||||
기본 예절</strong>이에요. 데이터(필드)는 금고에 넣어 잠그고, 꼭 필요한
|
||||
창구(메서드)만 열어 둡니다.
|
||||
</p>
|
||||
<Code>{CODE_ACCESS}</Code>
|
||||
<p>
|
||||
왜 이렇게까지 할까요? 필드를 <span className="icode">public</span>으로 열어 두면
|
||||
프로젝트의 <strong>아무 데서나</strong> 값을 바꿀 수 있어서, 버그가 났을 때 "누가
|
||||
언제 바꿨는지"를 추적할 수 없게 돼요. <span className="icode">private</span> + 공개
|
||||
메서드 구조면 값이 바뀌는 통로가 한 곳뿐이라, 검사·로그·검증을 그 창구 하나에만
|
||||
달면 됩니다. 이 습관을 <strong>캡슐화</strong>라고 불러요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="List와 Map" sub="여러 개를 담는 두 가지 통">
|
||||
<p>
|
||||
회원 한 명은 <span className="icode">User</span> 객체 하나지만, 서비스엔 회원이
|
||||
수백 명이죠. 여러 개를 담는 통이 <strong>컬렉션</strong>이고, 실무에서 제일 많이
|
||||
쓰는 둘이 <strong>List</strong>(번호표 있는 줄서기)와 <strong>Map</strong>(이름표
|
||||
달린 사물함)입니다.
|
||||
</p>
|
||||
<Code>{CODE_COLLECTIONS}</Code>
|
||||
<p>
|
||||
고르는 기준은 간단해요 — <strong>"몇 번째"로 찾고 싶으면 List</strong>,
|
||||
<strong> "이름으로" 찾고 싶으면 Map</strong>. 출석부는 번호 순서가 중요하니 List,
|
||||
신발장은 이름표로 바로 찾아야 하니 Map인 거죠.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>0번부터!</b> — List의 첫 번째 칸은 <span className="icode">get(0)</span>이에요.
|
||||
JS 배열과 똑같지만, "3명이 든 리스트의 마지막"이 <span className="icode">get(3)</span>이
|
||||
아니라 <span className="icode">get(2)</span>라는 건 몇 번을 배워도 헷갈리는 함정입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="예외 처리, 그리고 실습" sub="try-catch — 그리고 손으로 굳히기">
|
||||
<p>
|
||||
아무리 타입 검사가 깐깐해도, <strong>실행 중에만 알 수 있는 사고</strong>가 있어요.
|
||||
이용자가 숫자 칸에 "삼십"을 입력하는 것까지 컴파일러가 미리 알 수는 없으니까요.
|
||||
이런 사고에 대비하는 안전벨트가 <strong>try-catch</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_EXCEPTION}</Code>
|
||||
<p>이제 손으로 굳힐 시간이에요. 실습 두 개를 꼭 해 보세요.</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> IntelliJ에서 내 첫 Java 클래스를 만들어 실행해 보세요.
|
||||
아래 순서대로 하면 5분이면 됩니다. 마지막의 "도전"까지 꼭 —
|
||||
컴파일 에러를 <strong>일부러 내 보는 것</strong>이 오늘 배운 것의 핵심 체험이에요.
|
||||
</div>
|
||||
<Code>{CODE_HELLO}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 우리 Gitea(<span className="icode">edu.awesomedevapp.com</span>)에서
|
||||
백엔드 저장소를 열고, <span className="icode">domain</span> 폴더의 클래스 아무거나
|
||||
하나(예: <span className="icode">User.java</span>)를 골라 아래 체크리스트를 채워 보세요.
|
||||
읽기만 하는 실습이니 마음껏 열어 봐도 됩니다.
|
||||
</div>
|
||||
<Code>{CODE_READING}</Code>
|
||||
<ol className="olist">
|
||||
<li>체크리스트를 채우다 모르는 문법이 나오면 해당 섹션 번호로 돌아가 복습해요.</li>
|
||||
<li>다 채웠으면 그 클래스를 <strong>한 문장으로 요약</strong>해서 멘토에게 말해 보세요.</li>
|
||||
<li>멘토가 "왜 이 필드는 private일까?" 같은 꼬리 질문을 던질 거예요 — 그게 진짜 시험!</li>
|
||||
</ol>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>☕ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 <strong>타입과 컴파일</strong>이라는 Java의 세계관, <strong>클래스 →
|
||||
객체 → 메서드 → 생성자</strong>로 이어지는 뼈대, 데이터를 지키는
|
||||
<strong> private</strong>의 이유, <strong>List/Map/try-catch</strong>라는 실무
|
||||
도구까지 손에 넣었어요. 우리 백엔드 코드의 "단어"는 다 배운 셈입니다. 다음은
|
||||
그 단어들로 쓰인 "문장" — <Link to="/learn/spring-boot"><strong>Spring Boot
|
||||
입문</strong></Link> 코스에서 이 클래스들이 어떻게 API가 되는지 이어서 배워 보세요.
|
||||
실습 ②의 한 문장 요약은 멘토 확인 과제로 제출!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
390
frontend/src/pages/courses/LayoutPage.jsx
Normal file
390
frontend/src/pages/courses/LayoutPage.jsx
Normal file
@ -0,0 +1,390 @@
|
||||
// 이 파일이 하는 일: "레이아웃과 그리드" 코스 — 화면 뒤에 숨어 있는 보이지 않는 뼈대(그리드)에서
|
||||
// 시작해, 정렬·근접성·시각적 흐름·여백·카드 레이아웃·반응형까지 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (8pt 그리드 → 정렬 → 근접성 → Z/F 패턴 → 여백 → 카드 레이아웃 → 반응형 → 피그마 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_8PT = `8pt 그리드 — 모든 간격을 8의 배수로
|
||||
|
||||
허용되는 간격: 8 16 24 32 40 48 64 ...
|
||||
(작은 디테일엔 4도 허용 — '4의 배수'라고 외워도 좋아요)
|
||||
|
||||
나쁜 예 좋은 예
|
||||
────────────── ──────────────
|
||||
제목 아래 여백 13px 제목 아래 여백 16px
|
||||
카드 안쪽 여백 18px 카드 안쪽 여백 16px 또는 24px
|
||||
버튼 높이 37px 버튼 높이 40px
|
||||
|
||||
→ 13, 18, 37처럼 '어중간한 숫자'가 사라지면
|
||||
화면 전체가 같은 박자로 뛰기 시작합니다.`;
|
||||
|
||||
const CODE_ALIGN = `정렬 전 vs 정렬 후 — 왼쪽 선 하나만 맞춰도
|
||||
|
||||
정렬 전 (기준선 없음) 정렬 후 (왼쪽 기준선 하나)
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ 제목이 여기 │ │ 제목이 여기 │
|
||||
│ 본문은 여기서 시작 │ │ 본문은 여기서 시작 │
|
||||
│ 버튼 │ │ 버튼 │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
↑ 시작점이 제각각 ↑ 눈이 세로선 하나를 타고
|
||||
눈이 지그재그로 헤맴 미끄러지듯 내려감
|
||||
|
||||
핵심: 가운데 정렬을 남발하면 기준선이 사라져요.
|
||||
긴 글은 왼쪽 정렬이 기본, 가운데 정렬은 짧은 제목용.`;
|
||||
|
||||
const CODE_PROXIMITY = `근접성 — 붙어 있으면 한 팀, 떨어져 있으면 남남
|
||||
|
||||
나쁜 예 (간격이 전부 16px) 좋은 예 (팀 안 8px / 팀 사이 32px)
|
||||
|
||||
이름 이름
|
||||
홍길동 ← 라벨과 값이 한 팀
|
||||
홍길동
|
||||
이메일
|
||||
이메일 hong@... ← 다음 팀과는 멀리
|
||||
|
||||
hong@...
|
||||
|
||||
→ 선이나 박스를 긋지 않아도, 간격 차이만으로
|
||||
"이 라벨은 이 값의 것"이라는 관계가 보입니다.`;
|
||||
|
||||
const CODE_FLOW = `시선의 이동 경로 — Z 패턴과 F 패턴
|
||||
|
||||
Z 패턴 (그림·여백 많은 화면) F 패턴 (글 많은 화면)
|
||||
로고 ─────────→ 메뉴 제목 ──────────→
|
||||
╲ 본문 첫 줄 ────→
|
||||
╲ (대각선으로 훑고) 본문 둘째 줄 ──→
|
||||
╲ 나머지는
|
||||
문구 ─────────→ [버튼] │ 왼쪽만
|
||||
│ 훑으며
|
||||
↓ 내려감
|
||||
|
||||
활용법:
|
||||
Z의 끝(오른쪽 아래) = 가장 중요한 버튼 자리
|
||||
F의 왼쪽 세로선 = 제목·핵심 단어를 앞에 배치`;
|
||||
|
||||
const CODE_WHITESPACE = `여백 = 아무것도 없는 게 아니라 '숨 쉴 자리'
|
||||
|
||||
빽빽한 화면 여백 있는 화면
|
||||
┌────────────┐ ┌────────────┐
|
||||
│글글글글글글글│ │ │
|
||||
│글글글글글글글│ │ 핵심 문장 │
|
||||
│글글글글글글글│ │ │
|
||||
│글글글글글글글│ │ [버튼] │
|
||||
└────────────┘ │ │
|
||||
전부 소리치니까 └────────────┘
|
||||
아무것도 안 들림 하나만 말하니까 잘 들림
|
||||
|
||||
명품 매장이 물건을 띄엄띄엄 놓는 이유와 같아요 —
|
||||
여백이 넓을수록 그 안의 콘텐츠가 귀해 보입니다.`;
|
||||
|
||||
const CODE_CARD = `우리 학습 센터 허브가 카드 레이아웃인 이유
|
||||
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 네트워크 │ │ 디자인 │ │ 코딩 기초 │
|
||||
│ 코스 설명 │ │ 코스 설명 │ │ 코스 설명 │
|
||||
│ [시작하기] │ │ [시작하기] │ │ [시작하기] │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
|
||||
카드 하나 = 정보 한 묶음(근접성) + 같은 틀 반복(정렬)
|
||||
1) 경계가 분명해서 어디까지가 한 덩어리인지 고민할 필요 없음
|
||||
2) 전부 같은 규격 → 그리드에 착착 올라감
|
||||
3) 화면이 좁아지면 카드 단위로 접으면 됨 (다음 섹션!)
|
||||
|
||||
React와도 찰떡: 카드 = 컴포넌트 하나.
|
||||
배열.map()으로 찍어내면 카드 100개도 코드는 그대로.`;
|
||||
|
||||
const CODE_RESPONSIVE = `반응형 — 그리드가 '접히는' 방식
|
||||
|
||||
데스크톱 (12칸 그리드) 태블릿 모바일
|
||||
┌───┐┌───┐┌───┐ ┌────┐┌────┐ ┌────────┐
|
||||
│ A ││ B ││ C │ │ A ││ B │ │ A │
|
||||
└───┘└───┘└───┘ └────┘└────┘ └────────┘
|
||||
4칸 4칸 4칸 ┌────┐ ┌────────┐
|
||||
│ C │ │ B │
|
||||
└────┘ └────────┘
|
||||
6칸씩 ┌────────┐
|
||||
│ C │
|
||||
└────────┘
|
||||
12칸 통째로
|
||||
|
||||
칸(컬럼) 수는 그대로 12개 — 카드가 차지하는 칸 수만
|
||||
4칸 → 6칸 → 12칸으로 바뀌는 거예요.
|
||||
CSS 한 줄이면 브라우저가 알아서 접어 줍니다:
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));`;
|
||||
|
||||
const CODE_FIGMA = `실습 — 피그마에서 8pt 그리드로 코스 카드 3장 배치하기
|
||||
|
||||
준비: 피그마에서 새 파일 → Frame(단축키 F) → Desktop(1440) 선택
|
||||
|
||||
1) 그리드 켜기
|
||||
오른쪽 패널 Layout grid → + 클릭 → Grid → 8px로 설정
|
||||
(화면에 8px 모눈종이가 깔립니다)
|
||||
|
||||
2) 카드 한 장 만들기
|
||||
사각형(R) 320 x 200 → 모서리 둥글게 8
|
||||
안에 제목 텍스트(T), 설명 텍스트, 버튼 사각형(높이 40)
|
||||
카드 안쪽 여백은 사방 24 — 8의 배수인지 확인!
|
||||
|
||||
3) 팀 만들기 (근접성)
|
||||
제목↔설명 간격 8, 설명↔버튼 간격 24
|
||||
→ 제목과 설명은 한 팀, 버튼은 다음 팀으로 보이나요?
|
||||
|
||||
4) 카드 복제해서 3장 배치 (정렬)
|
||||
Alt+드래그로 복제, 카드 사이 간격 24
|
||||
세 카드의 윗변이 한 줄에 딱 맞는지 확인
|
||||
|
||||
5) 셀프 체크
|
||||
□ 화면의 모든 간격이 8의 배수인가?
|
||||
□ 세 카드의 제목이 같은 높이에서 시작하는가?
|
||||
□ 그리드를 꺼도(Shift+G) 정돈돼 보이는가?
|
||||
|
||||
완성본은 멘토에게 링크로 공유 — 간격 숫자를 물어볼 거예요!`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '8pt 그리드' },
|
||||
{ n: 2, label: '정렬의 힘' },
|
||||
{ n: 3, label: '근접성' },
|
||||
{ n: 4, label: 'Z/F 패턴' },
|
||||
{ n: 5, label: '여백' },
|
||||
{ n: 6, label: '카드 레이아웃' },
|
||||
{ n: 7, label: '반응형' },
|
||||
{ n: 8, label: '피그마 실습' },
|
||||
];
|
||||
|
||||
export default function LayoutPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>레이아웃과 그리드<br />— 보이지 않는 뼈대가 화면을 세운다</h1>
|
||||
<p>
|
||||
잘 만든 화면은 그림 실력이 아니라 <strong>배치의 규칙</strong>에서 나와요.
|
||||
이 코스에서는 화면 뒤에 숨어 있는 그리드라는 뼈대를 배우고, 마지막엔 피그마에서
|
||||
직접 손으로 카드를 배치해 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 피그마 과제</span>
|
||||
<span className="chip">준비물: 눈과 피그마</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="그리드 — 보이지 않는 뼈대" sub="왜 프로의 화면은 '괜히' 정돈돼 보일까?">
|
||||
<p>
|
||||
공책에 글씨를 쓸 때와 백지에 쓸 때를 비교해 보세요. 같은 글씨체라도
|
||||
<strong> 줄이 그어진 공책</strong> 쪽이 훨씬 단정해 보이죠. 그리드는 화면 위에
|
||||
깔아 두는 <strong>보이지 않는 모눈종이</strong>예요. 완성된 화면에서 격자는 안 보이지만,
|
||||
모든 요소가 그 격자 위에 올라앉아 있기 때문에 "이유는 모르겠는데 정돈돼 보인다"는
|
||||
느낌이 생깁니다.
|
||||
</p>
|
||||
<p>
|
||||
실무에서 가장 널리 쓰는 규칙이 <strong>8pt(8px) 그리드</strong>입니다.
|
||||
규칙은 딱 하나 — <strong>모든 간격·크기를 8의 배수로만</strong> 쓰는 거예요.
|
||||
</p>
|
||||
<Code>{CODE_8PT}</Code>
|
||||
<p>
|
||||
왜 하필 8일까요? 8은 2·4로도 나뉘고, 대부분의 화면 해상도와도 딱 떨어져서
|
||||
<strong> 어디서든 반 토막·배수 계산이 쉬운 숫자</strong>거든요. 그리고 더 현실적인 이유 —
|
||||
"여기 간격 몇으로 할까?"를 매번 고민하면 지쳐요. 선택지를 8, 16, 24, 32로
|
||||
좁혀 두면 <strong>고민이 사라지고 일관성은 저절로</strong> 생깁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 이 학습 센터 페이지에서 <span className="kbd">F12</span>를
|
||||
눌러 개발자 도구를 열고, 왼쪽 위 <strong>요소 선택 아이콘</strong>(화살표)을 클릭한 뒤
|
||||
이 카드 위에 마우스를 올려 보세요. <span className="icode">padding</span>과{' '}
|
||||
<span className="icode">margin</span> 값이 색깔로 표시되는데 — 대부분 8의 배수일 거예요.
|
||||
우리 플랫폼도 이 규칙 위에 서 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="정렬의 힘" sub="한 줄만 맞춰도 화면이 정돈된다">
|
||||
<p>
|
||||
디자인을 배우기 전과 후의 가장 큰 차이는 그림 실력이 아니라 <strong>정렬 습관</strong>이에요.
|
||||
요소들의 시작점이 제각각이면, 보는 사람의 눈은 다음 줄을 읽을 때마다
|
||||
시작점을 새로 찾아야 합니다. 왼쪽 기준선 하나만 맞춰도 눈은 그 <strong>세로선을
|
||||
엘리베이터처럼</strong> 타고 내려가요.
|
||||
</p>
|
||||
<Code>{CODE_ALIGN}</Code>
|
||||
<p>
|
||||
초보 화면에서 가장 흔한 실수가 <strong>가운데 정렬 남발</strong>입니다.
|
||||
가운데 정렬은 줄마다 시작점이 달라져서 기준선이 사라져요. 축하 카드 같은
|
||||
짧은 문구엔 어울리지만, 설명이 서너 줄만 넘어가면 왼쪽 정렬이 답입니다.
|
||||
"모든 요소는 <strong>다른 무언가와 선이 맞아야 한다</strong>" — 어디에도 안 맞는
|
||||
요소가 하나라도 있으면 그게 화면을 흐트러뜨리는 범인이에요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="근접성 — 간격으로 그룹 만들기" sub="붙어 있으면 한 팀, 떨어져 있으면 남남">
|
||||
<p>
|
||||
운동장에 학생들이 서 있는 모습을 멀리서 보면, 누가 어느 모둠인지
|
||||
<strong> 서 있는 거리만으로</strong> 알 수 있죠. 화면도 똑같아요. 관련 있는 것끼리는
|
||||
가깝게, 관련 없는 것끼리는 멀게 — 이게 <strong>근접성</strong>의 원리입니다.
|
||||
</p>
|
||||
<Code>{CODE_PROXIMITY}</Code>
|
||||
<p>
|
||||
포인트는 <strong>간격의 차이</strong>예요. 팀 안의 간격(8)보다 팀 사이의 간격(32)이
|
||||
확실히 넓어야 그룹이 보입니다. 모든 간격이 16으로 똑같으면 눈은 어디서 끊어 읽어야
|
||||
할지 몰라요. 선이나 박스를 긋기 전에 먼저 간격으로 그룹을 만들어 보세요 —
|
||||
<strong> 여백만으로 충분한데 선까지 그으면</strong> 화면이 창살처럼 답답해집니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 이 페이지의 섹션 카드를 보세요. 카드 <strong>안</strong>의
|
||||
문단 사이 간격과 카드와 카드 <strong>사이</strong> 간격 중 어느 쪽이 넓나요?
|
||||
그 간격 차이 덕분에 "여기까지가 섹션 3"이라는 걸 선 없이도 알 수 있는 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="시각적 흐름 — Z 패턴과 F 패턴" sub="사람의 눈은 정해진 길로 다닌다">
|
||||
<p>
|
||||
사람의 시선은 화면 위를 아무렇게나 떠돌지 않아요. 수십 년의 시선 추적 연구가
|
||||
찾아낸 <strong>단골 경로</strong>가 두 가지 있습니다. 그림과 여백이 많은 화면에선
|
||||
알파벳 <strong>Z</strong> 모양으로, 글이 빽빽한 화면에선 <strong>F</strong> 모양으로
|
||||
훑어요. (우리가 왼쪽→오른쪽, 위→아래로 읽는 습관 그대로입니다.)
|
||||
</p>
|
||||
<Code>{CODE_FLOW}</Code>
|
||||
<p>
|
||||
이걸 알면 배치가 <strong>전략</strong>이 됩니다. 랜딩 페이지의 "시작하기" 버튼이
|
||||
대부분 오른쪽 아래에 있는 건 우연이 아니라 <strong>Z의 종착역</strong>이기 때문이에요.
|
||||
긴 글에선 사람들이 왼쪽 세로선만 훑고 지나가니, 문단 첫머리에 핵심 단어를 놓아야
|
||||
하고요. 시선이 다니는 길목에 중요한 것을 놓는 것 — 목 좋은 자리에 가게를 여는 것과
|
||||
같은 이치죠.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="여백은 콘텐츠다" sub="비어 있는 게 아니라 일하고 있는 것">
|
||||
<p>
|
||||
초보 때 가장 참기 어려운 유혹이 <strong>"빈 곳을 채우고 싶다"</strong>예요.
|
||||
하지만 여백은 낭비되는 공간이 아니라, 콘텐츠가 <strong>숨 쉬고 돋보이게 하는
|
||||
장치</strong>입니다. 말하다가 잠깐 멈추는 침묵이 그다음 말에 힘을 실어 주듯이요.
|
||||
</p>
|
||||
<Code>{CODE_WHITESPACE}</Code>
|
||||
<p>
|
||||
여백에도 규칙이 있어요. <strong>바깥 여백은 안쪽 여백보다 크거나 같게</strong> —
|
||||
카드 안쪽 여백이 24라면 카드 사이는 24 이상. 이 규칙이 깨지면 요소가 자기 그룹보다
|
||||
남의 그룹에 더 가까워져서(섹션 3의 근접성!) 화면이 이상하게 뒤엉켜 보입니다.
|
||||
결국 그리드(1) · 근접성(3) · 여백(5)은 전부 <strong>간격을 다루는 한 가족</strong>이에요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>"허전한데요?"라는 말에 흔들리지 마세요</b> — 허전함과 여유는 다릅니다.
|
||||
기준선이 맞고 그룹이 분명하면 여백이 넓어도 '여유'로 보이고,
|
||||
정렬이 무너진 채 여백만 넓으면 그때가 진짜 '허전함'이에요.
|
||||
채우기 전에 먼저 정렬부터 점검!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="카드 레이아웃은 왜 유행일까" sub="우리 학습 센터 허브가 바로 그 실례">
|
||||
<p>
|
||||
학습 센터 첫 화면을 떠올려 보세요. 코스들이 <strong>같은 크기의 카드</strong>로
|
||||
나란히 놓여 있죠. 유튜브의 영상 목록도, 쇼핑몰의 상품 목록도 다 카드입니다.
|
||||
이 유행에는 분명한 이유가 있어요 — 카드는 지금까지 배운 원리들의
|
||||
<strong> 종합 선물 세트</strong>거든요.
|
||||
</p>
|
||||
<Code>{CODE_CARD}</Code>
|
||||
<p>
|
||||
개발자 입장에서도 카드는 효자예요. 카드 한 장이 곧 React <strong>컴포넌트 하나</strong>라서,
|
||||
코스가 10개로 늘어나도 데이터 배열에 항목만 추가하면 화면이 알아서 자랍니다.
|
||||
디자인 원리(근접성·정렬)와 코드 구조(컴포넌트 재사용)가 카드라는 한 지점에서
|
||||
만나는 셈이죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 학습 센터 허브로 돌아가서 코스 카드들을 관찰해 보세요.
|
||||
카드 사이 간격은 일정한가요? 카드 안에서 제목·설명·버튼의 위치는 카드마다 같은가요?
|
||||
그리고 <span className="kbd">F12</span> 개발자 도구로 카드 하나를 선택해 —
|
||||
같은 클래스 이름이 카드마다 반복되는 것도 확인해 보세요. 그게 컴포넌트 재사용의 흔적입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="반응형 — 그리드가 접히는 방식" sub="같은 화면이 폰에서도 예쁜 비밀">
|
||||
<p>
|
||||
같은 웹사이트가 모니터에서도, 폰에서도 자연스러운 건 화면을 두 벌 만들어서가
|
||||
아니에요. <strong>그리드가 접히기</strong> 때문입니다. 데스크톱 화면을 보통
|
||||
<strong> 12개의 세로 칸(컬럼)</strong>으로 나누는데, 화면이 좁아지면 각 요소가
|
||||
차지하는 칸 수를 늘려서 자연스럽게 아래로 쌓이게 해요.
|
||||
</p>
|
||||
<Code>{CODE_RESPONSIVE}</Code>
|
||||
<p>
|
||||
접는 기준이 되는 화면 폭을 <strong>브레이크포인트</strong>라고 불러요.
|
||||
식탁 접이식 상다리처럼, 공간이 좁아지면 정해진 관절에서 착착 접히는 거죠.
|
||||
섹션 6의 카드 레이아웃이 반응형과 찰떡인 이유가 여기 있습니다 —
|
||||
카드는 규격이 같아서 <strong>3장 → 2장 → 1장</strong>으로 줄 서는 방식만 바꾸면 되니까요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 브라우저 창의 오른쪽 끝을 잡고 천천히 좁혀 보세요.
|
||||
어느 순간 레이아웃이 <strong>툭</strong> 하고 접히는 지점이 브레이크포인트예요.
|
||||
<span className="kbd">F12</span> → 기기 아이콘(<span className="kbd">Ctrl</span>+
|
||||
<span className="kbd">Shift</span>+<span className="kbd">M</span>)을 누르면
|
||||
폰 화면 크기로 바로 전환해 볼 수도 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="실습 — 피그마에서 8pt 그리드로 카드 배치" sub="눈으로 본 것을 손으로 굳히기">
|
||||
<p>
|
||||
이제 배운 걸 전부 꺼내 쓸 시간이에요. 피그마에서 <strong>8pt 그리드를 깔고,
|
||||
코스 카드 3장을 배치</strong>합니다. 그리드(1)·정렬(2)·근접성(3)·여백(5)·카드(6)가
|
||||
한 화면에 다 들어가는 실습이에요.
|
||||
</p>
|
||||
<Code>{CODE_FIGMA}</Code>
|
||||
<p>
|
||||
다 만들었다면 마지막으로 <strong>그리드를 끄고</strong>(Shift+G) 화면을 보세요.
|
||||
모눈종이가 사라져도 정돈돼 보인다면 성공 — 뼈대는 안 보여도 자세를 잡아 준다는 걸
|
||||
직접 확인한 겁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>한 걸음 더</b> 여유가 있다면 만든 카드를 <strong>일부러 망가뜨려</strong> 보세요.
|
||||
간격 하나를 13px로, 카드 하나만 3px 아래로 내려 보는 거예요. "어딘가 어색한데?"가
|
||||
느껴진다면 — 축하해요, 이제 그리드가 <strong>보이는 눈</strong>이 생긴 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📐 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 화면을 볼 때 콘텐츠만이 아니라 그 뒤의 <strong>뼈대</strong>가 보일 거예요 —
|
||||
8의 배수로 뛰는 간격, 한 줄로 맞은 기준선, 간격이 만든 그룹까지. 다음은 그 뼈대 위에
|
||||
입힐 <strong>살</strong>을 배울 차례 — 색과 글꼴을 다루는 디자인 코스로 이어 가거나,{' '}
|
||||
<Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스에서 오늘 배운 레이아웃을
|
||||
직접 코드로 옮겨 보세요. 피그마 실습 결과물은 잊지 말고 멘토에게 공유!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
434
frontend/src/pages/courses/LinuxAdvancedPage.jsx
Normal file
434
frontend/src/pages/courses/LinuxAdvancedPage.jsx
Normal file
@ -0,0 +1,434 @@
|
||||
// 이 파일이 하는 일: "리눅스 심화" 코스 — 환경변수와 PATH에서 출발해
|
||||
// 셸 스크립트, 크론, 파이프, systemctl, 로그 관습, 그리고 우리 서버의
|
||||
// 실제 운영 명령까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (환경변수·PATH → 셸 스크립트 → 크론 → 파이프·리다이렉션 → 프로세스 관리 → /var/log → 운영 명령 모음)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 셸 예제·텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 셸 예제 · 텍스트 다이어그램 상수들 ──
|
||||
|
||||
const CODE_ENV_BASIC = `# 환경변수 = 셸이라는 '작업실'에 붙여 둔 포스트잇
|
||||
|
||||
# 지금 붙어 있는 포스트잇 전부 보기
|
||||
printenv # (또는 env)
|
||||
|
||||
# 하나만 골라 보기 — $를 붙이면 "이 이름의 값으로 바꿔 줘"
|
||||
echo $HOME # 내 홈 디렉터리 예) /home/ubuntu
|
||||
echo $USER # 지금 로그인한 사용자
|
||||
echo $SHELL # 지금 쓰는 셸 예) /bin/bash
|
||||
|
||||
# 새 포스트잇 붙이기 (export가 있어야 자식 프로세스에도 전달돼요)
|
||||
export DB_PASSWORD=secret123
|
||||
echo $DB_PASSWORD
|
||||
|
||||
# 단, 터미널을 닫으면 사라짐! 계속 쓰려면 ~/.bashrc에 적어 둡니다.`;
|
||||
|
||||
const CODE_PATH = `# PATH = "명령어를 찾을 때 뒤져 볼 폴더 목록" (콜론 : 으로 구분)
|
||||
|
||||
echo $PATH
|
||||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:...
|
||||
|
||||
# docker 라고 치면 셸이 하는 일:
|
||||
# 1) /usr/local/bin/docker 있나? → 없네
|
||||
# 2) /usr/bin/docker 있나? → 있다! 실행!
|
||||
# 왼쪽 폴더부터 순서대로 찾고, 처음 발견한 것을 실행해요.
|
||||
|
||||
# 어떤 명령이 실제로 어디 있는지 확인
|
||||
which docker # /usr/bin/docker
|
||||
which java # /usr/bin/java
|
||||
|
||||
# "command not found"의 정체:
|
||||
# 프로그램이 없거나, 있어도 PATH에 등록된 폴더 밖에 있다는 뜻!
|
||||
# 그래서 설치 안내문 끝에 "PATH에 추가하세요"가 자주 나오는 거예요.`;
|
||||
|
||||
const CODE_SH_FIRST = `#!/bin/bash
|
||||
# ↑ 셔뱅(shebang): "이 파일은 bash로 실행해 주세요"라는 첫 줄 선언
|
||||
|
||||
# hello.sh — 나의 첫 셸 스크립트
|
||||
NAME="어썸데브" # 변수 선언 (= 양옆에 공백 금지!)
|
||||
echo "안녕하세요, $NAME 수습 여러분!"
|
||||
echo "오늘 날짜: $(date +%Y-%m-%d)" # $( ) = 명령 실행 결과를 값으로 사용
|
||||
|
||||
# 실행하는 법:
|
||||
# chmod +x hello.sh ← 실행 권한 주기 (딱 한 번)
|
||||
# ./hello.sh ← 실행!`;
|
||||
|
||||
const CODE_SH_IF_FOR = `#!/bin/bash
|
||||
# if — "조건이 맞으면 이 길, 아니면 저 길"
|
||||
|
||||
if [ -f .env ]; then # -f = 파일이 존재하면
|
||||
echo ".env 있음 — 배포 진행!"
|
||||
else
|
||||
echo ".env 없음 — 먼저 만들어 주세요"
|
||||
exit 1 # 0이 아닌 종료 코드 = 실패 신호
|
||||
fi
|
||||
|
||||
# for — "목록의 항목마다 반복"
|
||||
for svc in frontend backend db; do
|
||||
echo "[$svc] 상태 점검 중..."
|
||||
done
|
||||
|
||||
# 실제 배포 스크립트(deploy.sh)의 뼈대가 딱 이 모양이에요:
|
||||
# "필요한 파일 있는지 확인(if) → 서비스마다 순서대로 처리(for)"
|
||||
# 스크립트 = 손으로 치던 명령들을 파일에 적어 둔 자동 조리법입니다.`;
|
||||
|
||||
const CODE_CRON = `# 크론(cron) = 리눅스에 내장된 알람시계 + 자동 실행 비서
|
||||
# "매일 새벽 4시에 이 명령 실행해 줘"를 등록해 두는 곳
|
||||
|
||||
crontab -l # 내 예약 목록 보기
|
||||
crontab -e # 예약 편집하기
|
||||
|
||||
# 예약 한 줄의 문법 — 별 5개 + 명령
|
||||
# ┌─ 분 (0-59)
|
||||
# │ ┌─ 시 (0-23)
|
||||
# │ │ ┌─ 일 (1-31)
|
||||
# │ │ │ ┌─ 월 (1-12)
|
||||
# │ │ │ │ ┌─ 요일 (0-7, 0과 7 = 일요일)
|
||||
# │ │ │ │ │
|
||||
# 0 4 * * * /home/ubuntu/backup.sh >> /var/log/backup.log 2>&1
|
||||
# → "매일(일·월·요일은 * = 아무 때나) 새벽 4시 0분에 백업 스크립트 실행,
|
||||
# 출력은 로그 파일에 이어 붙이기"
|
||||
|
||||
# 자주 쓰는 패턴
|
||||
# */10 * * * * → 10분마다
|
||||
# 0 * * * * → 매시 정각
|
||||
# 30 2 * * 1 → 매주 월요일 새벽 2시 30분
|
||||
|
||||
# 우리 서버의 DB 백업도 이렇게 돌아가요 — 사람이 새벽에 안 일어나도
|
||||
# pg_dump가 알아서 실행되고, 결과는 로그로 남습니다.`;
|
||||
|
||||
const CODE_PIPE = `# 파이프( | ) = 왼쪽 명령의 출력을 오른쪽 명령의 입력으로 흘려보내는 배관
|
||||
|
||||
# "프로세스 목록에서 java가 들어간 줄만 보여 줘"
|
||||
ps aux | grep java
|
||||
|
||||
# "로그에서 ERROR 줄만 골라 → 최근 20줄만"
|
||||
cat app.log | grep ERROR | tail -20
|
||||
|
||||
# 리다이렉션 — 출력의 방향을 파일로 꺾기
|
||||
echo "안녕" > memo.txt # > : 파일에 저장 (기존 내용 덮어씀! 주의)
|
||||
echo "추가" >> memo.txt # >> : 파일 끝에 이어 붙임 (안전)
|
||||
|
||||
# 조합 연습 — 작은 부품을 이어 큰 도구 만들기
|
||||
history | grep docker # 내가 쳤던 docker 명령 다시 찾기
|
||||
ls -l | wc -l # 현재 폴더의 항목 수 세기
|
||||
grep ERROR app.log | wc -l # 에러가 총 몇 번 났는지 세기
|
||||
ps aux | grep java > java_ps.txt # 조사 결과를 파일로 보관
|
||||
|
||||
# 리눅스 철학: 한 가지만 잘하는 작은 도구들을
|
||||
# 파이프로 이어 붙여 무엇이든 만든다 — 레고 블록과 같아요.`;
|
||||
|
||||
const CODE_SYSTEMCTL = `# systemd = 리눅스 서버의 '매니저'. 부팅하면 제일 먼저 일어나서
|
||||
# 등록된 서비스들을 깨우고, 죽으면 다시 살리고, 상태를 관리해요.
|
||||
# systemctl = 그 매니저에게 말을 거는 리모컨.
|
||||
|
||||
sudo systemctl status caddy # 상태 보기 (active (running)이면 정상)
|
||||
sudo systemctl restart caddy # 재시작 — "껐다 켜 보세요"의 서버 버전
|
||||
sudo systemctl stop caddy # 정지
|
||||
sudo systemctl start caddy # 시작
|
||||
sudo systemctl enable caddy # 부팅 시 자동 시작 등록 ★중요★
|
||||
|
||||
# enable을 빼먹으면? 서버 재부팅(정전, EC2 재시작...) 후
|
||||
# 서비스가 안 살아나서 "사이트가 안 열려요" 사태가 납니다.
|
||||
|
||||
# 직접 실행(./run.sh)과 서비스 등록의 차이:
|
||||
# 직접 실행 = 알바생이 손으로 켬 → 터미널 끊기면 같이 죽을 수 있음
|
||||
# systemd 등록 = 매니저가 관리 → 죽으면 재기동, 부팅하면 자동 시작
|
||||
# 참고: 우리 백엔드처럼 Docker로 띄우는 것들은 Docker가
|
||||
# restart 정책으로 같은 역할을 해 줘요. 매니저가 둘인 셈!`;
|
||||
|
||||
const CODE_VARLOG = `# /var/log = 서버의 블랙박스. 무슨 일이 있었는지 전부 여기 남아요.
|
||||
|
||||
/var/log/
|
||||
├── syslog (또는 messages) # 시스템 전반의 일기장
|
||||
├── auth.log # 로그인 시도 기록 — 침입 흔적도 여기!
|
||||
├── kern.log # 커널(OS 심장부) 메시지
|
||||
└── ... # 프로그램마다 자기 폴더/파일을 만들기도
|
||||
|
||||
# 로그 읽기 3종 세트
|
||||
tail -n 50 /var/log/syslog # 마지막 50줄
|
||||
tail -f /var/log/syslog # 실시간 감시 (f=follow, Ctrl+C로 종료)
|
||||
grep "Failed" /var/log/auth.log # 실패한 로그인 시도 찾기
|
||||
|
||||
# systemd가 관리하는 서비스의 로그는 journalctl로
|
||||
journalctl -u caddy --since "1 hour ago"
|
||||
|
||||
# Docker 컨테이너의 로그는 docker가 따로 모아 줘요
|
||||
docker logs -f backend
|
||||
|
||||
# 로그가 무한히 쌓이면 디스크가 꽉 차므로, logrotate라는 도구가
|
||||
# 오래된 로그를 압축·삭제해 줍니다. (일기장을 연도별로 묶어 창고에 넣는 셈)`;
|
||||
|
||||
const CODE_OPS = `# AWESOMEDEV 서울 EC2 — 실전 운영 명령 모음
|
||||
# (멘토 옆에서 실제로 보게 될 명령들이에요. 순서대로 읽어 보세요.)
|
||||
|
||||
# 1) 서버 접속 — 열쇠(키 파일)로 문 열기
|
||||
ssh -i awesomedev-key.pem ubuntu@<서버IP>
|
||||
|
||||
# 2) 서버 건강검진
|
||||
df -h # 디스크 남은 용량 (Use% 90↑면 위험!)
|
||||
free -h # 메모리 사용량
|
||||
docker ps # 컨테이너 목록 — STATUS가 Up이면 정상
|
||||
|
||||
# 3) 서비스 로그 확인 (문제 신고가 들어왔을 때 1순위)
|
||||
docker logs --tail 100 backend # 백엔드 최근 100줄
|
||||
docker logs -f backend | grep ERROR # 에러만 실시간 감시
|
||||
|
||||
# 4) 배포 — Gitea에서 최신 코드 받아 다시 띄우기
|
||||
cd ~/mirim-app
|
||||
git pull # edu.awesomedevapp.com 의 Gitea에서 받기
|
||||
docker compose up -d --build # 바뀐 것만 다시 빌드해서 교체
|
||||
|
||||
# 5) HTTPS 현관(Caddy) 상태
|
||||
sudo systemctl status caddy
|
||||
journalctl -u caddy --since "10 min ago"
|
||||
|
||||
# 6) DB 백업이 잘 돌았는지 확인 (섹션 3의 크론!)
|
||||
crontab -l # 예약 목록에 backup.sh가 있는지
|
||||
tail /var/log/backup.log # 오늘 새벽 백업 로그 확인
|
||||
|
||||
# 이 여섯 묶음이 "서버 좀 봐 주세요"를 받았을 때의 기본 동선입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '환경변수와 PATH' },
|
||||
{ n: 2, label: '셸 스크립트 첫걸음' },
|
||||
{ n: 3, label: '크론 예약 작업' },
|
||||
{ n: 4, label: '파이프와 리다이렉션' },
|
||||
{ n: 5, label: '프로세스 관리 심화' },
|
||||
{ n: 6, label: '로그는 /var/log에' },
|
||||
{ n: 7, label: '실전 운영 명령 모음' },
|
||||
];
|
||||
|
||||
export default function LinuxAdvancedPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>리눅스 심화<br />— 명령어를 넘어, 서버를 운영하는 감각으로</h1>
|
||||
<p>
|
||||
리눅스 기초에서 폴더를 오가고 파일을 다뤘다면, 이제는 서버가 <strong>스스로
|
||||
일하게</strong> 만들 차례예요. 환경변수와 스크립트, 예약 작업, 로그 읽는 법을 거쳐
|
||||
우리 EC2 서버에서 실제로 쓰는 운영 명령까지 — <strong>"직접 확인해 보기"</strong>는
|
||||
꼭 손으로 해보세요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 90분</span>
|
||||
<span className="chip">실습 3개</span>
|
||||
<span className="chip">선수 코스: 리눅스 기초</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="환경변수와 PATH" sub="셸의 포스트잇, 그리고 명령어를 찾는 지도">
|
||||
<p>
|
||||
<strong>환경변수</strong>는 셸이라는 작업실 벽에 붙여 둔 <strong>포스트잇</strong>이에요.
|
||||
"내 홈은 여기", "DB 비밀번호는 이것" 같은 정보를 이름표를 붙여 적어 두면,
|
||||
그 작업실에서 실행되는 프로그램들이 전부 읽어 갈 수 있죠. 우리 백엔드(Spring Boot)가
|
||||
DB 비밀번호를 코드에 박아 두지 않고 환경변수로 받는 이유가 바로 이것 —
|
||||
<strong>코드는 공개돼도 포스트잇은 서버에만</strong> 붙어 있으니까요.
|
||||
</p>
|
||||
<Code>{CODE_ENV_BASIC}</Code>
|
||||
<p>
|
||||
그중 가장 유명한 환경변수가 <span className="icode">PATH</span>입니다.
|
||||
터미널에 <span className="icode">docker</span>라고 치면 셸은 "docker라는 프로그램이
|
||||
어디 있지?" 하고 <strong>PATH에 적힌 폴더들을 왼쪽부터 순서대로</strong> 뒤져요.
|
||||
지도에 표시된 서랍만 열어 보는 셈이죠.
|
||||
</p>
|
||||
<Code>{CODE_PATH}</Code>
|
||||
<div className="warn">
|
||||
<b>흔한 함정</b> — 분명 설치했는데 <span className="icode">command not found</span>가
|
||||
뜬다면, 프로그램이 PATH에 없는 폴더에 설치된 거예요. 프로그램이 없는 게 아니라
|
||||
<strong> 지도에 그 서랍이 안 그려져 있는 것</strong>. 침착하게{' '}
|
||||
<span className="icode">which</span>와 <span className="icode">echo $PATH</span>부터 확인!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="셸 스크립트 첫걸음" sub="손으로 치던 명령을 자동 조리법으로">
|
||||
<p>
|
||||
매번 같은 명령 5개를 순서대로 치고 있다면, 그건 <strong>스크립트로 만들라는
|
||||
신호</strong>예요. 셸 스크립트(<span className="icode">.sh</span>)는 터미널에 칠 명령들을
|
||||
파일에 순서대로 적어 둔 <strong>조리법(레시피)</strong>입니다. 요리사(셸)에게 건네면
|
||||
위에서부터 한 줄씩 그대로 실행하죠.
|
||||
</p>
|
||||
<Code>{CODE_SH_FIRST}</Code>
|
||||
<p>
|
||||
조리법에도 "간을 보고 싱거우면 소금 추가" 같은 판단이 필요하죠.
|
||||
그게 <span className="icode">if</span>(조건)와 <span className="icode">for</span>(반복)예요.
|
||||
이 두 개만 알면 실무 배포 스크립트의 8할을 읽을 수 있습니다.
|
||||
</p>
|
||||
<Code>{CODE_SH_IF_FOR}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 위의 <span className="icode">hello.sh</span>를 그대로 만들어
|
||||
실행해 보세요. Windows라면 <strong>Git Bash</strong>에서, 서버 실습 계정이 있다면 EC2에서요.
|
||||
성공했다면 <span className="icode">NAME</span>을 내 이름으로 바꾸고,{' '}
|
||||
<span className="icode">for</span>문으로 "1일차 화이팅~5일차 화이팅"을 출력하도록
|
||||
고쳐 보세요. 그다음 멘토에게 우리 프로젝트의 <span className="icode">deploy.sh</span>를
|
||||
보여 달라고 해서 — <strong>어디가 if고 어디가 for인지</strong> 손가락으로 짚어 보기!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="크론으로 예약 작업" sub="새벽 4시의 백업, 사람 대신 알람시계가">
|
||||
<p>
|
||||
"매일 새벽에 DB를 백업해야 해요"라는 일을 사람이 하면 어떻게 될까요? 하루 이틀은
|
||||
하겠지만 언젠가 반드시 까먹어요. 그래서 리눅스에는 <strong>크론(cron)</strong>이라는
|
||||
알람시계 겸 비서가 있습니다. "이 시각이 되면 이 명령을 실행해"라고 등록해 두면,
|
||||
서버가 켜져 있는 한 <strong>영원히, 정확하게</strong> 반복하죠.
|
||||
</p>
|
||||
<Code>{CODE_CRON}</Code>
|
||||
<p>
|
||||
별 5개 문법이 처음엔 암호 같지만, <strong>왼쪽부터 "분·시·일·월·요일"</strong> 하나만
|
||||
기억하면 돼요. <span className="icode">*</span>는 "아무 때나(매번)"라는 뜻이고요.
|
||||
섹션 2에서 만든 스크립트를 크론에 등록하는 순간 — 여러분은 <strong>자동화</strong>를
|
||||
한 거예요. 이게 운영의 핵심 기술입니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>크론의 함정</b> — 크론은 여러분의 터미널과 <strong>다른 환경</strong>(다른 PATH,
|
||||
다른 작업 폴더)에서 실행돼요. "내가 치면 되는데 크론에선 안 돌아요"의 원인 대부분이
|
||||
이것! 그래서 크론에 등록하는 명령은 <strong>절대 경로</strong>로 쓰고, 출력을{' '}
|
||||
<span className="icode">>> 로그파일</span>로 남겨 두는 게 관례입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="파이프와 리다이렉션" sub="작은 도구들을 배관으로 이어 붙이기">
|
||||
<p>
|
||||
리눅스 명령어들은 하나하나는 단순해요. <span className="icode">grep</span>은 골라내기만,{' '}
|
||||
<span className="icode">wc</span>는 세기만, <span className="icode">tail</span>은 끝부분만.
|
||||
진짜 힘은 <strong>파이프(<span className="icode">|</span>)로 이어 붙일 때</strong> 나옵니다 —
|
||||
왼쪽 명령의 출력이 배관을 타고 오른쪽 명령의 입력으로 흘러 들어가요. 레고 블록처럼요.
|
||||
</p>
|
||||
<Code>{CODE_PIPE}</Code>
|
||||
<p>
|
||||
<span className="icode">></span>와 <span className="icode">>></span>의 차이는 꼭
|
||||
기억하세요. <span className="icode">></span>는 <strong>덮어쓰기</strong>(기존 내용 삭제!),{' '}
|
||||
<span className="icode">>></span>는 <strong>이어 붙이기</strong>예요. 로그를 모을 때{' '}
|
||||
<span className="icode">></span>를 잘못 쓰면 어제까지의 기록이 통째로 사라집니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> Git Bash를 열고 우리 프로젝트 폴더에서 파이프 3단 조합에
|
||||
도전해 보세요. <span className="icode">git log --oneline | grep fix | wc -l</span> —
|
||||
"커밋 기록에서 fix가 들어간 줄만 골라, 몇 개인지 세라"는 뜻이에요. 결과가 나오면{' '}
|
||||
<span className="icode">grep feat</span>으로 바꿔 기능 추가 커밋 수와 비교해 보고,
|
||||
마지막에 <span className="icode">> commit_report.txt</span>를 붙여 파일로 저장까지!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="프로세스 관리 심화" sub="서비스를 매니저(systemd)에게 맡기기">
|
||||
<p>
|
||||
리눅스 기초에서 <span className="icode">ps</span>로 프로세스를 구경했다면, 이제는
|
||||
프로세스를 <strong>서비스로 관리</strong>하는 법이에요. 서버 프로그램은 알바생이 손으로
|
||||
켜는 게 아니라, <strong>매니저에게 등록</strong>해 두고 "죽으면 살리고, 가게 문 열면
|
||||
(부팅하면) 자동으로 출근시켜"라고 맡겨야 합니다. 그 매니저가{' '}
|
||||
<strong>systemd</strong>이고, 매니저와 대화하는 리모컨이{' '}
|
||||
<span className="icode">systemctl</span>이에요.
|
||||
</p>
|
||||
<Code>{CODE_SYSTEMCTL}</Code>
|
||||
<p>
|
||||
우리 서버에서는 HTTPS 현관 역할의 <strong>Caddy</strong>가 systemd 서비스로 돌고,
|
||||
React 프론트와 Spring Boot 백엔드, PostgreSQL은 <strong>Docker</strong>가 컨테이너로
|
||||
관리해요. 관리 주체는 달라도 철학은 같습니다 — <strong>"사람 손이 아니라 시스템이
|
||||
살아 있게 한다."</strong>
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>기억할 것</b> — 문제가 생겼을 때의 순서는{' '}
|
||||
<span className="icode">status</span>(상태 확인) → 로그 확인(다음 섹션!) →{' '}
|
||||
<span className="icode">restart</span>(재시작)예요. 원인도 안 보고 restart부터 누르는 건
|
||||
아픈 이유를 안 물어보고 진통제부터 주는 것 — 당장은 낫지만 병은 그대로입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="로그는 /var/log에" sub="서버의 블랙박스를 여는 법">
|
||||
<p>
|
||||
서버에서 벌어지는 모든 일 — 누가 로그인했고, 어떤 서비스가 언제 죽었고, 디스크에
|
||||
무슨 문제가 있었는지 — 는 전부 <strong>로그</strong>로 남아요. 그리고 리눅스 세계의
|
||||
오랜 관습으로, 그 로그들은 <span className="icode">/var/log</span> 폴더에 모입니다.
|
||||
비행기의 블랙박스처럼, <strong>사고 원인은 언제나 여기서 찾아요</strong>.
|
||||
</p>
|
||||
<Code>{CODE_VARLOG}</Code>
|
||||
<p>
|
||||
특히 <span className="icode">tail -f</span>는 운영자의 단짝이에요. 로그 파일의 끝을
|
||||
<strong> 실시간으로 계속 보여 주는</strong> 명령이라, 배포 직후 화면에 띄워 두고
|
||||
"에러 안 나나?" 지켜보는 용도로 매일 씁니다. 섹션 4의 파이프와 합치면{' '}
|
||||
<span className="icode">tail -f 로그 | grep ERROR</span> — 에러만 골라 실시간 감시!
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>디스크 꽉 참 주의보</b> — 로그를 방치하면 몇 달 뒤 디스크가 가득 차서
|
||||
<strong> 멀쩡한 서비스가 갑자기 죽는</strong> 사고가 납니다. "잘 되던 서버가 이유 없이
|
||||
죽었다"의 단골 범인이에요. <span className="icode">df -h</span>로 디스크를 주기적으로
|
||||
확인하는 습관이 여러분을 구합니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="우리 서버의 실전 운영 명령 모음" sub="1~6섹션 총출동 — 멘토의 어깨너머 미리 보기">
|
||||
<p>
|
||||
이제 배운 걸 전부 이어 붙일 시간이에요. 아래는 AWESOMEDEV의 서울 EC2 서버에서
|
||||
실제로 쓰는 운영 동선입니다. <strong>접속 → 건강검진 → 로그 → 배포 → 현관 확인 →
|
||||
백업 확인</strong> — 어느 회사를 가도 이 뼈대는 같아요.
|
||||
</p>
|
||||
<Code>{CODE_OPS}</Code>
|
||||
<ol className="olist">
|
||||
<li><strong>ssh</strong> — 열쇠 파일(.pem)로 서버에 들어가요. 비밀번호보다 안전한 방식이에요.</li>
|
||||
<li><strong>df·free·docker ps</strong> — 디스크·메모리·컨테이너, 건강검진 3종 세트.</li>
|
||||
<li><strong>docker logs</strong> — "느려요/안 돼요" 신고가 오면 무조건 로그부터. (섹션 6)</li>
|
||||
<li><strong>git pull + compose up</strong> — Gitea의 최신 코드로 교체하는 우리 팀의 배포. (섹션 2의 스크립트가 이걸 묶은 것!)</li>
|
||||
<li><strong>systemctl·journalctl</strong> — Caddy 현관 점검. (섹션 5)</li>
|
||||
<li><strong>crontab -l + 백업 로그</strong> — 새벽의 자동 백업이 잘 돌았는지. (섹션 3)</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③</b> 실습 서버 계정을 받았다면 위 <strong>2번 건강검진 3종
|
||||
세트</strong>만 직접 쳐 보세요(읽기만 하는 안전한 명령들이에요). 디스크 사용률(Use%)이
|
||||
몇 %인지, 컨테이너가 몇 개 떠 있는지 수첩에 적어 멘토의 기록과 비교해 보기.
|
||||
아직 계정이 없다면 내 PC의 Git Bash에서 <span className="icode">docker ps</span>와{' '}
|
||||
<span className="icode">df -h</span>가 어떻게 나오는지 먼저 관찰!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🐧 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 환경변수로 설정을 주입하고, 스크립트와 크론으로 <strong>서버가 스스로
|
||||
일하게</strong> 만들고, 문제가 생기면 <strong>로그부터 여는</strong> 사람이 됐어요.
|
||||
명령어를 아는 것과 운영 감각이 있는 것의 차이가 바로 이것입니다. 다음은 이 서버 위에서
|
||||
돌아가는 데이터의 집 — <Link to="/learn/database"><strong>데이터베이스 기초</strong></Link>
|
||||
코스로 이어 가거나, 멘토에게 <strong>"배포 스크립트 함께 읽기"</strong> 과제를 요청해
|
||||
오늘 배운 if·for·파이프를 실전 코드에서 찾아보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
426
frontend/src/pages/courses/MemoryStoragePage.jsx
Normal file
426
frontend/src/pages/courses/MemoryStoragePage.jsx
Normal file
@ -0,0 +1,426 @@
|
||||
// 이 파일이 하는 일: "메모리와 저장장치 깊이 보기" 코스 — RAM이 데이터를 기억하는 원리에서
|
||||
// 출발해 SSD 내부 구조, 램 부족이 PC를 느리게 만드는 연쇄 반응까지 8개 섹션으로 안내하는
|
||||
// 정적 학습 페이지. (RAM 동작 → 용량 체감 → 듀얼채널 → SSD 내부 → NVMe vs SATA
|
||||
// → 스왑의 연쇄 → 리소스 모니터 실습 → 개발 PC 권장 사양)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_RAM_ADDRESS = `RAM = 번호가 붙은 거대한 사물함 벽
|
||||
|
||||
주소(번호) 내용물 (1칸 = 1바이트)
|
||||
─────────────────────────────────────
|
||||
0x0000 01001000 ← 'H'
|
||||
0x0001 01101001 ← 'i'
|
||||
0x0002 00100001 ← '!'
|
||||
...
|
||||
(8GB 램이면 이런 칸이 약 86억 개!)
|
||||
|
||||
CPU: "0x0001번 사물함 내용물 줘" → 즉시 꺼내 줌
|
||||
어느 칸이든 똑같은 속도로 접근 가능 — 그래서 이름이
|
||||
Random Access Memory (아무 데나 바로 접근하는 메모리)`;
|
||||
|
||||
const CODE_VOLATILE = `휘발성이란? — 전원이 꺼지면 전부 증발
|
||||
|
||||
RAM (휘발성) SSD/HDD (비휘발성)
|
||||
────────────────────────────────────────────────
|
||||
비유 책상 위 책장/서랍
|
||||
속도 매우 빠름 상대적으로 느림
|
||||
전원 OFF 전부 사라짐 ⚡ 그대로 남음
|
||||
역할 "지금 작업 중"인 것 "보관"하는 것
|
||||
|
||||
작업 흐름: 파일을 열면 → 책장(SSD)에서 꺼내 책상(RAM)에 펼침
|
||||
저장(Ctrl+S)하면 → 책상 위 내용을 책장에 다시 꽂음
|
||||
저장 안 하고 정전되면? → 책상 위 내용만 날아감 (겪어봤죠...)`;
|
||||
|
||||
const CODE_CAPACITY = `램 용량별 체감 — "책상 크기" 비교
|
||||
|
||||
8GB — 학생용 1인 책상
|
||||
브라우저 탭 10개 + 문서 작업 정도는 OK
|
||||
IDE + 브라우저 + Docker 띄우면 이미 꽉 참
|
||||
|
||||
16GB — 넉넉한 사무용 책상
|
||||
VSCode/IntelliJ + 브라우저 탭 수십 개 + Docker 컨테이너
|
||||
두세 개까지 무난. 개발 입문의 현실적인 최소선
|
||||
|
||||
32GB — 회의실 대형 테이블
|
||||
프론트(React 개발 서버) + 백엔드(Spring Boot) +
|
||||
PostgreSQL + Docker 여러 개 + 브라우저 전부 동시에.
|
||||
우리 회사 풀스택 개발 환경이 여유 있게 돌아가는 크기
|
||||
|
||||
핵심: 램이 크다고 '속도'가 빨라지는 게 아니라,
|
||||
'동시에 펼쳐놓을 수 있는 일'이 많아지는 것!`;
|
||||
|
||||
const CODE_DUAL_CHANNEL = `듀얼채널 — 램을 2개로 나눠 꽂으면 길이 2배
|
||||
|
||||
싱글채널 (16GB 1개) 듀얼채널 (8GB 2개)
|
||||
|
||||
CPU ══════ [16GB] CPU ══════ [8GB]
|
||||
1차선 ══════ [8GB]
|
||||
2차선!
|
||||
|
||||
같은 16GB라도 2개로 나눠 꽂으면 CPU↔램 사이가
|
||||
왕복 2차선 도로가 돼서 데이터 전송 대역폭이 최대 2배.
|
||||
|
||||
메인보드 램 슬롯이 4개라면 보통 1-3번 또는 2-4번처럼
|
||||
같은 색 슬롯에 짝지어 꽂아야 듀얼채널이 활성화돼요.
|
||||
(메인보드 설명서에 짝이 표시되어 있습니다)`;
|
||||
|
||||
const CODE_SSD_INSIDE = `SSD 뚜껑을 열면 — 부품은 딱 두 종류가 핵심
|
||||
|
||||
┌─────────────────────────────────┐
|
||||
│ [컨트롤러] [NAND] [NAND] │
|
||||
│ (사서) [NAND] [NAND] │
|
||||
│ (책장들) │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
NAND 플래시: 전기를 가둬서 0/1을 기억하는 칩. 전원이
|
||||
꺼져도 갇힌 전기가 유지돼서 데이터가 남아요(비휘발성).
|
||||
|
||||
컨트롤러: NAND를 관리하는 작은 CPU(도서관 사서).
|
||||
- 어느 칸에 뭘 쓸지 배치 (같은 칸만 닳지 않게 순환 배치)
|
||||
- 지우고 다시 쓰기 관리 (NAND는 덮어쓰기가 안 되고
|
||||
블록째 지운 뒤 써야 함 — 사서가 이걸 숨겨서 처리)
|
||||
|
||||
수명(TBW, Total Bytes Written): NAND 한 칸은 쓰고 지우기를
|
||||
반복하면 닳아요. "총 몇 TB까지 쓸 수 있는지"가 TBW 스펙.
|
||||
예) TBW 300TB = 매일 80GB씩 써도 10년 이상 — 일반 사용은
|
||||
걱정 X. 다만 로그를 초당 수백 MB씩 쓰는 서버는 계산 필요!`;
|
||||
|
||||
const CODE_NVME_SATA = `NVMe vs SATA — 같은 SSD인데 '도로'가 다르다
|
||||
|
||||
SATA SSD NVMe SSD
|
||||
──────────────────────────────────────────────
|
||||
연결 통로 SATA (구형 규격) PCIe (CPU 직행로)
|
||||
순차 읽기 ~550 MB/s 3,500~7,000+ MB/s
|
||||
생김새 2.5인치 네모 박스 껌처럼 얇은 M.2 스틱
|
||||
|
||||
체감 비유 — 10GB 게임 설치 파일 복사:
|
||||
HDD(~150MB/s) : 약 67초 시내버스
|
||||
SATA(~550MB/s) : 약 18초 지하철
|
||||
NVMe(~5GB/s) : 약 2초 KTX
|
||||
|
||||
단, '작은 파일 수천 개'나 프로그램 실행 체감은
|
||||
순차 속도만큼 차이 나지 않아요 — 부팅이 2초 만에
|
||||
끝나지 않는 이유. 스펙표의 최고 속도는
|
||||
"뻥 뚫린 고속도로에서의 최고 속도"라고 이해하면 됩니다.`;
|
||||
|
||||
const CODE_SWAP_CHAIN = `램 부족 → 스왑 → 느려짐의 연쇄 반응
|
||||
|
||||
1) 램(책상)이 꽉 참
|
||||
2) OS: "당장 안 쓰는 것들, 책상에서 치우자"
|
||||
→ 램의 일부 내용을 SSD의 '스왑 공간'(임시 창고)으로 옮김
|
||||
3) 그런데 그 프로그램을 다시 클릭하면?
|
||||
→ 창고에서 도로 책상으로 가져와야 함
|
||||
4) 램은 나노초(ns), SSD는 마이크로초(µs) 단위
|
||||
→ 수백~수천 배 느린 창고를 계속 왕복
|
||||
5) 책상↔창고 왕복만 하느라 정작 일을 못 함
|
||||
= "컴퓨터가 미친듯이 느려졌는데 CPU는 놀고 있는" 그 상태!
|
||||
|
||||
증상 체크리스트:
|
||||
- 창 전환할 때마다 1~2초 멈칫
|
||||
- 디스크 사용률이 100%에 붙어 있음
|
||||
- 램 사용률 90%↑ 인데 CPU는 한가함
|
||||
→ 셋 다 해당되면 범인은 십중팔구 램 부족(스왑)입니다.`;
|
||||
|
||||
const CODE_RESOURCE_MONITOR = `# 실습 1 — 작업 관리자로 내 램 상태 읽기
|
||||
Ctrl + Shift + Esc → [성능] 탭 → [메모리]
|
||||
|
||||
읽을 것:
|
||||
사용 중 / 전체 예) 12.3/16.0GB — 지금 책상이 얼마나 찼나
|
||||
속도 예) 3200MHz — 램의 클럭
|
||||
슬롯 사용됨 예) 2/4 — 듀얼채널인지 힌트! (섹션 3)
|
||||
커밋됨 실제 램보다 크면 스왑을 쓰고 있다는 뜻
|
||||
|
||||
# 실습 2 — 리소스 모니터로 더 깊이
|
||||
Win + R → resmon 입력 → [메모리] 탭
|
||||
|
||||
'하드 폴트/초' 그래프 주목!
|
||||
하드 폴트 = "찾는 데이터가 램에 없어서 디스크(스왑)까지
|
||||
다녀온 횟수". 이 그래프가 계속 치솟으면
|
||||
섹션 6에서 배운 스왑 연쇄가 지금 일어나는 중이에요.
|
||||
|
||||
# 실습 3 — 범인 잡기
|
||||
[프로세스] 탭에서 메모리 열을 클릭해 정렬 →
|
||||
브라우저를 20탭쯤 열었다 닫으며 숫자 변화를 관찰해 보세요.`;
|
||||
|
||||
const CODE_DEV_SPEC = `AWESOMEDEV 개발 PC 권장 사양 (2026 기준)
|
||||
|
||||
부품 최소 권장 이유
|
||||
─────────────────────────────────────────────────────
|
||||
RAM 16GB 32GB IDE+Docker+브라우저 동시
|
||||
(듀얼채널) (섹션 2·3에서 배운 그대로)
|
||||
SSD NVMe 512GB NVMe 1TB Docker 이미지·node_modules가
|
||||
수십 GB를 순식간에 먹음
|
||||
CPU 6코어 8코어↑ 빌드·컨테이너는 코어를 다 씀
|
||||
|
||||
왜 이 조합인가? — 우리 스택을 전부 띄워 보면:
|
||||
IntelliJ/VSCode ~2-4GB
|
||||
React 개발 서버 ~1-2GB
|
||||
Spring Boot ~1-2GB
|
||||
PostgreSQL(Docker) ~0.5-1GB
|
||||
Docker 엔진+기타 ~2GB
|
||||
브라우저(탭 20개) ~3-6GB
|
||||
Windows 자체 ~4GB
|
||||
──────────────────────────
|
||||
합계 ~14-21GB ← 16GB면 이미 스왑 직전!
|
||||
|
||||
교훈: "개발 PC는 CPU보다 램에서 먼저 병목이 온다."`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'RAM의 동작' },
|
||||
{ n: 2, label: '용량별 체감' },
|
||||
{ n: 3, label: '듀얼채널' },
|
||||
{ n: 4, label: 'SSD 내부와 수명' },
|
||||
{ n: 5, label: 'NVMe vs SATA' },
|
||||
{ n: 6, label: '스왑의 연쇄' },
|
||||
{ n: 7, label: '리소스 모니터 실습' },
|
||||
{ n: 8, label: '개발 PC 사양' },
|
||||
];
|
||||
|
||||
export default function MemoryStoragePage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>메모리와 저장장치 깊이 보기</h1>
|
||||
<p>
|
||||
"램을 늘리면 빨라진다"는 말, 왜 맞고 언제 틀릴까요? RAM이 데이터를 기억하는
|
||||
원리부터 SSD 속 NAND 칩, 그리고 램 부족이 PC 전체를 마비시키는 연쇄 반응까지 —
|
||||
내 PC를 열어 보듯 하나씩 뜯어봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 3개 (내 PC로 바로)</span>
|
||||
<span className="chip">준비물: Windows PC 하나</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="RAM은 어떻게 기억할까" sub="번호 붙은 사물함, 그리고 전원이 꺼지면 사라지는 이유">
|
||||
<p>
|
||||
RAM은 <strong>번호(주소)가 붙은 초소형 사물함 수십억 개</strong>가 늘어선 벽이에요.
|
||||
CPU가 "3번 사물함 내용 줘" 하면 즉시 꺼내 주죠. 1번부터 차례로 뒤질 필요 없이
|
||||
<strong> 어느 칸이든 같은 속도로 바로</strong> 접근할 수 있어서 이름이
|
||||
Random Access(임의 접근) Memory입니다.
|
||||
</p>
|
||||
<Code>{CODE_RAM_ADDRESS}</Code>
|
||||
<p>
|
||||
그런데 RAM에는 치명적인 성격이 하나 있어요 — <strong>휘발성</strong>.
|
||||
RAM의 각 칸은 아주 작은 축전지에 전기를 채워 0/1을 표현하는데, 이 전기가
|
||||
순식간에 새어 나가서 <strong>1초에 수천 번씩 다시 채워(리프레시)</strong> 줘야 해요.
|
||||
전원이 꺼지면? 다시 채워 줄 수 없으니 전부 증발합니다.
|
||||
</p>
|
||||
<Code>{CODE_VOLATILE}</Code>
|
||||
<div className="tip">
|
||||
<b>비유 하나로 정리</b> RAM은 <strong>책상</strong>, SSD는 <strong>책장</strong>이에요.
|
||||
책상은 손만 뻗으면 닿지만(빠름) 퇴근하면 치워지고, 책장은 걸어가야 하지만(느림)
|
||||
내일도 그대로 있죠. 과제하다 저장 안 하고 노트북이 꺼져서 울어 본 적 있다면 —
|
||||
그게 바로 휘발성을 몸으로 배운 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="8GB vs 16GB vs 32GB" sub="용량이 크면 '빨라지는' 게 아니라 '안 느려지는' 것">
|
||||
<p>
|
||||
램 용량은 <strong>책상의 크기</strong>예요. 책상이 크다고 글씨를 빨리 쓰게 되진 않지만,
|
||||
책상이 작으면 책을 폈다 접었다 하느라 시간을 다 씁니다. 즉 램 증설의 효과는
|
||||
"속도 상승"이 아니라 <strong>"동시에 펼칠 수 있는 작업량의 상승"</strong>이에요.
|
||||
</p>
|
||||
<Code>{CODE_CAPACITY}</Code>
|
||||
<p>
|
||||
그래서 램이 <strong>남아도는</strong> 사람이 8GB→32GB로 올려도 체감이 없고,
|
||||
램이 <strong>모자라던</strong> 사람은 "새 컴퓨터 같다"고 느끼는 거예요.
|
||||
내가 어느 쪽인지는 섹션 7의 실습에서 숫자로 직접 확인합니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 오해</b> "크롬이 램을 많이 먹는 건 나쁜 것"? — 절반만 맞아요.
|
||||
비어 있는 램은 그냥 낭비이기 때문에, OS와 프로그램은 <strong>남는 램을 캐시로
|
||||
적극 활용</strong>합니다. 문제는 램을 많이 '쓰는' 게 아니라, 다른 프로그램이
|
||||
필요할 때 <strong>내놓지 못하는</strong> 상황(진짜 부족)이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="듀얼채널" sub="같은 16GB라도 1개보다 2개가 빠른 이유">
|
||||
<p>
|
||||
램 용량이 책상 크기라면, <strong>채널</strong>은 CPU와 책상 사이의
|
||||
<strong> 도로 차선 수</strong>예요. 램을 한 개 꽂으면 1차선, 두 개를 짝지어 꽂으면
|
||||
2차선 — 같은 시간에 오갈 수 있는 데이터가 최대 2배가 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_DUAL_CHANNEL}</Code>
|
||||
<p>
|
||||
그래서 조립 PC를 맞출 때 "16GB 1개"보다 <strong>"8GB 2개"</strong>가 보통 더 나은
|
||||
선택이에요(나중에 증설 여유는 줄지만요). 특히 내장 그래픽을 쓰는 노트북은 그래픽
|
||||
메모리까지 램을 같이 쓰기 때문에 듀얼채널 여부가 게임 프레임에도 티가 확 납니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> <span className="kbd">Ctrl</span>+<span className="kbd">Shift</span>+<span className="kbd">Esc</span>로
|
||||
작업 관리자를 열고 [성능] → [메모리]에서 <strong>"슬롯 사용됨"</strong>을 보세요.
|
||||
<span className="icode">2/4</span>나 <span className="icode">2/2</span>면 듀얼채널일
|
||||
가능성이 높고, <span className="icode">1/2</span>면 싱글채널 — 램 한 개만 더 꽂아도
|
||||
대역폭이 늘어날 수 있는 PC라는 뜻이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="SSD 내부 — NAND와 컨트롤러" sub="전기를 가둬 기억하는 책장과, 그걸 관리하는 사서">
|
||||
<p>
|
||||
SSD 안에는 모터도 원판도 없어요. <strong>NAND 플래시</strong>라는 메모리 칩과,
|
||||
그걸 지휘하는 <strong>컨트롤러</strong> 칩이 사실상 전부입니다. NAND는 아주 작은
|
||||
방에 전자를 가두는 방식으로 0/1을 기억하는데, RAM과 달리 <strong>전원이 꺼져도
|
||||
갇힌 전자가 그대로 유지</strong>돼요 — 그래서 저장장치가 될 수 있는 거죠.
|
||||
</p>
|
||||
<Code>{CODE_SSD_INSIDE}</Code>
|
||||
<p>
|
||||
NAND의 방은 <strong>쓰고 지우기를 반복하면 문이 닳는</strong> 소모품이에요.
|
||||
그래서 컨트롤러(사서)는 특정 방만 혹사당하지 않게 쓰기를 골고루 분산시키고
|
||||
(웨어 레벨링), 제조사는 "이 SSD는 총 몇 TB까지 쓸 수 있어요"라는
|
||||
<strong> TBW</strong> 수치로 수명을 보증합니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 내 SSD의 건강 상태는 스펙표가 아니라 SSD 스스로가
|
||||
기록하고 있어요(S.M.A.R.T. 정보). Windows에서
|
||||
<span className="kbd">Win</span>+<span className="kbd">R</span> →
|
||||
<span className="icode">cmd</span> →
|
||||
<span className="icode">wmic diskdrive get model,status</span>를 실행하면 디스크
|
||||
모델명과 상태(<span className="icode">OK</span>)를 볼 수 있고, 제조사 관리 도구를
|
||||
설치하면 지금까지의 총 쓰기량과 남은 수명 %까지 확인할 수 있습니다.
|
||||
내 노트북이 몇 TB나 썼는지 찾아보세요 — 생각보다 훨씬 적어서 놀랄 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="NVMe vs SATA" sub="같은 SSD, 다른 도로 — 시내버스와 KTX">
|
||||
<p>
|
||||
SSD의 속도는 NAND 자체보다 <strong>어떤 통로(인터페이스)로 연결되느냐</strong>가
|
||||
좌우해요. <strong>SATA</strong>는 HDD 시절부터 쓰던 구형 도로라 최고 속도가
|
||||
약 550MB/s에서 막히고, <strong>NVMe</strong>는 CPU로 직행하는 고속도로(PCIe)를
|
||||
써서 수천 MB/s를 냅니다.
|
||||
</p>
|
||||
<Code>{CODE_NVME_SATA}</Code>
|
||||
<p>
|
||||
다만 스펙표의 숫자는 <strong>큰 파일을 쭉 이어서 읽을 때</strong>의 최고 속도예요.
|
||||
개발할 때 자주 만나는 <span className="icode">node_modules</span>처럼
|
||||
<strong> 자잘한 파일 수만 개</strong>를 다루는 작업은 NVMe라도 스펙 속도가 안 나옵니다.
|
||||
KTX도 정거장(파일 하나하나의 처리)마다 서면 평균 속도가 뚝 떨어지는 것과 같아요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>구매 함정</b> M.2 슬롯에 꽂는 껌 모양 SSD라고 전부 NVMe는 아니에요 —
|
||||
모양은 M.2인데 속은 SATA인 제품도 있습니다. 스펙표에서
|
||||
<span className="icode">PCIe</span>/<span className="icode">NVMe</span> 표기를
|
||||
꼭 확인하세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="램 부족 → 스왑 → 느려짐" sub="'컴퓨터가 왜 이렇게 느리지'의 가장 흔한 범인">
|
||||
<p>
|
||||
책상(RAM)이 꽉 차면 OS는 일을 멈추는 대신, 당장 안 쓰는 것들을
|
||||
<strong> SSD 위의 임시 창고(스왑, 페이지 파일)</strong>로 옮겨서 자리를 만들어요.
|
||||
여기까지는 똑똑한 대처인데 — 문제는 그 다음입니다.
|
||||
</p>
|
||||
<Code>{CODE_SWAP_CHAIN}</Code>
|
||||
<p>
|
||||
RAM과 SSD의 속도 차이는 <strong>수백~수천 배</strong>예요. 책상 위 자료는 눈만
|
||||
돌리면 보이는데, 창고 자료는 매번 엘리베이터를 타고 내려가야 하는 셈이죠.
|
||||
창고를 뻔질나게 드나들기 시작하면(스래싱), 컴퓨터는 일 대신
|
||||
<strong> 짐 옮기기만 하는 상태</strong>가 됩니다. 이때 CPU 사용률은 오히려 낮아요 —
|
||||
CPU도 데이터가 도착하길 기다리며 놀고 있으니까요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>연결해서 생각하기</b> 스왑이 잦은 PC는 SSD에 쓰기가 계속 발생해요 —
|
||||
섹션 4에서 배운 <strong>TBW(수명)</strong>도 그만큼 빨리 깎이죠. 램 부족은
|
||||
속도만이 아니라 SSD 수명까지 갉아먹는, 이중으로 손해인 상태입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="리소스 모니터 실습" sub="내 PC의 램 상태를 숫자로 진단하기">
|
||||
<p>
|
||||
이제 배운 걸 전부 내 PC에서 확인할 시간이에요. Windows에는 진단 도구가
|
||||
기본으로 두 개 들어 있습니다 — 간단한 <strong>작업 관리자</strong>와,
|
||||
더 깊이 보는 <strong>리소스 모니터(resmon)</strong>.
|
||||
</p>
|
||||
<Code>{CODE_RESOURCE_MONITOR}</Code>
|
||||
<p>
|
||||
핵심 지표는 <strong>하드 폴트/초</strong>예요. "필요한 데이터가 램에 없어서
|
||||
디스크까지 다녀온 횟수"니까, 섹션 6의 스왑 연쇄가 <strong>지금 일어나고 있는지</strong>를
|
||||
실시간 그래프로 보여 주는 셈이죠. 평소엔 잠잠하다가 프로그램을 잔뜩 열면
|
||||
치솟는 걸 직접 목격해 보세요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 (미니 과제)</b> ① 리소스 모니터를 열어 둔 채로
|
||||
브라우저 탭을 20~30개 열고, IDE와 Docker Desktop까지 실행해 보세요.
|
||||
② 램 사용률·하드 폴트 그래프가 어떻게 변하는지 관찰하고,
|
||||
③ "내 PC는 몇 GB쯤에서 스왑이 시작되더라"를 한 줄로 기록해 보세요.
|
||||
그 한 줄이 다음 섹션(권장 사양)을 읽는 나만의 기준이 됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="개발 PC 권장 사양" sub="우리 스택을 다 띄우면 램이 몇 GB 필요할까">
|
||||
<p>
|
||||
어썸데브의 개발 환경은 React 개발 서버 + Spring Boot + PostgreSQL + Docker를
|
||||
<strong> 동시에</strong> 띄우는 풀스택이에요. 하나하나는 가벼워도, 다 더하면
|
||||
책상이 순식간에 찹니다. 실제로 합산해 볼까요?
|
||||
</p>
|
||||
<Code>{CODE_DEV_SPEC}</Code>
|
||||
<p>
|
||||
합계를 보면 16GB는 <strong>"띄울 수는 있지만 여유가 없는"</strong> 선이라는 게
|
||||
보이죠. 여기서 브라우저 탭 몇십 개(개발자의 숙명)가 더해지면 섹션 6의 스왑
|
||||
연쇄가 시작됩니다. 그래서 권장이 32GB — 사양표의 숫자는 외우는 게 아니라,
|
||||
이렇게 <strong>내 작업량을 합산해서 스스로 도출</strong>하는 거예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 섹션 7에서 기록한 "내 PC의 스왑 시작 지점"과 위 합산표를
|
||||
비교해 보세요. 지금 내 장비로 우리 스택 전체를 띄울 수 있을지, 무엇을 줄여야
|
||||
할지(예: 브라우저 탭 정리, 안 쓰는 컨테이너 <span className="icode">docker stop</span>)
|
||||
판단할 수 있으면 이 코스는 합격입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🧠 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 "램을 늘리면 빨라져요?"라는 질문에 <strong>"네 상황엔 맞아/틀려"</strong>를
|
||||
근거와 함께 답할 수 있어요 — 책상과 책장, 스왑의 연쇄, 그리고 리소스 모니터의
|
||||
하드 폴트 그래프까지. 다음은 이 하드웨어 위에서 프로그램이 실제로 도는 과정 —{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서 데이터가
|
||||
내 PC 밖으로 나가는 여행을 이어서 배우고, 멘토에게 섹션 7의
|
||||
"내 PC 진단 한 줄"을 공유하는 것으로 과제를 마무리하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
384
frontend/src/pages/courses/MobileDesignPage.jsx
Normal file
384
frontend/src/pages/courses/MobileDesignPage.jsx
Normal file
@ -0,0 +1,384 @@
|
||||
// 이 파일이 하는 일: "모바일 디자인" 코스 — 엄지손가락 하나로 조작하는 화면을
|
||||
// 어떻게 설계하는지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (엄지의 법칙 → 모바일 우선 → 뷰포트/브레이크포인트 → 제스처 → 흔한 실수 → 웹 vs 앱 → 실습 점검)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_THUMB_ZONE = `한 손으로 폰을 쥐었을 때, 엄지가 닿는 영역 지도
|
||||
|
||||
┌─────────────────┐
|
||||
│ ✕ 힘든 영역 │ ← 화면 위쪽: 엄지를 쭉 뻗어야 닿음
|
||||
│ │ (뒤로가기·닫기 같은 '가끔 쓰는' 버튼 자리)
|
||||
│ △ 애매한 영역 │
|
||||
│ │
|
||||
│ ○ 편한 영역 │ ← 화면 아래쪽 1/3: 엄지의 홈그라운드
|
||||
│ [탭바] [탭바] [탭바]│ (자주 쓰는 내비게이션은 여기!)
|
||||
└─────────────────┘
|
||||
|
||||
그래서 요즘 앱들의 메인 메뉴가 전부 '하단 탭바'인 거예요.
|
||||
PC 시절엔 메뉴가 위에 있었죠 — 마우스는 어디든 똑같이 편하니까.`;
|
||||
|
||||
const CODE_TOUCH_TARGET = `터치 타깃 최소 크기 — 44px의 근거
|
||||
|
||||
마우스 커서 끝: 약 1px → 픽셀 단위로 정밀 클릭 가능
|
||||
어른 엄지 접촉면: 약 10mm → 화면에서 대략 44px 이상
|
||||
|
||||
┌──────┐ ┌──┐
|
||||
│ OK │ 44×44px │OK│ 24×24px
|
||||
└──────┘ └──┘
|
||||
누르기 쉬움 옆 버튼을 같이 누르거나 빗나감
|
||||
|
||||
CSS로 보장하는 법:
|
||||
.btn { min-width: 44px; min-height: 44px; }
|
||||
|
||||
버튼이 작아 보여야 한다면? 보이는 크기는 작게, 누르는 영역은
|
||||
padding으로 넓게 — '과녁'과 '그림'을 분리하면 됩니다.`;
|
||||
|
||||
const CODE_MOBILE_FIRST = `모바일 우선(Mobile First) — CSS를 쓰는 순서가 뒤집힌다
|
||||
|
||||
/* ✕ 데스크톱 우선: 큰 화면 기준으로 만들고 좁은 화면을 '고침' */
|
||||
.card-list { display: grid; grid-template-columns: repeat(4, 1fr); }
|
||||
@media (max-width: 768px) {
|
||||
.card-list { grid-template-columns: 1fr; } /* 덜어내는 작업의 연속 */
|
||||
}
|
||||
|
||||
/* ○ 모바일 우선: 좁은 화면이 기본값, 넓어지면 '더함' */
|
||||
.card-list { display: grid; grid-template-columns: 1fr; }
|
||||
@media (min-width: 768px) {
|
||||
.card-list { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
|
||||
핵심 차이: max-width로 '빼며' 내려가느냐, min-width로 '더하며' 올라가느냐.
|
||||
좁은 화면은 넣을 수 있는 게 적어서, 여기서 시작하면
|
||||
"진짜 중요한 것"부터 고르게 됩니다 — 우선순위 정리가 공짜로 따라와요.`;
|
||||
|
||||
const CODE_VIEWPORT = `뷰포트(viewport) = 브라우저가 페이지를 그리는 '창문'의 크기
|
||||
|
||||
index.html에 이 한 줄이 없으면 모바일 브라우저는
|
||||
"이건 PC용 페이지겠지" 하고 980px짜리 가상 화면에 그린 뒤 축소해서 보여줘요:
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
width=device-width → 창문 폭 = 기기 실제 폭 (375px이면 375px로)
|
||||
initial-scale=1 → 처음부터 확대/축소 없이 1:1로
|
||||
|
||||
우리 플랫폼 index.html에도 이 태그가 있어요. 지워 보면(실습 금지!)
|
||||
모든 글씨가 개미만 해집니다.`;
|
||||
|
||||
const CODE_BREAKPOINTS = `브레이크포인트 = 레이아웃이 '변신'하는 폭의 기준선
|
||||
|
||||
폭(px) 대표 기기 흔한 레이아웃
|
||||
──────────────────────────────────────────────
|
||||
~ 640 스마트폰 세로 1열, 하단 탭바
|
||||
640~1024 태블릿·폰 가로 2열, 사이드바 접힘
|
||||
1024 ~ 노트북·데스크톱 3~4열, 사이드바 펼침
|
||||
|
||||
주의: "아이폰은 390px이니까 390에 브레이크포인트!"는 함정이에요.
|
||||
기기는 매년 바뀝니다. 기기가 아니라
|
||||
"이 레이아웃이 몇 px부터 불편해지는가"를 기준으로 잡으세요.
|
||||
창을 천천히 줄여 보다가 디자인이 깨지는 지점 — 거기가 브레이크포인트입니다.`;
|
||||
|
||||
const CODE_GESTURES = `말 안 해도 통하는 제스처 관습 — 사용자의 '근육 기억'
|
||||
|
||||
제스처 사용자가 기대하는 동작
|
||||
─────────────────────────────────────────────
|
||||
아래로 당기기 새로고침 (풀 투 리프레시)
|
||||
좌우 스와이프 탭 전환, 카드 넘기기, 항목 삭제
|
||||
길게 누르기 복사·상세 메뉴 (PC의 우클릭 역할)
|
||||
두 손가락 벌리기 확대 (핀치 줌)
|
||||
화면 가장자리 스와이프 뒤로 가기 (특히 iOS)
|
||||
|
||||
규칙 1: 관습을 따르라 — 당겨서 새로고침인데 삭제가 되면 대참사.
|
||||
규칙 2: 제스처는 '지름길'로만 — 스와이프로만 삭제 가능하고
|
||||
버튼이 없다면, 그 기능은 없는 거나 마찬가지예요.
|
||||
보이는 버튼 + 제스처 지름길, 이게 정석입니다.`;
|
||||
|
||||
const CODE_MISTAKES = `모바일에서 가장 흔한 실수 2가지 — 우리도 겪었어요
|
||||
|
||||
① 너무 작은 글씨
|
||||
본문 16px 미만은 모바일에서 읽기 고문입니다.
|
||||
특히 input의 글씨가 16px보다 작으면 iOS는
|
||||
입력할 때 화면을 강제로 확대해 버려요(레이아웃이 튐).
|
||||
→ input { font-size: 16px; } 은 사실상 필수.
|
||||
|
||||
② 의도치 않은 가로 스크롤
|
||||
요소 하나가 화면 폭을 삐져나가면 페이지 전체가
|
||||
좌우로 흔들리는 '덜렁거리는 페이지'가 됩니다.
|
||||
단골 범인: 고정 폭(width: 600px), 긴 URL/코드 줄바꿈 실패,
|
||||
100vw + padding 조합, 스크롤 컨테이너 없는 넓은 표.
|
||||
|
||||
범인 색출 1줄 (개발자 도구 Console에서):
|
||||
[...document.querySelectorAll('*')]
|
||||
.filter(el => el.scrollWidth > document.documentElement.clientWidth)
|
||||
|
||||
② 번이 낯익다면 정상입니다 — 우리 플랫폼 FE-06 티켓이 바로
|
||||
"모바일에서 표가 화면을 뚫고 나가는" 가로 스크롤 버그예요.`;
|
||||
|
||||
const CODE_WEB_VS_APP = `모바일 웹 vs 네이티브 앱 — 어썸데브는 둘 다 합니다
|
||||
|
||||
모바일 웹 (React) 앱 (React Native · Flutter)
|
||||
────────────────────────────────────────────────────────────
|
||||
설치 필요 없음 (URL로 접속) 스토어에서 설치
|
||||
업데이트 배포 즉시 반영 스토어 심사 대기
|
||||
푸시 알림 제한적 자유로움
|
||||
카메라·센서 제한적 깊게 접근 가능
|
||||
첫 진입 장벽 낮음 높음 (설치 설득 필요)
|
||||
손맛(체감 성능) 브라우저 거침 네이티브급 부드러움
|
||||
|
||||
React Native: JS/React 문법으로 iOS·Android 동시 개발
|
||||
→ React를 아는 우리에게 진입장벽이 낮아요.
|
||||
Flutter: Dart 언어, 자체 렌더링 엔진으로 픽셀까지 직접 그림.
|
||||
|
||||
지금 배우는 모바일 디자인 원칙(엄지 영역·터치 타깃·제스처)은
|
||||
웹이든 앱이든 똑같이 적용됩니다 — 도구가 바뀌어도 엄지는 그대로니까요.`;
|
||||
|
||||
const CODE_DEVTOOLS = `개발자 도구로 모바일 뷰 켜기 (Chrome/Edge 기준)
|
||||
|
||||
1) 우리 플랫폼(edu.awesomedevapp.com) 접속
|
||||
2) F12 → 개발자 도구 열기
|
||||
3) Ctrl+Shift+M → '기기 툴바' 토글 (스마트폰 아이콘)
|
||||
4) 상단에서 기기 선택: iPhone SE(375px)가 좋은 시험대예요
|
||||
— 좁은 화면에서 살아남으면 어디서든 살아남습니다.
|
||||
5) 회전 아이콘으로 가로 모드도 확인
|
||||
|
||||
점검 체크리스트 (섹션 1~5 총출동!)
|
||||
□ 하단/주요 버튼이 엄지 영역에 있는가? (섹션 1)
|
||||
□ 터치 타깃이 44px 이상인가? 링크끼리 너무 붙지 않았나? (섹션 1)
|
||||
□ 본문 글씨가 16px 이상인가? (섹션 5)
|
||||
□ 가로 스크롤이 생기는 페이지는 없는가? (섹션 5)
|
||||
□ 375px에서 레이아웃이 깨지는 지점은? (섹션 3)`;
|
||||
|
||||
const CODE_REPORT = `개선점 리포트 양식 — 이렇게 3개를 적어 멘토에게 공유하세요
|
||||
|
||||
[개선점 1]
|
||||
- 발견 위치: (예: 학습 센터 목록 페이지, iPhone SE 세로)
|
||||
- 증상: (예: 코스 카드 안의 긴 제목이 잘리지 않고 삐져나가
|
||||
가로 스크롤 발생)
|
||||
- 어긴 원칙: (예: 섹션 5 — 의도치 않은 가로 스크롤)
|
||||
- 제안: (예: 제목에 줄바꿈 허용, 카드에 max-width: 100%)
|
||||
|
||||
→ 세 번째 개선점까지 채웠다면, 그중 하나는
|
||||
FE-06 같은 실제 티켓으로 등록해 보세요.
|
||||
"발견 → 리포트 → 티켓 → 수정"이 실무의 한 바퀴입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '엄지의 법칙' },
|
||||
{ n: 2, label: '모바일 우선 설계' },
|
||||
{ n: 3, label: '뷰포트와 브레이크포인트' },
|
||||
{ n: 4, label: '제스처 관습' },
|
||||
{ n: 5, label: '흔한 실수' },
|
||||
{ n: 6, label: '웹 vs 앱' },
|
||||
{ n: 7, label: '실습: 플랫폼 점검' },
|
||||
];
|
||||
|
||||
export default function MobileDesignPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>모바일 디자인<br />— 엄지손가락을 위한 화면 설계</h1>
|
||||
<p>
|
||||
모바일 화면은 "작은 PC 화면"이 아니라, <strong>엄지 하나로 조작하는 완전히 다른
|
||||
무대</strong>예요. 이 코스에서는 엄지가 닿는 영역부터 제스처 관습까지 배운 뒤,
|
||||
마지막엔 <strong>우리 플랫폼을 직접 모바일 뷰로 점검</strong>해서 개선점 3개를 찾아냅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 점검 리포트</span>
|
||||
<span className="chip">준비물: 내 PC와 내 폰</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="엄지의 법칙" sub="터치 타깃 44px, 그리고 내비게이션이 아래로 내려온 이유">
|
||||
<p>
|
||||
PC의 마우스 커서는 화면 어디든 <strong>똑같은 노력</strong>으로 갑니다. 하지만 폰을
|
||||
한 손으로 쥔 엄지는 달라요 — 화면 아래쪽은 홈그라운드, 위쪽은 원정 경기입니다.
|
||||
버스 손잡이를 잡은 반대 손으로 폰을 쓰는 자신을 떠올려 보세요.
|
||||
</p>
|
||||
<Code>{CODE_THUMB_ZONE}</Code>
|
||||
<p>
|
||||
두 번째 법칙은 <strong>과녁의 크기</strong>예요. 마우스 커서는 바늘 끝이지만,
|
||||
엄지는 도장입니다. 도장으로 바늘구멍을 맞출 수는 없죠. 그래서 눌리는 모든 것은
|
||||
<strong> 최소 44×44px</strong>을 확보해야 해요.
|
||||
</p>
|
||||
<Code>{CODE_TOUCH_TARGET}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 자기 폰에서 자주 쓰는 앱 3개를 열어 보세요.
|
||||
메인 메뉴가 전부 <strong>하단 탭바</strong>에 있지 않나요? 그리고 화면 맨 위
|
||||
왼쪽 구석의 버튼을 한 손 엄지로 눌러 보세요 — 폰을 고쳐 잡게 되는 그 불편함이
|
||||
오늘 배운 '엄지의 법칙'의 증거입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="모바일 우선 설계" sub="좁은 화면에서 시작하면 우선순위가 공짜로 정리된다">
|
||||
<p>
|
||||
<strong>모바일 우선(Mobile First)</strong>은 "좁은 화면용 CSS를 기본값으로 쓰고,
|
||||
화면이 넓어질 때 스타일을 <strong>더해 가는</strong>" 설계 방식이에요.
|
||||
작은 원룸에 살림을 먼저 들여 보면 진짜 필요한 물건이 뭔지 알게 되죠 — 넓은 집으로
|
||||
이사가면서 물건을 더하는 건 쉽지만, 반대로 큰 집 살림을 원룸에 욱여넣는 건 고역입니다.
|
||||
</p>
|
||||
<Code>{CODE_MOBILE_FIRST}</Code>
|
||||
<p>
|
||||
기술적인 이유도 있어요. 실제 사용자 트래픽의 절반 이상이 모바일이고, 모바일 기기는
|
||||
PC보다 성능과 네트워크가 약합니다. <strong>가장 약한 환경을 기본값</strong>으로 삼으면,
|
||||
나머지 환경에서는 저절로 쾌적해져요. 우리 플랫폼의 학습 페이지들도 1열 세로 흐름이
|
||||
기본이고, 넓은 화면에서 여백이 늘어나는 구조입니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 오해</b> 모바일 우선 = "모바일만 잘하면 됨"이 아니에요. PC 화면도
|
||||
똑같이 중요합니다. 순서의 문제예요 — <strong>어느 쪽을 기본값으로 두고
|
||||
시작하느냐</strong>의 이야기입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="뷰포트와 브레이크포인트" sub="창문의 크기, 그리고 레이아웃이 변신하는 기준선">
|
||||
<p>
|
||||
<strong>뷰포트</strong>는 브라우저가 페이지를 그리는 '창문'이에요. 모바일 브라우저는
|
||||
기본적으로 이 창문을 PC 크기로 가정하기 때문에, HTML에 <strong>viewport 메타 태그</strong>
|
||||
한 줄을 넣어 "창문 크기 = 기기 실제 크기"라고 알려줘야 합니다.
|
||||
</p>
|
||||
<Code>{CODE_VIEWPORT}</Code>
|
||||
<p>
|
||||
<strong>브레이크포인트</strong>는 화면 폭이 어느 선을 넘으면 레이아웃이 변신하는
|
||||
기준선이에요. 옷 사이즈 S·M·L처럼, 모든 몸에 맞는 옷 한 벌 대신
|
||||
<strong> 몇 개의 구간</strong>을 정해 두는 거죠.
|
||||
</p>
|
||||
<Code>{CODE_BREAKPOINTS}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 이 페이지에서 브라우저 창의 오른쪽 끝을 잡고
|
||||
<strong> 천천히 좁혀 보세요</strong>. 상단 메뉴와 카드 배치가 어느 폭에서
|
||||
'변신'하나요? 그 순간이 바로 이 페이지의 브레이크포인트가 발동한 지점입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="제스처 관습" sub="스와이프·풀투리프레시 — 사용자의 근육 기억을 존중하기">
|
||||
<p>
|
||||
모바일에는 버튼 없이도 통하는 <strong>몸짓 언어</strong>가 있어요. 목록을 아래로
|
||||
당기면 새로고침, 카드를 옆으로 밀면 넘기기 — 아무도 가르쳐 주지 않았는데 모두가
|
||||
알고 있죠. 수년간 수백 개의 앱이 같은 동작을 반복하며 만들어진
|
||||
<strong> 근육 기억</strong>이기 때문입니다.
|
||||
</p>
|
||||
<Code>{CODE_GESTURES}</Code>
|
||||
<p>
|
||||
디자이너가 할 일은 새 제스처를 발명하는 게 아니라 <strong>관습을 따르는 것</strong>이에요.
|
||||
문 손잡이를 상상해 보세요 — 아무리 예뻐도 돌리는 방향이 남들과 반대라면 나쁜
|
||||
손잡이입니다. 그리고 제스처는 눈에 보이지 않으니, 반드시 <strong>보이는 버튼과
|
||||
짝</strong>을 이뤄야 해요. 제스처는 아는 사람의 지름길일 뿐입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 자기 폰의 메신저 앱에서 대화 목록을 <strong>왼쪽으로
|
||||
살짝 밀어</strong> 보세요. 숨어 있던 버튼(알림 끄기·삭제 등)이 나타나죠?
|
||||
그 기능이 스와이프 없이도 (길게 누르기 등으로) 가능한지 확인해 보세요 —
|
||||
잘 만든 앱은 반드시 두 번째 길을 마련해 둡니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="모바일에서 흔한 실수" sub="작은 글씨와 가로 스크롤 — FE-06 티켓의 정체">
|
||||
<Code>{CODE_MISTAKES}</Code>
|
||||
<p>
|
||||
가로 스크롤이 특히 악질인 이유는 <strong>범인이 요소 하나</strong>여도 페이지
|
||||
전체가 덜렁거리기 때문이에요. 표(table)처럼 원래 넓은 콘텐츠는 페이지를 늘리는 대신,
|
||||
<span className="icode">overflow-x: auto</span>를 준 <strong>컨테이너 안에서만</strong>
|
||||
스크롤되게 가두는 것이 정석입니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>FE-06 티켓과 연결</b> 우리 플랫폼 이슈 트래커의 <strong>FE-06</strong>이 정확히
|
||||
이 유형이에요 — 모바일에서 표가 화면 폭을 뚫고 나가 가로 스크롤이 생기는 버그.
|
||||
이 섹션을 이해했다면 여러분은 이미 FE-06을 고칠 이론을 갖춘 겁니다.
|
||||
섹션 7 실습에서 직접 재현해 보고, 관심 있다면 멘토에게 이 티켓을 요청해 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="웹 vs 앱" sub="React Native·Flutter — 우리 회사가 둘 다 하는 이유">
|
||||
<p>
|
||||
같은 서비스라도 <strong>모바일 웹</strong>으로 낼지 <strong>네이티브 앱</strong>으로
|
||||
낼지는 큰 갈림길이에요. 웹은 URL 하나로 바로 열리는 <strong>노점</strong> — 진입은
|
||||
쉽지만 할 수 있는 게 제한적이죠. 앱은 <strong>정식 매장</strong> — 들어오게 하는
|
||||
문턱(설치)은 높지만, 들어온 손님에게는 푸시 알림·카메라·부드러운 손맛까지
|
||||
훨씬 많은 걸 해줄 수 있어요.
|
||||
</p>
|
||||
<Code>{CODE_WEB_VS_APP}</Code>
|
||||
<p>
|
||||
어썸데브는 React로 웹을 만들고, <strong>React Native와 Flutter로 앱도
|
||||
만듭니다</strong> — 남 얘기가 아니라 여러분이 하게 될 수도 있는 업무예요.
|
||||
좋은 소식: 지금 이 코스에서 배운 원칙들은 도구와 무관하게 전부 그대로 통합니다.
|
||||
화면을 누르는 건 언제나 사람의 엄지니까요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습: 우리 플랫폼 모바일 점검" sub="개발자 도구 모바일 뷰 + 개선점 3개 리포트">
|
||||
<p>
|
||||
이제 배운 걸 전부 들고 <strong>우리 플랫폼을 검사</strong>할 시간입니다. 폰이 없어도
|
||||
돼요 — 개발자 도구의 <strong>기기 툴바</strong>가 다양한 화면 크기를 흉내 내 줍니다.
|
||||
</p>
|
||||
<Code>{CODE_DEVTOOLS}</Code>
|
||||
<p>점검하면서 발견한 문제를 아래 양식으로 <strong>3개</strong> 정리해 보세요.</p>
|
||||
<ol className="olist">
|
||||
<li>기기 툴바(<span className="kbd">Ctrl</span>+<span className="kbd">Shift</span>+<span className="kbd">M</span>)로 375px 화면에서 플랫폼의 주요 페이지 4~5개를 돌아본다.</li>
|
||||
<li>체크리스트에 걸리는 지점을 스크린샷으로 남긴다.</li>
|
||||
<li>아래 리포트 양식으로 개선점 3개를 작성한다.</li>
|
||||
<li>여유가 되면 실제 폰으로도 접속해 개발자 도구와 <strong>느낌이 다른 지점</strong>(터치 정확도, 키보드가 화면을 가리는 문제 등)을 메모한다.</li>
|
||||
</ol>
|
||||
<Code>{CODE_REPORT}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 섹션 5의 <strong>가로 스크롤 범인 색출 1줄</strong>을
|
||||
개발자 도구 Console에 붙여넣어 보세요. 빈 배열 <span className="icode">[]</span>이
|
||||
나오면 그 페이지는 합격, 요소가 나오면 — 축하합니다, 개선점 1번을 코드로 찾아낸 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📱 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 화면을 볼 때 엄지 영역·터치 타깃·브레이크포인트가 보이는
|
||||
눈을 갖게 됐어요. 점검 리포트에서 찾은 개선점은 <strong>FE-06</strong> 같은
|
||||
실제 티켓으로 이어질 수 있습니다 — 발견한 사람이 고치는 사람이 되는 게
|
||||
가장 빠른 성장이에요. 이어서{' '}
|
||||
<Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스에서 오늘 본
|
||||
CSS(미디어 쿼리·overflow)를 직접 다뤄 보거나,{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서
|
||||
모바일 기기가 서버와 어떻게 대화하는지 배워 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
395
frontend/src/pages/courses/NetToolsPage.jsx
Normal file
395
frontend/src/pages/courses/NetToolsPage.jsx
Normal file
@ -0,0 +1,395 @@
|
||||
// 이 파일이 하는 일: "네트워크 명령어 실전" 코스 — 터미널에서 바로 써먹는
|
||||
// 네트워크 진단 명령 6개(ping·tracert·nslookup·ipconfig·netstat·curl)를
|
||||
// 실제 출력 예시와 함께 익히고, 마지막에 "인터넷 안 될 때" 진단 순서도로 묶는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 명령어 출력 예시는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 명령어 출력 예시 · 표 상수들 ──
|
||||
|
||||
const CODE_PING = `# "그 서버, 살아 있니?" 하고 노크해 보는 명령:
|
||||
ping edu.awesomedevapp.com
|
||||
|
||||
Ping edu.awesomedevapp.com [x.x.x.x] 32바이트 데이터 사용:
|
||||
x.x.x.x의 응답: 바이트=32 시간=8ms TTL=54
|
||||
x.x.x.x의 응답: 바이트=32 시간=9ms TTL=54
|
||||
x.x.x.x의 응답: 바이트=32 시간=8ms TTL=54
|
||||
x.x.x.x의 응답: 바이트=32 시간=10ms TTL=54
|
||||
|
||||
Ping 통계: 보냄 = 4, 받음 = 4, 손실 = 0 (0% 손실)
|
||||
왕복 시간: 최소 = 8ms, 최대 = 10ms, 평균 = 8ms
|
||||
|
||||
읽는 법 세 줄 요약:
|
||||
· 시간=8ms → 노크하고 대답 듣기까지 왕복 8밀리초 (낮을수록 빠름)
|
||||
· 손실 = 0% → 보낸 노크 4번에 4번 다 대답함 (손실이 있으면 회선 불안정)
|
||||
· "요청 시간이 만료되었습니다" → 대답 없음. 죽었거나, 길이 끊겼거나,
|
||||
일부러 대답 안 하는 서버일 수도 있어요(보안상 ping 무시하는 곳도 많음!)`;
|
||||
|
||||
const CODE_PING_COMPARE = `# 가까운 서버 vs 먼 서버 — 응답 속도 비교해 보기
|
||||
ping naver.com → 평균 5~15ms (한국 안, 옆 동네 수준)
|
||||
ping google.com → 평균 30~40ms (아시아 어딘가의 구글 서버)
|
||||
ping bbc.co.uk → 평균 250ms+ (영국까지 왕복!)
|
||||
|
||||
빛도 지구 반 바퀴를 도는 데 시간이 걸려요.
|
||||
ms 숫자는 사실상 "물리적 거리 + 환승 횟수"의 합산입니다.`;
|
||||
|
||||
const CODE_TRACERT = `# 목적지까지 어떤 환승역(라우터)들을 거치는지 추적:
|
||||
tracert bbc.co.uk
|
||||
|
||||
1 <1ms 192.168.0.1 ← 1번 홉: 내 공유기 (항상 첫 출발지!)
|
||||
2 3ms 10.x.x.x ← 통신사 동네 장비
|
||||
3 5ms ... ← 통신사 백본 진입
|
||||
...
|
||||
8 35ms ... ← 국제 관문 (여기서 해저 케이블로!)
|
||||
9 120ms ... ← 바다 건너 첫 홉 — 시간이 확 뜁니다
|
||||
...
|
||||
14 250ms x.x.x.x ← 영국의 BBC 서버 도착
|
||||
|
||||
읽는 법:
|
||||
· 한 줄 = 홉(hop) 하나 = 환승역 하나
|
||||
· 시간이 갑자기 확 뛰는 구간 = 대륙을 건너는 순간일 확률이 높아요
|
||||
· 중간에 "* * *" = 그 라우터가 대답을 생략한 것. 뒤가 이어지면 정상
|
||||
· 끝까지 "* * *"만 나오고 도착을 못 하면 → 그 지점 이후 길이 막힌 것`;
|
||||
|
||||
const CODE_NSLOOKUP = `# DNS(인터넷 전화번호부)에 직접 질문 던지기:
|
||||
nslookup edu.awesomedevapp.com
|
||||
|
||||
서버: ... ← 내 질문을 받아 준 DNS 서버
|
||||
Address: ...
|
||||
|
||||
권한 없는 응답: ← "원본이 아니라 캐시로 답해줌"이라는 뜻 (정상!)
|
||||
이름: edu.awesomedevapp.com
|
||||
Address: x.x.x.x ← 우리 학습 플랫폼 서버(AWS EC2 서울)의 실제 IP
|
||||
|
||||
# 비교 실험: nslookup naver.com
|
||||
# 큰 서비스는 Address가 여러 줄 나와요 — 손님(트래픽)을
|
||||
# 여러 창구로 나눠 받기 위해 IP를 여러 개 등록해 둔 겁니다.`;
|
||||
|
||||
const CODE_IPCONFIG = `# 남의 주소 말고 '내' 주소부터 알아야죠:
|
||||
ipconfig
|
||||
|
||||
이더넷 어댑터 이더넷: (WiFi면 "무선 LAN 어댑터 Wi-Fi")
|
||||
IPv4 주소 . . . . : 192.168.0.5 ← 내 사설 IP (아파트 호수)
|
||||
서브넷 마스크 . . : 255.255.255.0
|
||||
기본 게이트웨이 . : 192.168.0.1 ← 공유기 주소 (아파트 정문)
|
||||
|
||||
# 더 자세히 (DNS 서버 주소까지):
|
||||
ipconfig /all
|
||||
|
||||
# 이 명령이 중요한 이유: 뒤에 나올 '진단 순서도'의 출발점이
|
||||
# 전부 여기서 확인한 두 주소(내 IP, 게이트웨이)이기 때문!`;
|
||||
|
||||
const CODE_NETSTAT = `# 내 PC가 지금 누구랑 연결돼 있는지 전부 출력:
|
||||
netstat -ano
|
||||
|
||||
프로토콜 로컬 주소 외부 주소 상태 PID
|
||||
TCP 192.168.0.5:52310 x.x.x.x:443 ESTABLISHED 4312
|
||||
TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 8820
|
||||
TCP 127.0.0.1:5432 0.0.0.0:0 LISTENING 1204
|
||||
|
||||
읽는 법 (한 줄 = 연결 하나):
|
||||
· 로컬 주소 → 내 쪽 주소:포트
|
||||
· 외부 주소 → 상대방 주소:포트 (:443이면 HTTPS 접속 중이란 뜻)
|
||||
· ESTABLISHED → 지금 통화 중인 연결
|
||||
· LISTENING → "전화 오면 받을게요" 하고 대기 중인 서버 프로그램
|
||||
· PID → 그 연결을 쓰는 프로그램 번호 (작업 관리자에서 누군지 찾을 수 있음)
|
||||
|
||||
위 예시를 해석하면:
|
||||
· :443에 접속 중 → 브라우저가 어떤 사이트를 보는 중
|
||||
· :5173이 LISTENING → 내가 켜 둔 React(Vite) 개발 서버!
|
||||
· :5432가 LISTENING → PostgreSQL이 접속 대기 중 (Docker로 띄웠다면 그것)`;
|
||||
|
||||
const CODE_CURL = `# 브라우저 없이 터미널에서 웹 요청 보내기 — 백엔드 개발자의 청진기:
|
||||
curl https://edu.awesomedevapp.com/api/health
|
||||
|
||||
{"status":"UP"} ← 우리 Spring Boot 서버의 대답! (JSON)
|
||||
|
||||
# 자주 쓰는 옵션 3개만 먼저:
|
||||
curl -i https://edu.awesomedevapp.com/api/health # 응답 헤더까지 보기
|
||||
curl -s ... # 진행률 표시 숨기기
|
||||
curl -X POST ... # GET 말고 POST로 보내기
|
||||
|
||||
# -i 를 붙이면 이런 게 먼저 나와요:
|
||||
HTTP/2 200 ← 상태 코드! 200=성공, 404=없음, 500=서버 폭발
|
||||
content-type: application/json
|
||||
server: Caddy ← 우리 현관 안내데스크(Caddy)가 응대했다는 서명
|
||||
|
||||
브라우저는 사실 curl이 하는 일을 예쁘게 포장한 것뿐이에요.
|
||||
요청을 '날것'으로 보고 싶을 때, 프론트 없이 백엔드만 테스트하고
|
||||
싶을 때 curl을 씁니다. 실무에서 정말, 정말 많이 써요.`;
|
||||
|
||||
const CODE_FLOWCHART = `"인터넷이 안 돼요!" — 안쪽에서 바깥쪽으로 한 겹씩 벗겨 보는 순서
|
||||
|
||||
① 내 PC는 멀쩡한가? (집)
|
||||
ipconfig → IPv4 주소가 192.168.x.x 로 잡혀 있나?
|
||||
├─ 주소가 없거나 169.254.x.x → 공유기와 연결 자체가 안 됨.
|
||||
│ 랜선/WiFi부터 확인!
|
||||
└─ 정상이면 ↓
|
||||
|
||||
② 공유기까지는 가는가? (아파트 정문)
|
||||
ping 192.168.0.1 (①에서 본 '기본 게이트웨이' 주소)
|
||||
├─ 응답 없음 → 공유기 문제. 재부팅의 시간입니다
|
||||
└─ 응답 옴 → ↓
|
||||
|
||||
③ 바깥세상까지는 가는가? (도로)
|
||||
ping 8.8.8.8 (외우기 쉬운 유명 공용 DNS의 IP — 숫자로 직접!)
|
||||
├─ 응답 없음 → 집 밖 회선 문제. 통신사에 연락할 차례
|
||||
└─ 응답 옴 → ↓
|
||||
|
||||
④ 이름은 찾아지는가? (전화번호부)
|
||||
nslookup naver.com
|
||||
├─ 실패 → 회선은 되는데 DNS가 고장!
|
||||
│ "인터넷은 되는데 사이트만 안 열림"의 단골 범인
|
||||
└─ 성공 → ↓
|
||||
|
||||
⑤ 그럼 그 사이트만 문제 (상대방)
|
||||
다른 사이트는 열리는데 하나만 안 열리면
|
||||
내 잘못이 아니라 그 서버가 아픈 거예요. 기다리세요!
|
||||
|
||||
핵심: ③에서 IP(8.8.8.8)는 되는데 ④의 이름 찾기가 실패하면
|
||||
"회선 O, DNS X"라고 원인을 정확히 쪼갤 수 있어요.
|
||||
명령어 4개로 통신사 상담원보다 빠른 진단이 끝납니다.`;
|
||||
|
||||
const CODE_CHECKLIST = `오늘 배운 6가지 — 한 줄 요약 카드
|
||||
|
||||
ping 살아 있니? + 왕복 몇 ms? (생존 확인·응답 속도)
|
||||
tracert 거기까지 어떤 길로 가? (경로의 환승역 추적)
|
||||
nslookup 그 이름, 번호(IP)로 뭐야? (DNS에 직접 질문)
|
||||
ipconfig 그래서 '내' 주소는 뭔데? (내 IP·게이트웨이 확인)
|
||||
netstat 내 PC, 지금 누구랑 연결 중? (연결·대기 포트 목록)
|
||||
curl 브라우저 없이 서버에 말 걸기 (API 직접 호출·헤더 확인)
|
||||
|
||||
전부 설치가 필요 없는, Windows에 이미 들어 있는 도구들이에요.
|
||||
(curl도 Windows 10 이후 기본 탑재!)`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'ping' },
|
||||
{ n: 2, label: 'tracert' },
|
||||
{ n: 3, label: 'nslookup' },
|
||||
{ n: 4, label: 'ipconfig' },
|
||||
{ n: 5, label: 'netstat' },
|
||||
{ n: 6, label: 'curl' },
|
||||
{ n: 7, label: '진단 순서도' },
|
||||
];
|
||||
|
||||
export default function NetToolsPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 개념이 아니라 '손'을 훈련하는 코스라는 걸 못박기 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>네트워크 명령어 실전<br />— 터미널이 곧 청진기다</h1>
|
||||
<p>
|
||||
"인터넷이 안 돼요"라는 말 대신 <strong>"게이트웨이까지는 ping이 되는데 DNS 조회가
|
||||
실패해요"</strong>라고 말할 수 있게 되는 코스예요. 명령어 6개면 네트워크 문제의
|
||||
원인을 의사처럼 진단할 수 있습니다 — 전부 여러분 PC에 이미 설치돼 있어요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">명령어 6개 실습</span>
|
||||
<span className="chip">준비물: 터미널 하나</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="ping — 살아 있니?" sub="노크 한 번으로 생존 확인 + 응답 속도 측정">
|
||||
<p>
|
||||
<strong>ping</strong>은 상대 서버 방문 앞에서 <strong>노크</strong>하고 대답이
|
||||
돌아오기까지 시간을 재는 명령이에요. 가장 단순한데, 그래서 모든 네트워크 진단의
|
||||
출발점입니다. 대답이 오면 "살아 있고 길도 뚫려 있다", 안 오면 "죽었거나 길이
|
||||
막혔거나 대답을 거부 중" — 셋 중 하나로 좁혀지죠.
|
||||
</p>
|
||||
<Code>{CODE_PING}</Code>
|
||||
<p>
|
||||
<span className="icode">시간=8ms</span>의 <strong>ms(밀리초)</strong>는 1000분의 1초.
|
||||
내 목소리가 서버까지 갔다가 메아리로 돌아오는 왕복 시간이에요. 게임에서 말하는
|
||||
"핑이 튄다"의 그 핑이 바로 이 숫자입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 터미널(<span className="kbd">Win</span>+<span className="kbd">R</span> →
|
||||
<span className="icode">cmd</span>)을 열고 <span className="icode">ping edu.awesomedevapp.com</span>을
|
||||
쳐 보세요. 우리 학습 플랫폼 서버(EC2 서울)가 대답해요! 이어서 아래처럼 먼 나라
|
||||
서버들과 응답 속도를 비교해 보세요.
|
||||
</div>
|
||||
<Code>{CODE_PING_COMPARE}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="tracert — 어떤 길로 가?" sub="목적지까지의 환승역을 전부 보여주는 경로 추적">
|
||||
<p>
|
||||
ping이 "도착했다/못 했다"만 알려 준다면, <strong>tracert</strong>(trace route)는
|
||||
<strong> 지나간 정류장을 전부</strong> 보여줘요. 내 데이터는 공유기 → 통신사 →
|
||||
백본 → (해외라면) 해저 케이블을 갈아타며 여행하는데, 그 <strong>환승역(홉, hop)</strong>
|
||||
하나하나를 시간과 함께 찍어 줍니다.
|
||||
</p>
|
||||
<Code>{CODE_TRACERT}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> <span className="icode">tracert bbc.co.uk</span> 같은
|
||||
<strong> 해외 사이트</strong>로 홉을 세어 보세요. 국내 사이트(10홉 안팎)와 비교하면
|
||||
해외는 홉 수도 많고, 어느 순간 시간이 30ms → 120ms로 <strong>점프하는 구간</strong>이
|
||||
보여요 — 그게 여러분의 패킷이 바다를 건너는 순간입니다. 1번 홉이 내 공유기
|
||||
주소인 것도 꼭 확인!
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>중간이 * * * 이어도 당황 금지</b> — 보안상 대답을 생략하는 라우터가 많아요.
|
||||
마지막 줄에서 목적지에 도착했다면 경로는 정상입니다. 진짜 문제는
|
||||
"어느 홉 이후로 <strong>전부</strong> 끊기고 도착도 못 하는" 경우예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="nslookup — 이름 대신 번호를" sub="DNS 전화번호부에 직접 질문하기">
|
||||
<p>
|
||||
컴퓨터는 도메인 이름이 아니라 IP 숫자로 서로를 찾아요. 이름을 번호로 바꿔 주는
|
||||
전화번호부가 <strong>DNS</strong>고, <strong>nslookup</strong>(name server lookup)은
|
||||
그 전화번호부에 <strong>내가 직접 전화를 걸어 물어보는</strong> 명령입니다.
|
||||
"브라우저야 비켜 봐, 내가 직접 물어볼게" 하는 거죠.
|
||||
</p>
|
||||
<Code>{CODE_NSLOOKUP}</Code>
|
||||
<p>
|
||||
이 명령의 진짜 쓸모는 <strong>문제를 반으로 쪼개는 것</strong>이에요. 사이트가 안
|
||||
열릴 때 nslookup이 성공하면 "이름 찾기(DNS)는 정상, 그 뒤가 문제"고, 실패하면
|
||||
"범인은 DNS"로 확정 — 뒤에 나올 진단 순서도의 핵심 분기점입니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="ipconfig — 그래서 나는 누구?" sub="내 IP와 공유기 주소부터 확인하는 습관">
|
||||
<p>
|
||||
남의 서버를 진단하기 전에 <strong>내 주소부터</strong> 알아야 해요.
|
||||
<strong> ipconfig</strong>는 내 PC의 네트워크 명함을 보여주는 명령입니다.
|
||||
여기서 봐야 할 건 딱 두 줄 — <strong>내 IPv4 주소</strong>(아파트 호수)와
|
||||
<strong> 기본 게이트웨이</strong>(공유기, 즉 아파트 정문)예요.
|
||||
</p>
|
||||
<Code>{CODE_IPCONFIG}</Code>
|
||||
<div className="warn">
|
||||
<b>주소가 169.254.x.x 로 보인다면</b> — 공유기(DHCP)한테 주소를 못 받아서
|
||||
PC가 임시번호를 스스로 지어낸 상태예요. "저 아직 입주 못 했어요"라는 신호이니
|
||||
랜선·WiFi 연결부터 다시 확인해야 합니다.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③</b> <span className="icode">ipconfig</span>로 기본 게이트웨이
|
||||
주소를 확인한 뒤, 그 주소로 <span className="icode">ping 192.168.0.1</span>(내
|
||||
게이트웨이 주소로 바꿔서) 해 보세요. 1ms 안팎으로 즉답이 와요 — 같은 방 안에서의
|
||||
대화니까요. 섹션 1에서 잰 해외 서버 250ms와 비교하면 거리감이 확 옵니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="netstat — 지금 누구랑 통화 중?" sub="내 PC의 모든 연결과 대기 포트 목록">
|
||||
<p>
|
||||
<strong>netstat</strong>(network statistics)은 내 PC의 <strong>통화 목록</strong>이에요.
|
||||
지금 어떤 프로그램이 어디에 접속해 있고(ESTABLISHED), 어떤 프로그램이 전화를
|
||||
기다리며 대기 중인지(LISTENING) 전부 보여줍니다.
|
||||
</p>
|
||||
<Code>{CODE_NETSTAT}</Code>
|
||||
<p>
|
||||
개발자에게 특히 소중한 순간: <span className="icode">npm run dev</span>를 켰는데
|
||||
<strong> "포트가 이미 사용 중입니다"</strong> 에러가 날 때예요.
|
||||
<span className="icode">netstat -ano | findstr :5173</span>으로 그 포트를 물고 있는
|
||||
PID를 찾고, 작업 관리자에서 누군지 확인하면 범인 검거 끝. Docker로 띄운
|
||||
PostgreSQL(5432)이 살아 있는지 확인할 때도 똑같이 씁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ④</b> 브라우저로 우리 학습 플랫폼을 열어 둔 채
|
||||
<span className="icode"> netstat -ano | findstr :443</span>을 실행해 보세요.
|
||||
ESTABLISHED 줄들의 <strong>외부 주소</strong> 중에 섹션 3의 nslookup으로 알아낸
|
||||
우리 서버 IP가 있나요? 있다면 — 여러분의 브라우저와 서울 EC2 사이의 연결을
|
||||
방금 두 눈으로 목격한 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="curl — 브라우저 없이 API 호출" sub="백엔드 개발자의 필수 도구, 요청을 날것으로 보기">
|
||||
<p>
|
||||
<strong>curl</strong>은 터미널에서 웹 요청을 보내는 명령이에요. 브라우저가 하는 일의
|
||||
<strong> 알맹이만</strong> 남긴 도구죠. 화면도 버튼도 없이, 요청을 보내고 응답을
|
||||
글자 그대로 받아 봅니다. 프론트엔드를 거치지 않고 <strong>백엔드만 콕 집어
|
||||
테스트</strong>할 수 있어서, Spring Boot API를 만들 때 하루에도 수십 번 쓰게 돼요.
|
||||
</p>
|
||||
<Code>{CODE_CURL}</Code>
|
||||
<p>
|
||||
위 예시의 <span className="icode">/api/health</span>처럼 "나 살아 있어요"만 대답하는
|
||||
주소를 <strong>헬스체크</strong> 엔드포인트라고 해요. ping이 <strong>서버 기계</strong>의
|
||||
생존 확인이라면, curl로 헬스체크를 때리는 건 그 위에서 도는
|
||||
<strong> 애플리케이션(Spring Boot)</strong>의 생존 확인 — 기계는 멀쩡한데 앱만 죽는
|
||||
경우도 많거든요. 두 진단은 층이 다릅니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ⑤</b> 로컬에서 <span className="icode">npm run dev</span>로 개발
|
||||
서버를 켜 두고, 다른 터미널에서 <span className="icode">curl -i http://localhost:5173</span>을
|
||||
쳐 보세요. 브라우저가 받던 HTML이 날것 그대로 쏟아져요. 상태 코드
|
||||
<span className="icode"> 200</span>도 확인! 이어서 존재하지 않는 주소를 호출해
|
||||
<span className="icode"> 404</span>가 어떻게 생겼는지도 구경해 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="진단 순서도 — 인터넷이 안 될 때" sub="집 → 공유기 → DNS → 사이트, 안에서 밖으로 한 겹씩">
|
||||
<p>
|
||||
이제 배운 명령들을 <strong>수사 순서</strong>로 엮을 차례예요. 원칙은 하나 —
|
||||
<strong> 나에게서 가까운 쪽부터 바깥으로</strong> 한 겹씩 확인합니다. 정전이 났을 때
|
||||
우리 집 두꺼비집 → 복도 → 아파트 전체 → 동네 순으로 확인하는 것과 똑같아요.
|
||||
</p>
|
||||
<Code>{CODE_FLOWCHART}</Code>
|
||||
<p>
|
||||
이 순서도의 힘은 <strong>"어디까지는 정상"인지 선을 긋는 것</strong>이에요.
|
||||
무작정 "안 돼요"가 아니라 "③까지 통과, ④에서 실패 → DNS 문제"라고 말하는 순간,
|
||||
여러분은 문제를 <strong>신고하는 사람</strong>에서 <strong>진단하는 사람</strong>이
|
||||
됩니다. 실무에서 신뢰는 이런 데서 쌓여요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ⑥</b> 지금은 인터넷이 잘 되죠? 잘 될 때 ①~④를 한 번
|
||||
전부 돌려 보고 정상 출력을 눈에 익혀 두세요. 진짜 장애가 났을 때 "평소와 뭐가
|
||||
다른지"를 알아보는 게 진단의 절반입니다 — 건강할 때 건강검진 받아 두는 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🩺 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 터미널만으로 서버 생존 확인(ping), 경로 추적(tracert), DNS 질문(nslookup),
|
||||
내 주소 확인(ipconfig), 연결 목록(netstat), API 호출(curl)까지 —
|
||||
네트워크 청진기 세트를 전부 손에 넣었어요.
|
||||
</p>
|
||||
<Code>{CODE_CHECKLIST}</Code>
|
||||
<p className="muted">
|
||||
이 명령들이 오가는 길 자체가 궁금해졌다면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서 랜선부터
|
||||
해저 케이블까지의 여행을, curl로 두드려 본 API를 직접 만들어 보고 싶다면{' '}
|
||||
<Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스를 이어서 들어 보세요.
|
||||
멘토 과제: 오늘 배운 순서도로 <strong>모의 장애 진단 리포트</strong> 한 장을 써서
|
||||
제출해 보기!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
438
frontend/src/pages/courses/OpsBasicsPage.jsx
Normal file
438
frontend/src/pages/courses/OpsBasicsPage.jsx
Normal file
@ -0,0 +1,438 @@
|
||||
// 이 파일이 하는 일: "운영과 장애 대응 기초" 코스 — 배포 이후의 세계, 즉
|
||||
// 서비스가 느려지고 500이 뜨고 접속이 안 될 때 무엇을 어떤 순서로 확인하고,
|
||||
// 어떻게 복구하고, 어떻게 기록으로 남기는지를 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (배포는 시작이다 → 장애의 신호 → 원인 좁히기 → 로그 읽기 → 재시작의 정석
|
||||
// → 백업 3-2-1 → 장애 보고서 → 우리 플랫폼 시나리오 연습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip, warn 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 터미널 예시·표는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 터미널 예시 상수들 ──
|
||||
|
||||
const CODE_LIFECYCLE = `개발자가 보는 시간 vs 서비스가 사는 시간
|
||||
|
||||
기획 → 개발 → 테스트 → 배포 ← 여기까지가 "만드는 시간" (몇 주)
|
||||
└─→ 운영 · 운영 · 운영 · 운영 ... ← "사는 시간" (몇 년)
|
||||
|
||||
배포는 결승선이 아니라 개업식이에요.
|
||||
가게는 개업식 다음 날부터가 진짜 — 손님이 오고, 정전이 나고,
|
||||
냉장고가 고장 나는 건 전부 '개업 이후'에 벌어집니다.`;
|
||||
|
||||
const CODE_SIGNALS = `장애의 3대 신호 — 증상이 다르면 범인도 다르다
|
||||
|
||||
신호 사용자가 보는 것 흔한 범인 후보
|
||||
──────────────────────────────────────────────────────────
|
||||
① 느림 빙글빙글... 10초 뒤 뜸 DB 느린 쿼리, 메모리 부족,
|
||||
트래픽 폭주
|
||||
② 500 에러 "서버 오류" 페이지 백엔드 예외(코드 버그),
|
||||
DB 연결 끊김
|
||||
③ 접속 불가 아예 응답 없음 / 타임아웃 서버 다운, 컨테이너 죽음,
|
||||
도메인·인증서 문제
|
||||
|
||||
같은 "안 돼요"라도 셋 중 뭔지부터 구분하세요.
|
||||
의사가 "어디가 어떻게 아픈지"부터 묻는 것과 같아요.`;
|
||||
|
||||
const CODE_NARROWING = `원인 좁히기 — 바깥에서 안쪽으로, 큰 것에서 작은 것으로
|
||||
|
||||
[1] 서버 자체가 살았나? ping, SSH 접속 시도
|
||||
│ 살았다면 ↓
|
||||
[2] 컨테이너가 살았나? docker ps (Up? Restarting? Exited?)
|
||||
│ 살았다면 ↓
|
||||
[3] 앱이 뭐라고 말하나? docker logs 로 에러 메시지 확인
|
||||
│ 앱은 멀쩡해 보이면 ↓
|
||||
[4] DB가 살았나? DB 컨테이너 상태, 연결 에러 로그
|
||||
|
||||
정전됐을 때를 떠올려 보세요:
|
||||
동네 전체 정전인가?(서버) → 우리 집 두꺼비집인가?(컨테이너)
|
||||
→ 이 방 스위치인가?(앱) → 전구가 나갔나?(DB)
|
||||
큰 범위부터 확인해야 헛수고가 없어요.`;
|
||||
|
||||
const CODE_DOCKER_PS = `# EC2에 SSH로 들어간 뒤, 컨테이너 생존 확인:
|
||||
docker ps
|
||||
|
||||
CONTAINER ID IMAGE STATUS NAMES
|
||||
a1b2c3d4 mirim-backend Up 3 days backend
|
||||
e5f6a7b8 postgres:16 Up 3 days (healthy) db
|
||||
c9d0e1f2 caddy Restarting (1) 5s ago caddy ← 범인!
|
||||
|
||||
# STATUS 읽는 법:
|
||||
# Up 3 days → 3일째 정상 가동 중
|
||||
# Restarting (1) → 죽었다 살아나기를 반복 중 (뭔가 잘못됨!)
|
||||
# Exited (137) → 죽어 있음. 137은 메모리 부족(OOM)으로
|
||||
# 강제 종료됐을 때 자주 보이는 코드
|
||||
|
||||
# 멈춘 컨테이너까지 다 보려면:
|
||||
docker ps -a`;
|
||||
|
||||
const CODE_LOGS = `# 최근 로그 100줄만 보기 (전체를 보면 화면이 폭발해요):
|
||||
docker logs --tail 100 backend
|
||||
|
||||
# 실시간으로 흘러가는 로그 지켜보기 (-f = follow, 따라가기):
|
||||
docker logs -f backend
|
||||
# 끄기: Ctrl + C
|
||||
|
||||
# 컨테이너 밖 일반 로그 파일도 같은 요령:
|
||||
tail -f /var/log/caddy/access.log
|
||||
|
||||
# 로그에서 눈이 먼저 가야 할 곳:
|
||||
2026-07-16 14:03:21 ERROR ... Connection refused: db:5432
|
||||
───── ──────────────────────────
|
||||
레벨! 메시지 — "DB 5432 포트 연결 거부"
|
||||
# ERROR·FATAL 레벨 + 시각 + 마지막 메시지, 이 셋이 단서의 90%예요.
|
||||
# 특히 죽기 '직전' 마지막 몇 줄이 유언장입니다.`;
|
||||
|
||||
const CODE_RESTART = `# 재시작의 정석 — 순서가 있어요
|
||||
|
||||
# 1) 재시작 '전에' 증거(로그)부터 확보! 재시작하면 로그 맥락이 흐려져요
|
||||
docker logs --tail 300 backend > ~/incident_$(date +%m%d_%H%M).log
|
||||
|
||||
# 2) 컨테이너 하나만 재시작
|
||||
docker restart backend
|
||||
|
||||
# 3) 살아났는지 확인 — STATUS와 로그 둘 다
|
||||
docker ps
|
||||
docker logs -f backend # 에러 없이 기동 로그가 쭉 올라오면 OK
|
||||
|
||||
# 4) 사용자 입장에서 최종 확인 (서버 안에서 직접 요청 쏘기)
|
||||
curl -I https://edu.awesomedevapp.com
|
||||
# HTTP/2 200 ← 이게 떠야 진짜 복구!`;
|
||||
|
||||
const CODE_321 = `백업 3-2-1 규칙
|
||||
|
||||
3 — 복사본을 최소 3개 유지 (원본 1 + 백업 2)
|
||||
2 — 서로 다른 2가지 저장 장소/매체에 (예: 서버 디스크 + S3)
|
||||
1 — 그중 1개는 반드시 다른 장소(원격)에 (서버가 통째로 죽어도 살아남게)
|
||||
|
||||
우리 플랫폼 예시:
|
||||
원본: EC2 위 PostgreSQL (지금 돌아가는 DB)
|
||||
백업 1: 같은 EC2의 매일 새벽 pg_dump 파일
|
||||
백업 2: AWS S3에 업로드된 사본 ← "다른 장소 1개" 담당
|
||||
|
||||
# DB 백업(덤프) 만들기 — 개념만 눈으로 익혀 두세요:
|
||||
docker exec db pg_dump -U mirim mirimdb > backup_20260716.sql
|
||||
|
||||
# 복구는 반대 방향:
|
||||
docker exec -i db psql -U mirim mirimdb < backup_20260716.sql`;
|
||||
|
||||
const CODE_REPORT = `장애 보고서 5줄 골격 — 이 순서 그대로 채우면 됩니다
|
||||
|
||||
① 무슨 일이: 7/16 14:00~14:25 학습 플랫폼 접속 시 500 에러 (25분)
|
||||
② 영향 범위: 로그인한 전체 사용자, 페이지 로딩 실패
|
||||
③ 원인: DB 컨테이너가 메모리 부족(OOM)으로 종료됨
|
||||
④ 조치: 로그 확보 → db 컨테이너 재시작 → 정상 확인(14:25)
|
||||
⑤ 재발 방지: 메모리 사용량 모니터링 추가, 컨테이너 메모리 상한 조정 검토
|
||||
|
||||
포인트:
|
||||
- 시각은 반드시 숫자로 (❌ "아까" / ⭕ "14:03")
|
||||
- 추측과 사실을 구분 ("~로 추정됨"이라고 솔직하게 쓰기)
|
||||
- ⑤가 없는 보고서는 반쪽 — 같은 장애를 두 번 겪지 않는 게 목적!`;
|
||||
|
||||
const CODE_SCENARIO = `시나리오 훈련 — 소방 대피 훈련처럼, 말로 리허설해 보기
|
||||
|
||||
[시나리오 A] 아침에 출근했더니 edu.awesomedevapp.com이 안 열린다
|
||||
→ 내 순서: ping/SSH → docker ps → 죽은 컨테이너 logs
|
||||
→ 로그 저장 → restart → curl로 200 확인 → 보고
|
||||
|
||||
[시나리오 B] 사이트는 열리는데 로그인만 500 에러
|
||||
→ 힌트: 사이트가 '열린다'는 건 서버·Caddy는 살았다는 뜻!
|
||||
범위가 이미 좁혀졌죠 → 백엔드(Spring Boot) 로그부터
|
||||
|
||||
[시나리오 C] 어제까지 되던 게 배포 직후부터 이상하다
|
||||
→ 제1용의자는 '방금 바꾼 것'. 배포 내역부터 확인,
|
||||
필요하면 이전 버전으로 롤백 → 원인은 그다음에 천천히
|
||||
|
||||
[시나리오 D] DB 데이터가 실수로 지워졌다
|
||||
→ 재시작은 아무 소용 없음! (섹션 5에서 배운 '덮으면 안 되는 것')
|
||||
최근 백업 확인 → 복구 → 유실 구간 파악 → 정직하게 보고`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '배포는 시작이다' },
|
||||
{ n: 2, label: '장애의 신호' },
|
||||
{ n: 3, label: '원인 좁히기' },
|
||||
{ n: 4, label: '로그 읽기' },
|
||||
{ n: 5, label: '재시작의 정석' },
|
||||
{ n: 6, label: '백업 3-2-1' },
|
||||
{ n: 7, label: '장애 보고서' },
|
||||
{ n: 8, label: '시나리오 연습' },
|
||||
];
|
||||
|
||||
export default function OpsBasicsPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 배포 이후의 세계로 초대 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>운영과 장애 대응 기초<br />— 서비스가 아플 때 침착해지는 법</h1>
|
||||
<p>
|
||||
코드를 배포하는 순간부터 서비스는 24시간 살아 움직이고, 언젠가 반드시 아픕니다.
|
||||
이 코스에서는 장애의 신호를 알아채고, 원인을 <strong>순서대로</strong> 좁히고,
|
||||
복구하고, 기록으로 남기는 — 운영자의 기본기를 배워요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 60분</span>
|
||||
<span className="chip">실습 2개 + 시나리오 4개</span>
|
||||
<span className="chip">준비물: 내 PC + Docker(선택)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="배포가 끝이 아니다" sub="만드는 시간보다 '살아 있는 시간'이 훨씬 길다">
|
||||
<p>
|
||||
개발자의 달력에서 배포일은 빨간 동그라미가 쳐진 결승선처럼 보여요. 하지만
|
||||
서비스 입장에서 배포일은 <strong>개업식</strong>입니다. 가게는 개업식 다음 날부터가
|
||||
진짜죠 — 손님이 몰리고, 정전이 나고, 냉장고가 고장 나는 건 전부 개업 <strong>이후</strong>에
|
||||
벌어지니까요.
|
||||
</p>
|
||||
<Code>{CODE_LIFECYCLE}</Code>
|
||||
<p>
|
||||
우리 학습 플랫폼도 마찬가지예요. AWS 서울 리전의 EC2 위에서 Docker 컨테이너들
|
||||
(React 프론트, Spring Boot 백엔드, PostgreSQL, Caddy)이 <strong>지금 이 순간에도</strong>
|
||||
돌아가고 있습니다. 이렇게 돌아가는 서비스를 건강하게 유지하는 일이 <strong>운영(Operations)</strong>,
|
||||
아플 때 고치는 일이 <strong>장애 대응</strong>이에요. 운영을 아는 개발자는 코드를 짤 때부터
|
||||
"이게 새벽 3시에 죽으면 어떤 로그가 남을까?"를 생각합니다 — 그게 이 코스의 목표예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>마음가짐 하나</b> 장애는 실력이 부족해서 나는 게 아니라, 살아 있는 서비스라면
|
||||
<strong> 반드시</strong> 겪는 일이에요. 세계적인 서비스들도 매년 장애 공지를 올립니다.
|
||||
중요한 건 장애를 0으로 만드는 게 아니라, <strong>빨리 알아채고 · 침착하게 고치고 · 같은 일을
|
||||
반복하지 않는 것</strong>입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="장애의 신호 — 느림 · 500 · 접속 불가" sub="증상이 다르면 범인도 다르다">
|
||||
<p>
|
||||
병원에 가면 의사 선생님이 "어디가, 어떻게, 언제부터 아프세요?"부터 묻죠.
|
||||
장애 대응도 똑같아요. 사용자의 "안 돼요!"라는 말만 듣고 뛰어들면 안 되고,
|
||||
<strong> 증상이 셋 중 무엇인지</strong>부터 구분해야 합니다.
|
||||
</p>
|
||||
<Code>{CODE_SIGNALS}</Code>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>느림</strong> — 응답은 오는데 오래 걸려요. 서버는 살아 있지만 어딘가
|
||||
힘에 부치는 상태. 손님이 몰려 주방(DB)이 밀렸거나, 주방장이 지쳤거나(메모리 부족).
|
||||
</li>
|
||||
<li>
|
||||
<strong>500 에러</strong> — 서버가 "저 지금 처리하다 넘어졌어요"라고 스스로 알려주는
|
||||
상태예요. 요청은 도착했는데 처리 중 예외가 터진 것. 범인은 대부분 백엔드 코드나 DB 연결.
|
||||
</li>
|
||||
<li>
|
||||
<strong>접속 불가</strong> — 아예 대답이 없어요. 가게 문이 잠긴 상태.
|
||||
서버가 죽었거나, 컨테이너가 내려갔거나, 도메인·인증서가 꼬였을 수 있어요.
|
||||
</li>
|
||||
</ol>
|
||||
<div className="warn">
|
||||
<b>참고로, 400번대와 500번대는 달라요</b> — <span className="icode">404</span>(없는 주소),
|
||||
<span className="icode">403</span>(권한 없음) 같은 400번대는 "요청한 쪽 문제",
|
||||
<span className="icode">500</span>번대가 "서버 쪽 문제"입니다. 사용자가 주소를 잘못 친 것까지
|
||||
장애로 착각하면 밤새 엉뚱한 곳을 뒤지게 돼요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="원인 좁히기 — 확인에는 순서가 있다" sub="서버 → 컨테이너 → 로그 → DB, 바깥에서 안쪽으로">
|
||||
<p>
|
||||
집에 불이 안 들어올 때를 떠올려 보세요. 전구부터 갈아 끼우는 사람은 없어요.
|
||||
창밖을 보고 <strong>동네 전체 정전인지</strong> 먼저 확인하고, 아니면 <strong>두꺼비집</strong>,
|
||||
그다음 <strong>방 스위치</strong>, 마지막에 <strong>전구</strong> 순서죠.
|
||||
장애 대응도 똑같이 <strong>큰 범위에서 작은 범위로</strong> 좁혀 갑니다.
|
||||
</p>
|
||||
<Code>{CODE_NARROWING}</Code>
|
||||
<p>
|
||||
우리 플랫폼(EC2 + Docker)에서 2번 단계의 핵심 명령이
|
||||
<span className="icode">docker ps</span>예요. 컨테이너들의 출석부라고 생각하면 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_DOCKER_PS}</Code>
|
||||
<p>
|
||||
이 순서를 지키면 좋은 점이 하나 더 있어요 — <strong>중간에 멀쩡한 단계는 용의선상에서
|
||||
빠집니다</strong>. "사이트는 열리는데 로그인만 안 된다"면 서버·Caddy는 이미 무죄고,
|
||||
백엔드나 DB로 범위가 확 좁혀지죠. 탐정이 알리바이로 용의자를 지워 가는 것과 같아요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 내 PC에 Docker Desktop이 있다면 터미널에서
|
||||
<span className="icode">docker ps</span>를 쳐 보세요. 로컬에서 실습 컨테이너를 띄운 적이
|
||||
있다면 STATUS 열에 <span className="icode">Up ...</span>이 보일 거예요. 이어서
|
||||
<span className="icode">docker ps -a</span>로 죽어 있는(Exited) 컨테이너까지 확인해 보고,
|
||||
둘의 차이를 눈으로 비교해 보세요. Docker가 없다면 멘토와 함께 개발 서버에서 구경해도 좋아요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="로그 읽기 — 서버가 남긴 일기장" sub="docker logs와 tail -f, 그리고 어디부터 볼 것인가">
|
||||
<p>
|
||||
로그는 서버가 매 순간 쓰는 <strong>일기장</strong>이에요. "14시 3분, DB에 연결하려 했는데
|
||||
거절당했다"처럼 자기가 겪은 일을 시각과 함께 전부 적어 둡니다. 장애의 원인은
|
||||
십중팔구 이 일기장 안에 있어요 — 문제는 일기가 하루에 수십만 줄이라는 것.
|
||||
그래서 <strong>어디부터 읽을지</strong>가 기술입니다.
|
||||
</p>
|
||||
<Code>{CODE_LOGS}</Code>
|
||||
<ol className="olist">
|
||||
<li><strong>레벨부터</strong> — <span className="icode">INFO</span>는 평범한 일기,
|
||||
<span className="icode">WARN</span>은 "좀 이상한데?", <span className="icode">ERROR</span>·
|
||||
<span className="icode">FATAL</span>이 비명이에요. 비명만 먼저 골라 읽으세요.</li>
|
||||
<li><strong>시각을 맞춰서</strong> — 장애가 14:03에 시작됐다면 14:03 <strong>직전</strong> 로그가
|
||||
결정적 단서예요. 사건 발생 직전 CCTV부터 돌려 보는 것과 같죠.</li>
|
||||
<li><strong>죽었다면 마지막 줄</strong> — 컨테이너가 죽어 있다면 마지막 몇 줄이 유언장입니다.
|
||||
<span className="icode">OutOfMemoryError</span>, <span className="icode">Connection refused</span>
|
||||
같은 사인(死因)이 거기 적혀 있어요.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 로그 감각은 브라우저에서도 기를 수 있어요.
|
||||
지금 이 페이지에서 <span className="kbd">F12</span> → <span className="kbd">Console</span> 탭을
|
||||
열어 두고 플랫폼 여기저기를 돌아다녀 보세요. 프론트엔드의 로그가 흐르는 걸 볼 수 있습니다.
|
||||
빨간 글씨(에러)가 있다면 눌러서 메시지를 읽어 보세요 — 서버 로그를 읽는 눈과 똑같은 눈이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="재시작의 정석 — 그리고 재시작으로 덮으면 안 되는 것" sub="껐다 켜기는 치료제이자, 증거인멸이 될 수도 있다">
|
||||
<p>
|
||||
"껐다 켜 보세요"는 농담 같지만 실제로 강력한 응급처치예요. 꼬인 메모리, 끊긴 연결,
|
||||
멈춘 프로세스가 재시작 한 번에 초기화되니까요. 하지만 정석에는 <strong>순서</strong>가 있습니다 —
|
||||
특히 <strong>재시작 전에 로그부터 저장</strong>하는 것. 재시작은 병을 고치는 동시에
|
||||
<strong> 현장을 치워 버리는</strong> 행동이라, 증거를 먼저 챙기지 않으면 원인을 영영 모르게 돼요.
|
||||
</p>
|
||||
<Code>{CODE_RESTART}</Code>
|
||||
<p>
|
||||
그리고 더 중요한 이야기 — <strong>재시작으로 덮으면 안 되는 것</strong>들이 있어요.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>반복되는 장애</strong> — 일주일에 세 번 죽어서 세 번 재시작했다면, 그건 해결이 아니라
|
||||
<strong> 미룸</strong>이에요. 열이 날 때마다 해열제만 먹고 원인 검사를 안 받는 것과 같죠.
|
||||
메모리 누수나 느린 쿼리 같은 진짜 병은 그대로 자라고 있습니다.
|
||||
</li>
|
||||
<li>
|
||||
<strong>데이터 문제</strong> — 데이터가 지워졌거나 꼬였다면 재시작은 <strong>아무것도</strong>
|
||||
해결하지 못해요. 오히려 복구에 쓸 수 있던 흔적이 사라질 수 있습니다. 이건 백업의 영역이에요(다음 섹션!).
|
||||
</li>
|
||||
<li>
|
||||
<strong>원인 불명인 채로 종료 선언</strong> — "재시작했더니 되네요, 끝!"은 금지.
|
||||
보고서에 최소한 "원인은 OOM으로 <strong>추정</strong>, 로그 확보함"까지는 남겨야
|
||||
다음 사람(그리고 미래의 나)이 이어서 조사할 수 있어요.
|
||||
</li>
|
||||
</ol>
|
||||
<div className="warn">
|
||||
<b>절대 하지 말 것</b> — 확인 안 된 상태에서 <span className="icode">docker compose down</span>처럼
|
||||
<strong> 전체를 내리는</strong> 명령이나, DB 컨테이너를 <strong>삭제</strong>하는 명령을 습관처럼
|
||||
치지 마세요. 컨테이너 하나 재시작과 전체 초기화는 반창고와 대수술만큼 달라요.
|
||||
수습 기간에는 재시작도 <strong>멘토에게 먼저 알리고</strong> 함께 진행하는 게 원칙입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="백업 3-2-1 — 그리고 복구 '연습'의 중요성" sub="백업은 보험, 복구 연습은 보험금 청구 리허설">
|
||||
<p>
|
||||
재시작으로도 못 고치는 최악의 상황 — 데이터가 사라졌을 때 — 을 위한 마지막 안전망이
|
||||
<strong> 백업</strong>이에요. 백업의 세계에는 유명한 <strong>3-2-1 규칙</strong>이 있습니다.
|
||||
</p>
|
||||
<Code>{CODE_321}</Code>
|
||||
<p>
|
||||
왜 "다른 장소 1개"가 꼭 필요할까요? 백업 파일을 같은 서버에만 두면, 그 서버가 통째로
|
||||
죽는 순간 원본과 백업이 <strong>같이</strong> 사라지거든요. 일기장 복사본을 만들어 놓고
|
||||
같은 가방에 넣어 다니는 것과 같아요 — 가방을 잃어버리면 둘 다 끝이죠. 그래서 우리는
|
||||
EC2 밖의 S3에 사본을 하나 더 둡니다.
|
||||
</p>
|
||||
<p>
|
||||
그런데 백업보다 더 자주 잊히는 게 <strong>복구 연습</strong>이에요. 백업 파일이 있다는 것과
|
||||
그걸로 <strong>실제로 되살릴 수 있다</strong>는 건 전혀 다른 문제입니다. 막상 복구하려니
|
||||
파일이 깨져 있거나, 명령어를 아무도 모르거나, 복구에 하루가 걸리는 경우가 현실에서 정말 많아요.
|
||||
소화기를 사 두는 것(백업)과 소화기 사용법을 연습해 두는 것(복구 연습)의 차이죠 —
|
||||
불이 난 뒤에 사용 설명서를 처음 읽으면 늦습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>복구 연습의 3가지 질문</b> — 우리 팀은 이 질문에 답할 수 있어야 해요.
|
||||
① 가장 최근 백업은 <strong>언제</strong> 것인가? ② 복구하는 데 <strong>얼마나</strong> 걸리는가?
|
||||
③ 백업 시점 이후의 데이터는 <strong>얼마나</strong> 잃는가? 분기에 한 번은 테스트용 DB에
|
||||
진짜로 복구해 보면서 세 숫자를 갱신하는 게 이상적입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="장애 보고서 쓰는 법" sub="다섯 줄이면 충분하다 — 수습 과제 A2와 연결">
|
||||
<p>
|
||||
불을 다 껐다고 끝이 아니에요. 마지막 임무는 <strong>기록</strong>입니다. 장애 보고서는
|
||||
잘잘못을 따지는 반성문이 아니라, <strong>같은 불이 다시 나지 않게 하는 소방 일지</strong>예요.
|
||||
어썸데브에서는 다섯 줄 골격으로 씁니다.
|
||||
</p>
|
||||
<Code>{CODE_REPORT}</Code>
|
||||
<ol className="olist">
|
||||
<li><strong>사실과 추측을 구분하세요</strong> — 로그로 확인한 건 단정하고, 아닌 건
|
||||
"~로 추정"이라고 솔직하게. 틀린 단정 하나가 다음 사람을 엉뚱한 길로 보냅니다.</li>
|
||||
<li><strong>시각은 숫자로</strong> — "아까", "점심쯤" 대신 "14:03". 나중에 로그와
|
||||
대조할 수 있는 건 숫자뿐이에요.</li>
|
||||
<li><strong>재발 방지가 본론</strong> — ①~④는 과거 이야기, ⑤가 미래 이야기예요.
|
||||
"모니터링 추가", "백업 주기 단축"처럼 <strong>행동</strong>으로 적어야 합니다.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>수습 과제 A2와 연결</b> — 수습 온보딩의 <strong>A2 보고 훈련</strong>에서 배운
|
||||
"상황 → 원인 → 조치 → 다음 계획" 구조, 기억나나요? 장애 보고서는 그 구조의 서버 버전이에요.
|
||||
A2에서 연습한 대로 쓰되, 시각과 로그라는 <strong>증거</strong>가 붙는다는 점만 다릅니다.
|
||||
다음 A2 보고 때 이 5줄 골격으로 한번 써서 멘토에게 피드백을 받아 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="우리 플랫폼 장애 대응 시나리오 연습" sub="소방 훈련처럼 — 실제 상황 전에 말로 리허설">
|
||||
<p>
|
||||
마지막은 실전 리허설이에요. 진짜 장애가 났을 때 침착할 수 있는 비결은 딱 하나 —
|
||||
<strong> 미리 머릿속으로 겪어 보는 것</strong>입니다. 소방 대피 훈련을 해 본 사람이
|
||||
실제 화재에서 덜 당황하는 것과 같아요. 아래 네 가지 시나리오를 읽고, 각각
|
||||
<strong> 내가 무엇을 어떤 순서로 할지</strong> 소리 내어 설명해 보세요.
|
||||
</p>
|
||||
<Code>{CODE_SCENARIO}</Code>
|
||||
<p>
|
||||
시나리오 B가 특히 좋은 연습이에요 — "일부만 안 된다"는 정보 자체가 이미
|
||||
<strong> 원인 좁히기(섹션 3)의 절반</strong>을 끝내 준다는 걸 몸으로 느낄 수 있거든요.
|
||||
그리고 시나리오 D는 섹션 5·6의 교훈이 한꺼번에 등장합니다: 재시작이 만능이 아니라는 것,
|
||||
그리고 백업이 왜 마지막 안전망인지.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>멘토와 함께</b> — 넷 중 하나를 골라 멘토 앞에서 <strong>모의 장애 대응 브리핑</strong>을
|
||||
해 보세요. "먼저 docker ps로 확인하고요..."처럼 명령어까지 말로 짚으면서요.
|
||||
막히는 지점이 나오면 그 섹션으로 돌아가면 됩니다 — 말로 설명하다 막히는 곳이
|
||||
진짜 복습 포인트예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🚒 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 장애의 세 가지 신호를 구분하고, <strong>서버 → 컨테이너 → 로그 → DB</strong> 순서로
|
||||
원인을 좁히고, 증거를 남기며 재시작하고, 3-2-1 백업과 5줄 보고서까지 아는 사람이 됐어요.
|
||||
서버와 Docker가 아직 낯설다면 <Link to="/learn/server"><strong>서버와 배포 기초</strong></Link> 코스를
|
||||
먼저 복습하고, 로그 속 DB 에러를 더 깊이 이해하고 싶다면{' '}
|
||||
<Link to="/learn/database"><strong>데이터베이스 기초</strong></Link>로 이어 가세요.
|
||||
그리고 다음 <strong>A2 보고</strong>에서 오늘 배운 5줄 골격을 꼭 한번 써 보기!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
373
frontend/src/pages/courses/OsPage.jsx
Normal file
373
frontend/src/pages/courses/OsPage.jsx
Normal file
@ -0,0 +1,373 @@
|
||||
// 이 파일이 하는 일: "운영체제의 이해" 코스 — 컴퓨터를 켜는 순간부터
|
||||
// 운영체제가 뒤에서 해주는 일들(자원 관리·프로세스·파일·보안)을 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (OS의 역할 → 윈도우/맥/리눅스 → 프로세스와 멀티태스킹 → 부팅 과정 → 가상 메모리 → 업데이트 → 서버와 리눅스)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_OS_ROLE = `운영체제(OS) = 컴퓨터라는 건물의 '총괄 매니저'
|
||||
|
||||
[카톡] [브라우저] [VS Code] [게임] ← 앱(입주 손님들)
|
||||
│ │ │ │
|
||||
┌────┴───────┴────────┴───────┴────┐
|
||||
│ 운영체제 (매니저) │ ← Windows, macOS, Linux
|
||||
└────┬───────┬────────┬───────┬────┘
|
||||
│ │ │ │
|
||||
[CPU] [메모리] [디스크] [네트워크] ← 하드웨어(건물 시설)
|
||||
|
||||
매니저가 하는 4가지 큰 일:
|
||||
1) 자원 관리 — CPU·메모리를 누구에게 얼마나 줄지 배분
|
||||
2) 프로세스 관리 — 실행 중인 프로그램들을 만들고, 돌리고, 정리
|
||||
3) 파일 관리 — 디스크의 0과 1을 '폴더와 파일'로 보이게 정리
|
||||
4) 보안·권한 — "이 앱이 카메라를 써도 될까?" 허락을 관리`;
|
||||
|
||||
const CODE_OS_COMPARE = `세 가지 대표 OS — 어디서 만나게 될까?
|
||||
|
||||
OS 주로 쓰는 곳 어썸데브에서는?
|
||||
──────────────────────────────────────────────────────
|
||||
Windows 회사·학교 PC, 게임 여러분의 개발 PC 대부분
|
||||
macOS 디자인·개발자 노트북 일부 개발자의 노트북
|
||||
Linux 서버, 클라우드, 임베디드 AWS EC2 서버 전부!
|
||||
|
||||
공통점: 셋 다 위에서 배운 4가지 일(자원·프로세스·파일·보안)을 해요.
|
||||
차이점: '누구를 위해 최적화됐나'가 달라요.
|
||||
Windows/macOS → 화면 앞의 '사람' 한 명이 편하게
|
||||
Linux → 화면 없이 24시간 일하는 '프로그램들'이 안정적으로`;
|
||||
|
||||
const CODE_PROCESS = `프로그램 vs 프로세스 — 레시피와 요리 중인 냄비
|
||||
|
||||
프로그램 = 디스크에 저장된 레시피(파일). 가만히 있음.
|
||||
프로세스 = 그 레시피로 지금 요리 중인 냄비. CPU와 메모리를 먹으며 살아 움직임.
|
||||
|
||||
크롬을 3개 창으로 띄우면?
|
||||
chrome.exe (프로그램 1개) → 프로세스 여러 개!
|
||||
|
||||
멀티태스킹의 비밀:
|
||||
CPU 코어 1개는 한 순간에 한 가지 일만 해요.
|
||||
대신 1초에 수천 번씩 프로세스를 갈아 끼우죠(문맥 교환).
|
||||
┌────┬────┬────┬────┬────┐
|
||||
│카톡│크롬│음악│카톡│크롬│ ... ← 아주 잘게 쪼갠 시간 조각
|
||||
└────┴────┴────┴────┴────┘
|
||||
너무 빨라서 사람 눈에는 '동시에' 도는 것처럼 보입니다.
|
||||
(선생님 한 분이 질문하는 학생들 사이를 초고속으로 오가는 셈!)`;
|
||||
|
||||
const CODE_TASKMGR = `# 작업 관리자 여는 법 (셋 중 아무거나):
|
||||
Ctrl + Shift + Esc ← 가장 빠른 단축키
|
||||
Ctrl + Alt + Del → 작업 관리자
|
||||
작업 표시줄 우클릭 → 작업 관리자
|
||||
|
||||
# '프로세스' 탭에서 볼 것:
|
||||
이름 CPU 메모리
|
||||
──────────────────────────────
|
||||
Google Chrome 4.2% 1,824MB ← 창은 1개인데 프로세스는 수십 개!
|
||||
Code 1.1% 512MB
|
||||
System 0% ... ← OS 자신도 프로세스로 돌아요
|
||||
|
||||
# '성능' 탭에서 볼 것:
|
||||
CPU 사용률 그래프, 코어 개수, 메모리 사용량 —
|
||||
지금 이 순간 매니저(OS)가 자원을 어떻게 배분 중인지 실시간 중계입니다.`;
|
||||
|
||||
const CODE_BOOT = `전원 버튼을 누르면 벌어지는 일 (부팅 4단계)
|
||||
|
||||
① 펌웨어(UEFI/BIOS) 기상
|
||||
메인보드에 새겨진 첫 프로그램이 눈을 뜸.
|
||||
"키보드 있나? 메모리 정상인가?" 하드웨어 점호(POST).
|
||||
|
||||
② 부트로더 호출
|
||||
펌웨어: "디스크야, OS를 깨울 담당자(부트로더) 나와!"
|
||||
부트로더가 디스크에서 OS 커널을 찾아냄.
|
||||
|
||||
③ 커널 적재
|
||||
OS의 심장인 '커널'이 메모리에 올라와 시동을 걺.
|
||||
드라이버(하드웨어 통역사)들을 하나씩 깨움.
|
||||
|
||||
④ 서비스·로그인 화면
|
||||
백그라운드 서비스들 시작 → 로그인 화면 등장.
|
||||
여러분이 비밀번호를 치는 순간, 부팅 완료!
|
||||
|
||||
비유: 아침의 학교 — 경비아저씨가 문을 열고(①) 교무실에 불이 켜지고(②)
|
||||
교장선생님이 출근해 방송을 켜면(③) 각 반 선생님들이 조회를 시작(④)해요.`;
|
||||
|
||||
const CODE_VMEM = `가상 메모리 — "RAM이 모자라면 디스크를 빌려 쓴다"
|
||||
|
||||
물리 메모리(RAM) 8GB인데 앱들이 12GB를 원한다면?
|
||||
|
||||
[ RAM 8GB ] ← 지금 활발히 쓰는 것들 (책상 위)
|
||||
[ 디스크의 페이지 파일 ] ← 한동안 안 쓴 것들 (사물함)
|
||||
|
||||
OS가 하는 일:
|
||||
- 안 쓰는 메모리 조각을 몰래 디스크로 옮겨 둠 (스왑 아웃)
|
||||
- 다시 필요해지면 RAM으로 가져옴 (스왑 인)
|
||||
- 각 프로세스에게는 "너 혼자 메모리 다 쓰는 거야"라고
|
||||
널찍한 가상 주소 공간을 보여줌 (서로 침범 못 하게 격리!)
|
||||
|
||||
부작용: 디스크는 RAM보다 훨씬 느려서, 스왑이 잦아지면
|
||||
컴퓨터가 갑자기 굼떠져요 — "메모리 부족하면 느려진다"의 정체입니다.`;
|
||||
|
||||
const CODE_UPDATE = `업데이트가 담고 있는 것들
|
||||
|
||||
종류 내용 미루면?
|
||||
─────────────────────────────────────────────────────
|
||||
보안 패치 발견된 구멍(취약점) 메꾸기 해커의 뚫린 문이 계속 열려 있음
|
||||
버그 수정 "가끔 멈춰요" 같은 오류 고침 같은 오류를 계속 겪음
|
||||
드라이버 새 하드웨어와의 통역 개선 기기 인식 오류, 성능 저하
|
||||
기능 추가 새 기능·UI 개선 그냥 못 씀 (이건 좀 아쉬운 정도)
|
||||
|
||||
핵심: 업데이트의 절반 이상은 '보안'입니다.
|
||||
취약점은 발견되는 순간 전 세계에 공개돼요. 패치 안 한 컴퓨터는
|
||||
"우리 집 도어락 비밀번호가 인터넷에 올라왔는데 안 바꾼 집"과 같아요.
|
||||
회사 서버(EC2)도 마찬가지 — 그래서 서버 OS도 꾸준히 패치합니다.`;
|
||||
|
||||
const CODE_SYSTEMINFO = `# 터미널(PowerShell)에서 내 OS의 신상명세 보기:
|
||||
systeminfo | Select-Object -First 12
|
||||
|
||||
호스트 이름: MY-PC
|
||||
OS 이름: Microsoft Windows 11 ...
|
||||
OS 버전: 10.0.26xxx ... ← 업데이트할 때마다 올라가는 번호
|
||||
부팅 시간: 2026-07-16 오전 8:31 ← 마지막 부팅(섹션 4!)이 언제였는지
|
||||
총 물리 메모리: 16,384MB ← 내 RAM 크기(섹션 5!)
|
||||
|
||||
# 더 간단히 버전만 보려면: 시작 → 실행(Win+R) → winver 입력`;
|
||||
|
||||
const CODE_SERVER_LINUX = `왜 서버는 거의 다 리눅스일까?
|
||||
|
||||
우리 플랫폼이 사는 곳: AWS EC2 (서울) — OS는 리눅스
|
||||
|
||||
이유 1) 무료 + 오픈소스 서버 수백 대를 띄워도 OS 값 0원
|
||||
이유 2) 가볍다 화면(GUI)이 없어 자원을 앱에 몰아줌
|
||||
이유 3) 안 끄고 오래 감 몇 달씩 재부팅 없이 안정적으로 동작
|
||||
이유 4) 자동화 친화 전부 명령어·스크립트로 조작 → 배포 자동화
|
||||
이유 5) Docker의 고향 컨테이너 기술 자체가 리눅스 커널 기능 기반
|
||||
|
||||
여러분이 매일 쓰는 것들 뒤에도:
|
||||
학습 플랫폼(이 페이지!) · Gitea(edu.awesomedevapp.com) ·
|
||||
PostgreSQL · Caddy — 전부 리눅스 위의 프로세스(섹션 3!)로 돌고 있어요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'OS가 하는 일' },
|
||||
{ n: 2, label: '윈도우·맥·리눅스' },
|
||||
{ n: 3, label: '프로세스와 멀티태스킹' },
|
||||
{ n: 4, label: '부팅 과정' },
|
||||
{ n: 5, label: '가상 메모리 한 입' },
|
||||
{ n: 6, label: '업데이트는 왜?' },
|
||||
{ n: 7, label: '서버는 왜 리눅스?' },
|
||||
];
|
||||
|
||||
export default function OsPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>운영체제의 이해<br />— 전원 버튼 뒤에서 벌어지는 일</h1>
|
||||
<p>
|
||||
여러분이 카톡을 켜고 브라우저를 띄우는 동안, 뒤에서 CPU와 메모리를 나눠 주고
|
||||
질서를 지키는 총괄 매니저가 있어요 — 바로 <strong>운영체제(OS)</strong>입니다.
|
||||
이 코스에서는 그 매니저가 하는 일을 하나씩 열어 보고, <strong>"직접 확인해 보기"</strong>로
|
||||
내 PC에서 그 증거를 눈으로 찾아봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 3개</span>
|
||||
<span className="chip">준비물: 내 PC 하나</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="운영체제가 하는 일" sub="앱과 하드웨어 사이의 총괄 매니저">
|
||||
<p>
|
||||
컴퓨터를 건물에 비유하면, CPU·메모리·디스크는 <strong>건물 시설</strong>(전기·수도·방)이고,
|
||||
카톡·브라우저 같은 앱은 <strong>입주 손님</strong>이에요. 손님이 시설을 직접 뜯어 쓰게 두면
|
||||
건물은 순식간에 아수라장이 되겠죠. 그래서 그 사이에 <strong>총괄 매니저</strong>가 서 있습니다 —
|
||||
이게 운영체제예요.
|
||||
</p>
|
||||
<Code>{CODE_OS_ROLE}</Code>
|
||||
<p>매니저의 4가지 일을 조금만 더 풀면 이래요.</p>
|
||||
<ol className="olist">
|
||||
<li><strong>자원 관리</strong> — 여러 앱이 CPU와 메모리를 달라고 아우성칠 때, 공평하고 효율적으로 배분해요.</li>
|
||||
<li><strong>프로세스 관리</strong> — 프로그램을 실행시키고(탄생), 순서를 조율하고(생활), 끝나면 자원을 회수해요(정리).</li>
|
||||
<li><strong>파일 관리</strong> — 디스크에 있는 건 사실 0과 1의 바다예요. 이걸 폴더·파일·확장자로 보이게 정리해 주는 게 OS입니다.</li>
|
||||
<li><strong>보안·권한</strong> — "이 앱이 마이크를 켜도 될까?", "이 사용자가 이 파일을 지워도 될까?"를 검문해요.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>미리 보는 연결고리</b> 우리가 만들 웹 서비스(React 프론트, Spring Boot 백엔드)도
|
||||
결국 어느 컴퓨터 위에서 <strong>OS에게 자원을 받아 쓰는 손님</strong>이에요.
|
||||
OS를 알면 "왜 서버가 느려졌지?", "왜 메모리가 부족하지?"를 추적할 수 있게 됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="윈도우 · 맥 · 리눅스" sub="같은 일을 하는 세 매니저, 다른 성격">
|
||||
<p>
|
||||
운영체제는 하나가 아니에요. 여러분의 PC엔 <strong>Windows</strong>, 디자이너 노트북엔
|
||||
<strong> macOS</strong>, 그리고 우리 서비스가 사는 AWS 서버엔 <strong>Linux</strong>가
|
||||
돌고 있죠. 셋 다 섹션 1의 4가지 일을 똑같이 하지만, <strong>성격</strong>이 다릅니다.
|
||||
</p>
|
||||
<Code>{CODE_OS_COMPARE}</Code>
|
||||
<p>
|
||||
비유하면 이래요. Windows는 <strong>대형 마트</strong> — 누구나 들어와서 뭐든 할 수 있게
|
||||
다 갖춰 놨어요. macOS는 <strong>큐레이션 잘 된 편집숍</strong> — 정해진 하드웨어(맥) 위에서
|
||||
매끈하게 다듬어져 있죠. Linux는 <strong>공구 창고</strong> — 화려하진 않지만 튼튼하고,
|
||||
필요한 만큼만 꺼내 조립할 수 있어서 전문가(서버)들이 사랑해요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 오해 바로잡기</b> "리눅스는 검은 화면에 글자만 나오는 어려운 OS"가 아니에요.
|
||||
화면(GUI)을 <strong>일부러 뺀 채</strong> 서버용으로 쓰는 경우가 많은 것뿐,
|
||||
우분투처럼 Windows 못지않은 데스크톱 화면을 가진 리눅스도 많습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="프로세스와 멀티태스킹" sub="CPU 하나로 어떻게 '동시에' 다 돌아갈까?">
|
||||
<p>
|
||||
디스크에 저장된 실행 파일은 <strong>프로그램</strong>, 그게 실행돼서 메모리에 올라가
|
||||
살아 움직이는 상태가 <strong>프로세스</strong>예요. 레시피(프로그램)와
|
||||
지금 불 위에서 끓고 있는 냄비(프로세스)의 차이죠.
|
||||
</p>
|
||||
<Code>{CODE_PROCESS}</Code>
|
||||
<p>
|
||||
그래서 "멀티태스킹"은 사실 <strong>초고속 돌려막기</strong>예요. OS가 아주 잘게 쪼갠
|
||||
시간 조각을 프로세스들에게 번갈아 나눠 주는데, 그 전환이 1초에 수천 번이라 사람 눈엔
|
||||
전부 동시에 도는 것처럼 보이는 거죠. 이 배분 순서를 정하는 게 OS의
|
||||
<strong> 스케줄러</strong>입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> <span className="kbd">Ctrl</span>+<span className="kbd">Shift</span>+<span className="kbd">Esc</span>로
|
||||
작업 관리자를 열어 보세요. 브라우저 창은 하나인데 프로세스는 수십 개인 것,
|
||||
OS 자신(<span className="icode">System</span>)도 목록에 있는 것, 그리고 성능 탭에서
|
||||
CPU 코어 개수와 실시간 사용률 그래프를 찾아보세요. 탭을 잔뜩 열면 메모리 숫자가
|
||||
쑥쑥 올라가는 것도 관찰 포인트!
|
||||
</div>
|
||||
<Code>{CODE_TASKMGR}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="부팅 과정" sub="전원 버튼과 로그인 화면 사이의 몇 초">
|
||||
<p>
|
||||
전원을 누른 순간의 컴퓨터는 아무것도 모르는 상태예요. OS는 디스크에 잠들어 있고요.
|
||||
그럼 <strong>누가 OS를 깨울까요?</strong> 이 릴레이가 바로 부팅입니다.
|
||||
</p>
|
||||
<Code>{CODE_BOOT}</Code>
|
||||
<ol className="olist">
|
||||
<li><strong>펌웨어(UEFI)</strong>가 하드웨어 점호를 하고,</li>
|
||||
<li><strong>부트로더</strong>가 디스크에서 OS 커널을 찾아 메모리에 올리고,</li>
|
||||
<li><strong>커널</strong>이 드라이버와 함께 시동을 걸고,</li>
|
||||
<li><strong>서비스들</strong>이 켜지면 로그인 화면이 여러분을 맞이해요.</li>
|
||||
</ol>
|
||||
<p>
|
||||
"부팅이 느려요"의 범인은 대부분 ④단계 — 시작할 때 같이 켜지는 프로그램이 너무 많은
|
||||
경우예요. 작업 관리자의 <strong>시작 앱</strong> 탭에서 그 목록을 볼 수 있습니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>주의</b> 시작 앱 목록은 <strong>구경만</strong> 하세요. 회사 PC에서 보안 프로그램이나
|
||||
모르는 항목을 함부로 끄면 안 돼요. 끄고 싶은 게 있다면 멘토와 먼저 상의!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="가상 메모리 한 입" sub="RAM 8GB로 12GB짜리 일을 하는 마법">
|
||||
<p>
|
||||
앱들이 원하는 메모리를 다 합치면 실제 RAM보다 클 때가 많아요. 그런데도 컴퓨터가
|
||||
안 터지는 이유가 <strong>가상 메모리</strong>입니다. 책상(RAM)이 좁으면 당장 안 쓰는
|
||||
책을 <strong>사물함(디스크)</strong>에 넣어 두고, 필요할 때 다시 꺼내 오는 거예요.
|
||||
</p>
|
||||
<Code>{CODE_VMEM}</Code>
|
||||
<p>
|
||||
덤으로 중요한 효과가 하나 더 있어요 — <strong>격리</strong>. OS는 각 프로세스에게
|
||||
자기만의 가상 주소 공간을 주기 때문에, 한 앱이 다른 앱의 메모리를 마음대로 읽거나
|
||||
망가뜨릴 수 없어요. 크롬이 죽어도 카톡은 멀쩡한 이유죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 작업 관리자 → 성능 → 메모리에서
|
||||
<strong> "커밋된 크기"</strong>를 찾아보세요. <span className="icode">12/20GB</span>처럼
|
||||
두 숫자가 보이는데, 뒤 숫자가 실제 RAM보다 크다면 — 그게 바로 디스크까지 합쳐진
|
||||
가상 메모리의 크기입니다. 방금 배운 마법의 실물이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="업데이트는 왜 하나" sub="귀찮은 재부팅에 숨어 있는 것">
|
||||
<p>
|
||||
한창 작업 중인데 "업데이트 후 다시 시작" — 다들 미뤄 본 적 있죠?
|
||||
그런데 업데이트의 정체를 알면 생각이 좀 달라져요.
|
||||
</p>
|
||||
<Code>{CODE_UPDATE}</Code>
|
||||
<p>
|
||||
OS도 사람이 만든 거대한 소프트웨어라 계속 구멍(취약점)이 발견돼요. 발견된 취약점은
|
||||
공개되고, 공격자들은 <strong>"패치 안 한 컴퓨터"</strong>부터 노립니다.
|
||||
그래서 업데이트는 새 기능 선물이라기보다 <strong>도어락 비밀번호 교체</strong>에 가까워요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③</b> 터미널에서 아래 명령으로 내 PC의 OS 버전·마지막 부팅
|
||||
시간·RAM 크기를 확인해 보세요. 섹션 4(부팅)와 섹션 5(메모리)에서 배운 것들의
|
||||
신상명세가 한 화면에 나옵니다.
|
||||
</div>
|
||||
<Code>{CODE_SYSTEMINFO}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="서버는 왜 리눅스인가" sub="우리 플랫폼이 리눅스 위에 사는 이유">
|
||||
<p>
|
||||
지금 보고 있는 이 학습 플랫폼도, 코드가 사는 Gitea도, 전부 AWS EC2 위의
|
||||
<strong> 리눅스</strong>에서 돌아가요. 전 세계 서버의 대다수가 리눅스인 데는
|
||||
분명한 이유들이 있습니다.
|
||||
</p>
|
||||
<Code>{CODE_SERVER_LINUX}</Code>
|
||||
<p>
|
||||
섹션 2에서 말한 "성격 차이"가 여기서 빛나요. 서버는 화면 앞에 사람이 앉아 있지 않고,
|
||||
수십 개의 프로세스(섹션 3!)가 24시간 묵묵히 일하는 곳이에요. 그래서 화려함보다
|
||||
<strong> 가볍고, 안정적이고, 명령어로 전부 자동화되는</strong> 리눅스가 딱 맞습니다.
|
||||
우리가 쓰는 <strong>Docker</strong>도 리눅스 커널의 격리 기능(섹션 5의 격리, 기억나죠?)을
|
||||
토대로 만들어진 기술이고요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>다음 코스 예고</b> "그래서 리눅스는 어떻게 다루는데?"가 궁금해졌다면 성공이에요.
|
||||
터미널에서 파일을 만지고, 프로세스를 확인하고, 서버에 접속하는 법은{' '}
|
||||
<Link to="/learn/linux"><strong>리눅스 기초</strong></Link> 코스에서 손으로 직접 배웁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🖥️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 전원 버튼 뒤의 세계 — 매니저(OS)가 자원을 배분하고, 프로세스를 돌려막고,
|
||||
부팅 릴레이로 잠에서 깨고, 가상 메모리로 책상을 넓히는 과정을 전부 알게 됐어요.
|
||||
다음은 우리 서버의 OS를 직접 다뤄 볼 차례 —{' '}
|
||||
<Link to="/learn/linux"><strong>리눅스 기초</strong></Link> 코스에서 터미널로 프로세스와
|
||||
파일을 만져 보고, <Link to="/learn/network"><strong>네트워크의 이해</strong></Link>와
|
||||
이어 붙이면 "서버 한 대가 어떻게 굴러가는지"의 큰 그림이 완성됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
399
frontend/src/pages/courses/PeripheralsPage.jsx
Normal file
399
frontend/src/pages/courses/PeripheralsPage.jsx
Normal file
@ -0,0 +1,399 @@
|
||||
// 이 파일이 하는 일: "노트북과 주변기기 고르기" 코스 — 개발자의 첫 장비 선택을
|
||||
// 노트북 vs 데스크탑 고민부터 스펙표 읽는 법, 모니터·키보드·USB·독까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (노트북 vs 데스크탑 → 스펙표 읽기 → 모니터 → 키보드 → 듀얼 모니터 → USB 규격 → 허브·독 → 정리)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 표·다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 표 · 다이어그램 상수들 ──
|
||||
|
||||
const CODE_LAPTOP_VS_DESKTOP = `노트북 vs 데스크탑 — 개발자 관점 비교
|
||||
|
||||
항목 노트북 데스크탑
|
||||
──────────────────────────────────────────────────
|
||||
휴대성 어디서든 개발 OK 책상에 고정
|
||||
성능/가격 같은 돈이면 불리 같은 돈이면 유리
|
||||
업그레이드 거의 불가(램 온보드!) 램·SSD·GPU 교체 자유
|
||||
발열/소음 좁아서 뜨겁고 시끄러움 쿨링 여유, 조용
|
||||
화면/자세 작고 목이 굽음 모니터·의자 자유 배치
|
||||
|
||||
→ 실무 개발자의 흔한 정답: "노트북 + 자리에선 독으로
|
||||
큰 모니터·키보드에 연결" — 두 세계의 장점만 취하기.`;
|
||||
|
||||
const CODE_SPEC_SHEET = `노트북 스펙표 예시 — 이렇게 읽어요
|
||||
|
||||
CPU : 8코어 / 16스레드, 접미사 U or H (아래 참고!)
|
||||
RAM : 16GB LPDDR5 (온보드) ← "온보드" 단어를 찾아라!
|
||||
저장장치 : 512GB NVMe SSD ← HDD면 개발용으론 탈락
|
||||
화면 : 14" 1920x1200 IPS
|
||||
무게 : 1.2kg / 배터리 70Wh
|
||||
|
||||
CPU 접미사 해독법 (모델명 끝 알파벳):
|
||||
U = Ultra-low power. 저전력·저발열. 배터리 오래감.
|
||||
가볍게 코딩+문서엔 충분, 무거운 빌드는 느림.
|
||||
H = High performance. 고성능·고발열. 빌드·Docker 빠름.
|
||||
대신 무겁고 배터리 빨리 닳음.
|
||||
→ "들고 다니며 수업+코딩" = U도 OK
|
||||
"Docker 여러 개 + 무거운 IDE" = H 추천`;
|
||||
|
||||
const CODE_RAM_ONBOARD = `램 "온보드"가 왜 함정인가
|
||||
|
||||
[교체 가능 램] 메인보드 ── [슬롯] ←→ 램 카드 꽂았다 뺐다 OK
|
||||
[온보드 램] 메인보드에 램 칩이 납땜 → 영원히 그 용량
|
||||
|
||||
개발자 램 체감 (2026 기준):
|
||||
8GB → IDE + 브라우저만으로 허덕임. 비추천.
|
||||
16GB → IDE + 브라우저 + Docker 컨테이너 몇 개. 최소선.
|
||||
32GB → 로컬에 PostgreSQL·백엔드·프론트 다 띄워도 여유.
|
||||
|
||||
→ 온보드 16GB를 사면 3년 뒤에도 16GB.
|
||||
"나중에 추가하지 뭐"가 불가능하니 처음부터 여유 있게!`;
|
||||
|
||||
const CODE_MONITOR = `모니터 스펙표 3대장
|
||||
|
||||
① 해상도 — 화면에 찍히는 점(픽셀)의 개수
|
||||
FHD 1920x1080 가장 흔함. 코드 두 파일 나란히는 빠듯
|
||||
QHD 2560x1440 개발자 인기. 코드+브라우저 여유롭게
|
||||
4K 3840x2160 글자 또렷. 단, 배율(스케일링) 설정 필요
|
||||
|
||||
② 주사율(Hz) — 1초에 화면을 몇 번 새로 그리는가
|
||||
60Hz 사무·코딩 기본. 개발엔 충분
|
||||
144Hz+ 게임용. 스크롤이 부드럽긴 하지만 코드가 빨리
|
||||
짜지진 않아요(!)
|
||||
|
||||
③ 패널 — 화면을 만드는 방식
|
||||
IPS 색 정확, 어느 각도에서 봐도 균일 → 개발·디자인 추천
|
||||
VA 명암비 좋음(까만 게 진짜 까맣다), 곡면 많음 → 영화·게임
|
||||
TN 응답속도만 빠르고 색·시야각 나쁨 → 요즘은 비추천`;
|
||||
|
||||
const CODE_PIVOT = `개발자의 비밀 무기 — 세로 회전(피벗, Pivot)
|
||||
|
||||
[가로 모니터] [세로 모니터]
|
||||
┌───────────────┐ ┌───────┐
|
||||
│ 코드 40줄쯤 │ │ 코드 │
|
||||
│ 보이다가 끝 │ → │ 100줄+ │
|
||||
└───────────────┘ │ 한눈에!│
|
||||
│ 문서· │
|
||||
│ 로그도 │
|
||||
└───────┘
|
||||
|
||||
코드는 옆으로 길지 않고 아래로 길어요.
|
||||
스탠드가 "피벗 지원"인 모니터를 세로로 돌리면
|
||||
긴 함수·로그·문서를 스크롤 없이 통째로 봅니다.
|
||||
듀얼 모니터라면 하나는 가로(메인) + 하나는 세로(코드/로그)가 국룰.`;
|
||||
|
||||
const CODE_KEYBOARD = `키보드 — 배열과 축
|
||||
|
||||
배열(키 개수):
|
||||
풀배열(104키) 숫자패드 있음. 넓은 책상용
|
||||
텐키리스(87키) 숫자패드 삭제. 마우스가 가까워져 어깨 편함
|
||||
60~75% 방향키·F열까지 압축. 휴대 좋지만 조합키 암기 필요
|
||||
|
||||
기계식 키보드의 "축" = 키 하나하나 속 스위치 종류:
|
||||
청축 "찰칵!" 소리 큼. 타건감 최고, 사무실에선 민폐
|
||||
갈축 적당한 걸림 + 적당한 소리. 무난한 첫 축
|
||||
적축 걸림 없이 부드럽고 조용한 편. 장시간 타이핑
|
||||
저소음 적축 도서관급 정숙. 사무실 최강
|
||||
|
||||
→ 개발자는 하루 수천 번 키를 눌러요. 의자·키보드는
|
||||
"몸이 닿는 시간"이 가장 긴 장비라 투자 가치가 큽니다.`;
|
||||
|
||||
const CODE_USB = `USB — 이름이 꼬여서 생긴 대혼란 정리
|
||||
|
||||
헷갈림의 원인: "모양"과 "속도"는 별개다!
|
||||
|
||||
① 모양(커넥터):
|
||||
USB-A 납작한 직사각형. 뒤집으면 안 들어가는 그거
|
||||
USB-C 타원형. 위아래 구분 없음. 요즘 표준
|
||||
|
||||
② 속도(규격):
|
||||
USB 2.0 480Mbps 마우스·키보드용
|
||||
USB 3.x Gen1 5Gbps 보통 파란색 단자
|
||||
USB 3.x Gen2 10Gbps
|
||||
USB4 40Gbps~
|
||||
|
||||
③ 함정: USB-C 모양이라고 다 빠른 게 아니다!
|
||||
C 모양인데 속도는 2.0인 저가 케이블도 많아요.
|
||||
충전만 되고 데이터·영상은 안 되는 C 케이블도 있고요.
|
||||
|
||||
→ 확인법: 제품 스펙에서 "Gen" 숫자와
|
||||
"영상 출력(DP Alt Mode) 지원" 여부를 읽을 것.
|
||||
모양만 보고 사면 모니터가 안 켜지는 수가 있어요.`;
|
||||
|
||||
const CODE_DOCK = `허브 vs 독 — 문어발의 두 등급
|
||||
|
||||
[USB 허브] = 멀티탭
|
||||
노트북 C포트 1개 → A포트 몇 개 + HDMI 1개 정도로 분배
|
||||
전원 없이 노트북 전력을 나눠 씀. 가볍고 저렴.
|
||||
|
||||
[도킹 스테이션(독)] = 노트북의 "자리 기지"
|
||||
┌─ 모니터 2대 (HDMI/DP)
|
||||
├─ 유선 랜(RJ45) 독 ←C 케이블 1개→ 노트북
|
||||
├─ 키보드·마우스 (USB-A)
|
||||
└─ 노트북 충전(전원 공급)까지!
|
||||
|
||||
아침에 출근 → C 케이블 딱 1개 꽂으면
|
||||
모니터·랜·키보드·충전이 한 번에 연결.
|
||||
퇴근할 땐 1개만 뽑으면 끝. 이게 "노트북+독" 조합의 힘.`;
|
||||
|
||||
const CODE_CHECKLIST = `내 PC 스펙 직접 확인하기 (Windows)
|
||||
|
||||
# 방법 1 — 시스템 정보 창:
|
||||
윈도우 키 → "시스템 정보" 검색 → 실행
|
||||
프로세서 항목에서 CPU 모델명 끝 알파벳(U/H) 찾기!
|
||||
|
||||
# 방법 2 — 작업 관리자:
|
||||
Ctrl + Shift + Esc → [성능] 탭
|
||||
CPU: 코어/논리 프로세서 수
|
||||
메모리: 용량 + "슬롯 사용: 1/2" ← 2칸 중 1칸이면 증설 가능,
|
||||
"슬롯" 표기가 아예 없으면 온보드일 가능성!
|
||||
|
||||
# 방법 3 — 터미널에서:
|
||||
윈도우 키 + R → cmd 입력 → 확인
|
||||
systeminfo | findstr /C:"메모리"`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '노트북 vs 데스크탑' },
|
||||
{ n: 2, label: '스펙표 읽기' },
|
||||
{ n: 3, label: '모니터 고르기' },
|
||||
{ n: 4, label: '키보드' },
|
||||
{ n: 5, label: '듀얼 모니터' },
|
||||
{ n: 6, label: 'USB 규격 정리' },
|
||||
{ n: 7, label: '허브와 독' },
|
||||
];
|
||||
|
||||
export default function PeripheralsPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 장비는 개발자의 '작업 환경' — 스펙표를 읽을 줄 알면 광고에 안 속는다 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>노트북과 주변기기 고르기<br />— 스펙표가 읽히는 순간, 광고가 안 통한다</h1>
|
||||
<p>
|
||||
개발자에게 장비는 요리사의 칼과 같아요 — 매일 몇 시간씩 손과 눈이 닿는 도구니까,
|
||||
남의 추천이 아니라 <strong>스펙표를 스스로 읽고</strong> 고를 수 있어야 합니다.
|
||||
이 코스를 마치면 노트북 상세 페이지의 암호 같은 숫자들이 전부 한국어로 읽혀요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개</span>
|
||||
<span className="chip">준비물: 지금 쓰는 내 PC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="노트북 vs 데스크탑" sub="개발자는 어느 쪽? — 정답은 '섞어 쓰기'">
|
||||
<p>
|
||||
같은 가격이면 데스크탑이 성능은 늘 앞서요. 부품이 클수록 싸고, 발열 처리도
|
||||
쉬우니까요. 하지만 개발은 <strong>자리를 옮겨 다니며</strong> 하는 일이 많죠 —
|
||||
수업, 회의, 카페, 멘토 옆자리. 그래서 실무에선 노트북 파가 다수입니다.
|
||||
</p>
|
||||
<Code>{CODE_LAPTOP_VS_DESKTOP}</Code>
|
||||
<p>
|
||||
어썸데브에서도 개발자들은 노트북을 쓰되, 자리에서는 큰 모니터와 키보드에
|
||||
연결해 데스크탑처럼 씁니다. 어떻게 케이블 한 가닥으로 그게 되는지는
|
||||
마지막 <a href="#sec-7">7번 섹션(허브와 독)</a>에서 밝혀져요. 결국 이 코스 전체가
|
||||
"노트북 한 대를 데스크탑급 작업 환경으로 만드는 법" 이야기이기도 합니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>생각해 보기</b> 서버는 어느 쪽일까요? 우리 플랫폼이 돌아가는 AWS EC2(서울)는
|
||||
사실 <strong>데스크탑의 극단</strong>이에요 — 휴대성을 0으로 만들고 성능·안정성에
|
||||
전부 투자한, 데이터센터에 붙박이로 사는 컴퓨터죠.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="노트북 스펙표 읽기" sub="U와 H, 그리고 '온보드'라는 세 글자">
|
||||
<p>
|
||||
노트북 상세 페이지의 스펙표는 외계어처럼 보이지만, 개발자가 볼 곳은 사실
|
||||
몇 군데 안 돼요. 특히 <strong>CPU 모델명 끝의 알파벳</strong>이 그 노트북의
|
||||
성격을 통째로 말해 줍니다.
|
||||
</p>
|
||||
<Code>{CODE_SPEC_SHEET}</Code>
|
||||
<p>
|
||||
그리고 램 칸에서 <strong>"온보드"</strong>라는 세 글자를 발견하면 긴장하세요.
|
||||
온보드는 램이 메인보드에 <strong>납땜</strong>되어 있다는 뜻 — 스티커처럼 붙였다
|
||||
떼는 게 아니라 문신처럼 새겨진 거라, 나중에 용량을 늘릴 수 없습니다.
|
||||
</p>
|
||||
<Code>{CODE_RAM_ONBOARD}</Code>
|
||||
<div className="warn">
|
||||
<b>가장 흔한 후회</b> "8GB로 아껴 사고 나중에 늘려야지" → 온보드라 증설 불가 →
|
||||
Docker로 PostgreSQL 하나 띄우는 순간 PC가 숨을 헐떡임. 저장장치(SSD)는 교체 가능한
|
||||
모델이 많지만 <strong>램은 산 날의 용량이 평생 용량</strong>인 경우가 많아요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 지금 이 페이지를 보고 있는 PC의 스펙을 스스로 읽어 보세요.
|
||||
<span className="kbd">Ctrl</span>+<span className="kbd">Shift</span>+<span className="kbd">Esc</span>로
|
||||
작업 관리자를 열고 <strong>성능 탭</strong>에서 CPU 이름(끝 알파벳!)과 메모리 용량,
|
||||
슬롯 표기를 확인하는 거예요. 아래 세 가지 방법 중 편한 걸로!
|
||||
</div>
|
||||
<Code>{CODE_CHECKLIST}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="모니터 스펙 읽기" sub="해상도·주사율·패널 — 그리고 개발자는 세로로 돌린다">
|
||||
<p>
|
||||
모니터 광고는 숫자 잔치지만, 개발자가 볼 스펙은 세 가지로 정리돼요.
|
||||
해상도는 <strong>책상 크기</strong>(넓을수록 코드·브라우저를 많이 펼침),
|
||||
주사율은 <strong>페이지 넘기는 속도</strong>, 패널은 <strong>종이의 재질</strong>이라고
|
||||
생각하면 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_MONITOR}</Code>
|
||||
<p>
|
||||
개발용 우선순위는 <strong>해상도 > 패널(IPS) > 주사율</strong>이에요.
|
||||
게임용과 정반대죠. 코드는 결국 글자라서, 글자가 많이·또렷하게 보이는 게 최고입니다.
|
||||
</p>
|
||||
<p>그리고 개발자만 아는 반전 기능이 하나 있어요 — 모니터를 90도로 돌리는 겁니다.</p>
|
||||
<Code>{CODE_PIVOT}</Code>
|
||||
<div className="tip">
|
||||
<b>알아 두기</b> 모니터 스탠드 스펙에서 <span className="icode">피벗(Pivot)</span>,
|
||||
<span className="icode">높낮이 조절</span>, <span className="icode">베사(VESA) 홀</span> —
|
||||
이 세 단어가 있으면 자세와 배치의 자유도가 확 올라가요. 패널 좋고 스탠드가 나쁜
|
||||
모니터보다, 베사 홀에 모니터 암을 다는 쪽이 목에는 이득일 때도 많습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="키보드" sub="하루 수천 타 — 손이 닿는 시간이 가장 긴 장비">
|
||||
<p>
|
||||
개발자는 마우스보다 키보드와 보내는 시간이 압도적으로 길어요. 그래서 키보드는
|
||||
"취향의 영역"이 아니라 <strong>손목 건강과 타이핑 속도의 영역</strong>입니다.
|
||||
고를 때 보는 건 두 가지 — <strong>배열</strong>(키가 몇 개냐)과
|
||||
<strong> 축</strong>(눌리는 느낌이 어떠냐)이에요.
|
||||
</p>
|
||||
<Code>{CODE_KEYBOARD}</Code>
|
||||
<p>
|
||||
숫자패드를 안 쓴다면 <strong>텐키리스</strong>가 의외의 꿀팁이에요. 키보드가
|
||||
좁아진 만큼 마우스가 몸 가까이 오고, 어깨가 벌어지지 않아 장시간 코딩이 편해집니다.
|
||||
축은 글로 백날 읽어도 몰라요 — 매장이나 친구 키보드로 <strong>직접 눌러 보고</strong> 고르세요.
|
||||
다만 사무실·교실에서 청축은... 옆자리의 원망을 사기 딱 좋습니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>화려함의 함정</b> RGB 조명이 번쩍인다고 코드가 빨리 짜지진 않아요.
|
||||
예산이 정해져 있다면 조명보다 <strong>축 취향과 손목 각도</strong>(팜레스트 포함)에
|
||||
먼저 쓰는 게 남는 장사입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="듀얼 모니터의 생산성" sub="화면 전환(Alt+Tab)을 눈동자 이동으로 바꾸기">
|
||||
<p>
|
||||
개발은 본질적으로 <strong>여러 화면을 오가는 일</strong>이에요. 코드를 쓰면서
|
||||
브라우저로 결과를 확인하고, 문서를 읽으면서 터미널 로그를 지켜보죠. 모니터가
|
||||
한 대면 이 전환이 전부 <span className="kbd">Alt</span>+<span className="kbd">Tab</span> —
|
||||
요리하면서 냉장고 문을 매번 여닫는 셈이에요. 듀얼 모니터는 재료를 조리대에
|
||||
<strong> 전부 꺼내 놓고</strong> 요리하는 것과 같습니다.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li><strong>메인 화면</strong>: 에디터(코드) — 시선이 가장 오래 머무는 정면에.</li>
|
||||
<li><strong>보조 화면</strong>: 브라우저(결과 확인)·문서·터미널 로그 — 곁눈질로 확인.</li>
|
||||
<li>여유가 되면 보조 화면을 <strong>세로(피벗)</strong>로 — 3번 섹션에서 배운 그 기능!</li>
|
||||
</ol>
|
||||
<p>
|
||||
프론트엔드 개발이라면 한쪽에 코드, 한쪽에 브라우저를 띄우고 저장할 때마다
|
||||
화면이 바뀌는 걸 <strong>고개 돌리지 않고</strong> 확인할 수 있어요. 이 "저장 → 결과가
|
||||
바로 보임" 루프가 짧아지는 것이 듀얼 모니터 생산성의 정체입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 모니터가 한 대여도 연습할 수 있어요! 브라우저에서 우리
|
||||
학습 플랫폼(edu.awesomedevapp.com)을 열고 <span className="kbd">윈도우 키</span>+
|
||||
<span className="kbd">←</span>를 눌러 화면 왼쪽 절반에 붙이고, 메모장(또는 에디터)을 열어
|
||||
<span className="kbd">윈도우 키</span>+<span className="kbd">→</span>로 오른쪽에 붙여 보세요.
|
||||
이 <strong>화면 분할</strong>이 듀얼 모니터의 축소판이에요. 왼쪽 글을 보며 오른쪽에
|
||||
요약을 적어 보면 — Alt+Tab이 사라진 세상이 얼마나 편한지 3분 만에 느껴집니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="USB 규격 정리" sub="C 모양이라고 다 같은 C가 아니다">
|
||||
<p>
|
||||
USB는 이름 정책이 여러 번 바뀌면서 개발자들도 헷갈리는 동네가 됐어요.
|
||||
딱 하나만 기억하면 안개가 걷힙니다 — <strong>"모양"과 "속도"는 완전히 별개</strong>라는 것.
|
||||
USB-C는 <strong>커넥터 모양</strong> 이름이고, USB 3.x니 USB4니 하는 건
|
||||
<strong> 속도 규격</strong> 이름이에요. 겉은 같은 타원 구멍인데 속도는 제각각일 수 있죠.
|
||||
</p>
|
||||
<Code>{CODE_USB}</Code>
|
||||
<p>
|
||||
비유하면 USB-C 커넥터는 <strong>도로의 폭</strong>이고 속도 규격은
|
||||
<strong> 제한 속도</strong>예요. 같은 왕복 2차선(같은 모양)이어도 어떤 길은 시속
|
||||
480(2.0), 어떤 길은 40,000(USB4)까지 달릴 수 있는 거죠. 케이블·포트·기기
|
||||
<strong> 셋 중 가장 느린 놈</strong>이 전체 속도를 정한다는 것도 함정 포인트!
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>실무에서 진짜 겪는 일</b> "독에 모니터를 꽂았는데 화면이 안 나와요" —
|
||||
범인은 십중팔구 <strong>충전 전용 C 케이블</strong>이에요. 겉모습은 똑같지만
|
||||
영상 신호(DP Alt Mode)를 못 나르는 케이블이 있습니다. 케이블을 살 땐 모양이 아니라
|
||||
<span className="icode">Gen 숫자</span>와 <span className="icode">영상 출력 지원</span>
|
||||
문구를 읽으세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="허브와 독" sub="케이블 한 가닥으로 자리에 '도킹'하기">
|
||||
<p>
|
||||
요즘 얇은 노트북은 포트가 C 두어 개뿐이라, 모니터·랜선·키보드·마우스를 다 꽂으려면
|
||||
구멍이 모자라요. 이걸 해결하는 물건이 <strong>허브</strong>와 <strong>독(도킹
|
||||
스테이션)</strong>입니다. 허브가 <strong>멀티탭</strong>이라면, 독은 우주선이
|
||||
정거장에 접속하듯 노트북이 자리에 <strong>도킹</strong>하는 기지예요.
|
||||
</p>
|
||||
<Code>{CODE_DOCK}</Code>
|
||||
<p>
|
||||
1번 섹션의 복선 회수 — "노트북인데 자리에선 데스크탑처럼"의 비밀이 바로 이거예요.
|
||||
독에 모니터 두 대·유선 랜·키보드·전원을 상시 연결해 두면, 출근해서 C 케이블
|
||||
<strong> 한 가닥</strong>만 꽂는 순간 완전한 데스크탑 환경이 됩니다. 유선 랜(RJ45)이
|
||||
되는 독이면 WiFi보다 안정적인 연결까지 덤 — 네트워크 코스에서 배운 그 랜선이
|
||||
여기서 다시 등장하죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>고를 때 체크 3가지</b> ① 내 노트북 C포트가 <strong>영상 출력·충전(PD)을 지원</strong>하는지
|
||||
(노트북 스펙표 확인!) ② 독의 <strong>전원 공급 와트(W)</strong>가 내 노트북 충전기 이상인지
|
||||
③ 연결할 <strong>모니터 개수·해상도</strong>를 독이 감당하는지. 6번 섹션에서 배운
|
||||
"모양 말고 스펙 읽기"가 여기서 그대로 쓰입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🖥️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 스펙표의 <strong>U/H 접미사</strong>와 <strong>온보드</strong>의 함정을 읽고,
|
||||
모니터는 <strong>해상도·패널·피벗</strong>으로 고르고, USB는 <strong>모양이 아니라
|
||||
규격</strong>으로 판단하고, 독으로 <strong>케이블 한 가닥 출근</strong>을 설계할 수
|
||||
있어요. 좋은 장비가 준비됐다면 그 위를 달릴 지식을 채울 차례 — 데이터가 어떻게
|
||||
이 장비들 사이를 여행하는지 궁금하다면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스로,
|
||||
바로 코드를 쓰고 싶다면 <Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스로
|
||||
이어가 보세요. 그리고 오늘 배운 걸 써먹을 겸 — 내 PC 스펙을 정리해서 멘토에게
|
||||
공유하는 것을 이번 주 미니 과제로 추천합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
367
frontend/src/pages/courses/PortfolioPage.jsx
Normal file
367
frontend/src/pages/courses/PortfolioPage.jsx
Normal file
@ -0,0 +1,367 @@
|
||||
// 이 파일이 하는 일: "포트폴리오 만들기" 코스 — 포트폴리오를 '결과 자랑'이 아니라
|
||||
// '과정의 이야기'로 다시 정의하고, 수습 8주 산출물을 재료 삼아 개발자/디자이너
|
||||
// 각자의 포트폴리오를 만드는 방법을 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (과정의 이야기 → STAR 틀 → 수습 8주 = 재료 → 개발자 편 → 디자이너 편 → 스크린샷 → 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램·예시는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 예시 상수들 ──
|
||||
|
||||
const CODE_STORY_VS_LIST = `같은 프로젝트, 두 가지 소개 방식
|
||||
|
||||
[나열형 — 읽는 사람 기억에 안 남음]
|
||||
"React와 Spring Boot로 게시판을 만들었습니다.
|
||||
로그인, 글쓰기, 댓글 기능이 있습니다."
|
||||
|
||||
[이야기형 — 읽는 사람이 '같이 일하고 싶다'고 느낌]
|
||||
"댓글이 300개 넘는 글에서 페이지가 3초씩 멈추는 문제(문제)를 만나,
|
||||
개발자 도구로 렌더링 병목을 찾아보고(고민)
|
||||
목록을 페이지 단위로 잘라 불러오게 바꿔(해결)
|
||||
0.4초로 줄였습니다. '처음부터 데이터 양을 상상하며
|
||||
설계해야 한다'는 걸 배웠어요(배움)."
|
||||
|
||||
→ 기능 목록은 검색하면 나와요. 하지만
|
||||
문제 → 고민 → 해결 → 배움 의 이야기는 나만 쓸 수 있습니다.`;
|
||||
|
||||
const CODE_STAR = `STAR — 프로젝트 한 개를 정리하는 4칸 틀
|
||||
|
||||
S (Situation, 상황) 어떤 프로젝트였고, 나는 무슨 역할이었나?
|
||||
T (Task, 과제) 내가 맡은 문제/목표는 정확히 무엇이었나?
|
||||
A (Action, 행동) 나는 '구체적으로' 무엇을 했나? (제일 길게!)
|
||||
R (Result, 결과) 그래서 뭐가 달라졌나? + 뭘 배웠나?
|
||||
|
||||
예시 — 수습 과제 하나를 STAR로:
|
||||
S: 학습 플랫폼 과제로 출석 체크 화면을 맡음 (React, 2인 팀)
|
||||
T: 새로고침하면 체크 상태가 사라지는 버그 해결
|
||||
R: 새로고침해도 상태 유지, 멘토 코드리뷰 통과
|
||||
A: 상태가 어디 저장되는지 추적 → 컴포넌트 안에만 있음을 발견
|
||||
→ 서버 저장 vs 브라우저 저장을 비교 → 서버 API 저장으로 결정
|
||||
|
||||
함정 주의: 다들 S와 R만 쓰고 A를 한 줄로 끝내요.
|
||||
읽는 사람이 궁금한 건 A — "너는 뭘 했는데?" 입니다.`;
|
||||
|
||||
const CODE_8WEEKS = `수습 8주 = 포트폴리오 재료 창고
|
||||
|
||||
주차 생기는 재료 포트폴리오에서의 쓰임
|
||||
──────────────────────────────────────────────────────────
|
||||
1~2주차 과제집 산출물 (미니 실습들) "기초를 이렇게 다졌다" 증거
|
||||
3~6주차 티켓 (실제 이슈 처리 기록) "실무 문제를 이렇게 풀었다" 사례
|
||||
7~8주차 종합 프로젝트 대표작 후보 1순위!
|
||||
매주 회고·멘토 피드백 '배움(R)' 칸을 채울 원석
|
||||
|
||||
→ 즉, 포트폴리오를 "나중에 몰아서" 만드는 게 아니라
|
||||
지금 하는 과제 하나하나가 이미 포트폴리오의 한 페이지예요.
|
||||
오늘 닫은 티켓의 번호와 한 줄 메모만 남겨도 반은 완성입니다.`;
|
||||
|
||||
const CODE_README = `좋은 프로젝트 README의 뼈대 (Gitea/GitHub 공통)
|
||||
|
||||
# 프로젝트 이름 — 한 줄 소개
|
||||
"무엇을 왜 만들었는지" 한 문장. (예: 출석 체크를 3초→즉시로)
|
||||
|
||||
## 스크린샷 또는 데모 GIF
|
||||
글보다 먼저 화면. 읽는 사람은 3초 안에 떠나요.
|
||||
|
||||
## 내가 한 일 (팀 프로젝트라면 특히!)
|
||||
- 출석 API 설계·구현 (Spring Boot)
|
||||
- 상태 저장 버그 해결 (아래 트러블슈팅 참고)
|
||||
|
||||
## 트러블슈팅 — 이 프로젝트의 하이라이트
|
||||
문제 → 원인 → 해결 을 짧게. STAR의 A를 여기에!
|
||||
|
||||
## 실행 방법
|
||||
docker compose up 한 줄이면 최고. 안 되면 단계별로.
|
||||
|
||||
## 기술 스택
|
||||
React · Spring Boot · PostgreSQL · Docker · AWS EC2`;
|
||||
|
||||
const CODE_DESIGNER_CASE = `디자이너 포트폴리오 — 한 작품의 케이스 스터디 구성
|
||||
|
||||
1) 커버 결과물 대표 이미지 + 프로젝트 한 줄 소개
|
||||
2) 배경 누구를 위해, 왜 이 디자인이 필요했나 (S·T)
|
||||
3) 과정 레퍼런스 조사 → 스케치 → 시안 A/B → 선택 이유 (A!)
|
||||
4) 결과 최종 화면들 + 전/후 비교
|
||||
5) 회고 다시 한다면 뭘 다르게 할까 (R)
|
||||
|
||||
버린 시안도 재료예요:
|
||||
"시안 A는 예뻤지만 버튼이 안 보여서 B를 골랐다"
|
||||
→ 이 한 줄이 '감각'이 아니라 '판단'을 보여줍니다.
|
||||
읽는 사람이 궁금한 건 최종본이 아니라 왜 그렇게 골랐는가!`;
|
||||
|
||||
const CODE_SCREENSHOT = `스크린샷 체크리스트 — 찍기 전 30초 점검
|
||||
|
||||
□ 진짜 데이터처럼 보이는가?
|
||||
"ㅁㄴㅇㄹ", "test123" 대신 그럴듯한 이름·글 제목 넣기
|
||||
□ 개인정보가 없는가?
|
||||
실명·이메일·토큰이 화면에 있으면 가리거나 더미로 교체
|
||||
□ 브라우저 잡동사니를 잘랐는가?
|
||||
북마크바·탭 20개·알림 배지는 작품의 일부가 아님
|
||||
□ 핵심이 한눈에 보이는가?
|
||||
전체 화면 1장보다, 자랑할 부분을 크게 자른 1장이 낫다
|
||||
□ 흐름이 이어지는가?
|
||||
"클릭 전 → 클릭 후" 2장 세트가 긴 설명보다 강력
|
||||
|
||||
Windows 단축키:
|
||||
Win + Shift + S 원하는 영역만 골라 캡처 (가장 추천!)
|
||||
Alt + PrtSc 지금 활성 창만 캡처
|
||||
Win + G 화면 녹화 (움직이는 기능 시연용)`;
|
||||
|
||||
const CODE_PRACTICE = `실습 양식 — 그대로 복사해서 채워 보세요
|
||||
|
||||
[1단계] 산출물 전수조사 (15분)
|
||||
────────────────────────────────
|
||||
번호 산출물 어디에 있나 대표작 후보?
|
||||
1 (예) 출석 체크 과제 Gitea 저장소 ○○ ★
|
||||
2 (예) 버그 티켓 #41 이슈 트래커
|
||||
3 ...
|
||||
→ 사소해 보여도 일단 다 적기. 고르는 건 다음 단계에서.
|
||||
|
||||
[2단계] 대표작 1개 스토리 초안 (25분)
|
||||
────────────────────────────────
|
||||
제목(한 줄):
|
||||
S 상황:
|
||||
T 과제:
|
||||
A 행동: ← 최소 3문장! "무엇을 시도했고, 왜 그 방법을 골랐나"
|
||||
R 결과와 배움:
|
||||
첨부할 스크린샷 2장: ① 문제 상황 ② 해결된 화면
|
||||
|
||||
다 썼으면 → 멘토에게 공유하고 피드백 받기.
|
||||
"A가 더 궁금해요"라는 말을 들으면 정상 궤도입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '과정의 이야기' },
|
||||
{ n: 2, label: 'STAR 정리 틀' },
|
||||
{ n: 3, label: '수습 8주 = 재료' },
|
||||
{ n: 4, label: '개발자 편' },
|
||||
{ n: 5, label: '디자이너 편' },
|
||||
{ n: 6, label: '스크린샷 잘 찍기' },
|
||||
{ n: 7, label: '실습: 대표작 초안' },
|
||||
];
|
||||
|
||||
export default function PortfolioPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 무엇을 바꿔 주는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>포트폴리오 만들기<br />— 결과가 아니라 과정을 파는 법</h1>
|
||||
<p>
|
||||
포트폴리오는 완성작 전시회가 아니라, <strong>내가 문제를 어떻게 푸는 사람인지</strong>를
|
||||
보여주는 이야기책이에요. 이 코스에서는 수습 8주 동안 이미 쌓인 재료로
|
||||
그 이야기책의 첫 장을 직접 써 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 포함</span>
|
||||
<span className="chip">준비물: 지금까지의 과제들</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="포트폴리오는 '과정의 이야기'다" sub="완성작 사진첩이 아니라, 문제를 푸는 사람의 다큐멘터리">
|
||||
<p>
|
||||
요리 오디션 프로그램을 떠올려 보세요. 심사위원의 마음을 움직이는 건 완성된 접시
|
||||
사진 한 장이 아니라, <strong>재료가 없을 때 어떻게 대처했고 실패한 소스를 어떻게
|
||||
살려냈는지</strong>의 과정이에요. 포트폴리오도 똑같습니다. 보는 사람(선배 개발자,
|
||||
디자이너, 면접관)이 진짜 궁금한 건 "무엇을 만들었나"보다
|
||||
<strong> "문제를 만났을 때 어떻게 움직이는 사람인가"</strong>예요.
|
||||
</p>
|
||||
<p>
|
||||
그래서 모든 프로젝트 소개는 네 박자로 씁니다 —
|
||||
<strong> 문제 → 고민 → 해결 → 배움</strong>. 아래 두 소개를 비교해 보세요.
|
||||
</p>
|
||||
<Code>{CODE_STORY_VS_LIST}</Code>
|
||||
<div className="warn">
|
||||
<b>흔한 착각</b> — "대단한 걸 만들어야 포트폴리오가 된다"는 생각이요.
|
||||
반대예요. 작은 버그 하나라도 <strong>내가 왜, 어떻게 풀었는지</strong> 설명할 수
|
||||
있으면 훌륭한 재료고, 화려한 결과물이라도 과정을 설명 못 하면 남의 옷 같아 보입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="프로젝트 정리 틀 — STAR" sub="어떤 프로젝트든 4칸에 넣으면 이야기가 된다">
|
||||
<p>
|
||||
"과정을 이야기로 쓰라"는 말이 막막하죠? 그래서 틀이 있어요. 면접·자기소개서에서도
|
||||
쓰이는 <strong>STAR</strong>입니다. 상황(S)과 과제(T)로 무대를 깔고,
|
||||
행동(A)으로 주인공(나)이 움직이고, 결과(R)로 막을 내리는 — 사실상
|
||||
<strong> 모든 드라마의 구조</strong>예요.
|
||||
</p>
|
||||
<Code>{CODE_STAR}</Code>
|
||||
<p>
|
||||
핵심은 <strong>A(행동)가 주인공</strong>이라는 것. "팀으로 만들었습니다"가 아니라
|
||||
"나는 이 부분에서 두 가지 방법을 비교해 이걸 골랐습니다"까지 내려가야
|
||||
읽는 사람이 <em>나</em>를 볼 수 있어요. 팀 프로젝트일수록 더 그래요 —
|
||||
단체 사진 속에서 나를 찾으려면 내가 어디 서 있는지 표시해 줘야 하니까요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 가장 최근에 끝낸 과제 하나를 골라, 종이에 S/T/A/R 네 줄만
|
||||
써 보세요. 5분이면 됩니다. A가 한 줄밖에 안 나온다면 — 그 과제를 하며
|
||||
"뭘 시도했다가 버렸는지"를 떠올려 보세요. 버린 시도가 곧 A의 재료예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="수습 8주가 곧 포트폴리오 재료다" sub="나중에 몰아서 만드는 게 아니라, 지금 쌓이고 있다">
|
||||
<p>
|
||||
"포트폴리오 만들 시간이 없어요"는 절반만 맞는 말이에요. 만들 <strong>시간</strong>은
|
||||
따로 필요하지만, <strong>재료</strong>는 이미 매일 생기고 있거든요. 우리 수습 과정의
|
||||
산출물을 재료 창고로 보면 이렇게 정리됩니다.
|
||||
</p>
|
||||
<Code>{CODE_8WEEKS}</Code>
|
||||
<p>
|
||||
농사와 같아요. 수확(포트폴리오 완성)은 마지막이지만, 씨 뿌리기(기록)는 지금이에요.
|
||||
티켓을 닫을 때 <strong>"무엇이 문제였고 어떻게 풀었는지 두 줄"</strong>만 이슈에
|
||||
남겨 두면, 8주 뒤의 나는 과거의 나에게 큰절을 하게 됩니다. 기억은 반드시
|
||||
휘발되거든요 — 특히 "고민(A)" 부분이 제일 먼저 사라져요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 우리 Gitea(<span className="icode">edu.awesomedevapp.com</span>)에
|
||||
로그인해서 내가 올린 커밋과 닫은 이슈 목록을 열어 보세요. 그중 "이건 좀 고생했는데"
|
||||
싶은 것 1개를 골라, 그 이슈에 <strong>문제/해결 두 줄 코멘트</strong>를 지금 달아 보세요.
|
||||
방금 포트폴리오 한 조각을 저장한 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="개발자 포트폴리오 — 저장소가 곧 얼굴" sub="잔디는 성실함을, README는 사고력을 보여준다">
|
||||
<p>
|
||||
개발자의 포트폴리오는 별도의 예쁜 사이트가 아니어도 됩니다.
|
||||
<strong> Gitea/GitHub 프로필 자체</strong>가 포트폴리오예요. 보는 사람의 시선 순서는
|
||||
보통 이래요 — ① 잔디(활동 그래프) ② 고정된 대표 저장소 ③ 그 저장소의 README.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>잔디(커밋 그래프)</strong>는 출석부예요. 하루에 몰아서 100커밋보다,
|
||||
매일 조금씩 초록 칸이 이어지는 쪽이 "꾸준한 사람"으로 읽힙니다. 커밋 메시지도
|
||||
기록의 일부 — <span className="icode">fix</span> 한 단어보다
|
||||
<span className="icode">fix: 새로고침 시 출석 상태 유실 수정</span>처럼 쓰면
|
||||
그 자체가 이야기가 돼요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>README</strong>는 저장소의 현관문이에요. 코드를 한 줄도 안 읽고도
|
||||
"이 사람이 뭘 왜 어떻게 만들었는지" 알 수 있어야 합니다.
|
||||
</li>
|
||||
<li>
|
||||
<strong>트러블슈팅 문단</strong>이 진짜 승부처 — 섹션 2에서 쓴 STAR의 A를
|
||||
여기 옮겨 담으세요.
|
||||
</li>
|
||||
</ol>
|
||||
<Code>{CODE_README}</Code>
|
||||
<div className="warn">
|
||||
<b>올리기 전에 꼭!</b> — 저장소를 공개하기 전에 <strong>비밀번호·API 키·
|
||||
<span className="icode">.env</span> 파일</strong>이 커밋에 섞여 있지 않은지 확인하세요.
|
||||
한 번 커밋된 비밀은 삭제해도 이력에 남습니다. 회사 코드는 공개 저장소에 올리지 않는 게
|
||||
원칙이에요 — 공개용은 개인 연습 프로젝트로, 회사 작업은 사내 Gitea에서 링크로.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="디자이너 포트폴리오 — 케이스 스터디로 말하기" sub="비핸스는 전시장, 노션은 서재">
|
||||
<p>
|
||||
디자이너는 결과물이 눈에 보이니 오히려 함정에 빠지기 쉬워요 — 예쁜 최종본만
|
||||
주르륵 올리는 것. 하지만 섹션 1에서 배웠죠? 보는 사람이 궁금한 건
|
||||
<strong> 왜 그렇게 디자인했는가</strong>입니다. 그래서 작품 하나를
|
||||
<strong> 케이스 스터디</strong>(사건 기록) 형식으로 폅니다.
|
||||
</p>
|
||||
<Code>{CODE_DESIGNER_CASE}</Code>
|
||||
<p>
|
||||
어디에 올릴까요? <strong>비핸스</strong>는 갤러리형 전시장 — 이미지 중심이라
|
||||
첫인상이 강하고, 다른 디자이너들에게 노출되기 좋아요. <strong>노션</strong>은
|
||||
서재형 — 글·이미지·링크를 자유롭게 섞을 수 있어 과정 설명이 긴 케이스 스터디에
|
||||
유리하고, 링크 하나로 공유가 끝나죠. 둘 중 하나를 고르기보다
|
||||
<strong> 비핸스에 전시하고 노션에 과정을 쌓는</strong> 조합이 흔한 정답입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>개발자도 남 얘기가 아니에요</b> — 우리 학습 플랫폼 과제에서 화면을 만들었다면
|
||||
"왜 버튼을 여기 뒀는지" 한 줄 설명하는 순간 그게 케이스 스터디예요. 반대로
|
||||
디자이너도 Gitea에 시안 파일과 결정 기록을 커밋하면 잔디가 자랍니다.
|
||||
두 세계는 생각보다 붙어 있어요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="스크린샷 잘 찍는 법" sub="같은 작업물도 사진이 절반이다">
|
||||
<p>
|
||||
중고 거래 앱에서 같은 물건이라도 사진이 흐릿하면 안 팔리죠. 포트폴리오의
|
||||
스크린샷도 똑같아요 — <strong>작업물의 증명사진</strong>입니다. 찍기 전에
|
||||
아래 체크리스트로 30초만 점검하세요.
|
||||
</p>
|
||||
<Code>{CODE_SCREENSHOT}</Code>
|
||||
<p>
|
||||
한 가지 더 — 움직이는 기능(드래그, 실시간 갱신 등)은 정지 사진으론 전달이 안 돼요.
|
||||
짧은 <strong>화면 녹화 → GIF</strong>로 만들면 README 맨 위에서 3초 만에
|
||||
시선을 잡습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 바로 <span className="kbd">Win</span> +
|
||||
<span className="kbd">Shift</span> + <span className="kbd">S</span>를 눌러
|
||||
이 페이지의 섹션 하나를 영역 캡처해 보세요. 그리고 체크리스트에 비춰 보기 —
|
||||
잡동사니 없이 핵심만 담겼나요? 이 감각이 손에 붙으면 캡처가 습관이 됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습 — 산출물 목록화 + 대표작 스토리 초안" sub="오늘 40분, 포트폴리오의 첫 페이지">
|
||||
<p>
|
||||
이제 배운 걸 전부 모아 손을 움직일 시간이에요. 목표는 두 가지 —
|
||||
<strong> ① 내 재료 창고의 재고 조사</strong>,
|
||||
<strong> ② 대표작 1개의 스토리 초안</strong>. 완벽하게 쓰려고 하지 마세요.
|
||||
초안은 원래 못생긴 게 정상이고, 못생긴 초안이 없는 사람은 완성본도 없습니다.
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE}</Code>
|
||||
<ol className="olist">
|
||||
<li>과제집 산출물·닫은 티켓·진행 중인 종합 프로젝트를 <strong>빠짐없이</strong> 표에 적어요. (섹션 3의 창고 조사)</li>
|
||||
<li>그중 "고생한 이야기가 있는" 것 하나에 ★을 칩니다 — 화려한 것 말고 <strong>이야기가 있는 것</strong>.</li>
|
||||
<li>★ 과제를 STAR 4칸으로 풀어 씁니다. A는 최소 3문장! (섹션 2)</li>
|
||||
<li>첨부할 스크린샷 2장을 체크리스트대로 찍어요. (섹션 6)</li>
|
||||
<li>개발자는 해당 저장소 README에, 디자이너는 노션 페이지에 초안을 옮겨 담고 멘토에게 링크를 공유합니다.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>제출 기준은 하나</b> — 멘토가 읽고 "그래서 그 다음엔 어떻게 했어요?"라고
|
||||
물어보고 싶어지면 성공이에요. 질문이 생기는 글이 좋은 포트폴리오 글입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📁 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 포트폴리오가 <strong>결과 전시가 아니라 과정의 이야기</strong>라는 것,
|
||||
그 이야기를 <strong>STAR 틀</strong>에 담는 법, 그리고 수습 8주의 산출물이
|
||||
이미 재료라는 걸 알게 됐어요. 오늘 쓴 초안은 종합 프로젝트가 끝날 때마다 한 장씩
|
||||
늘려 가세요. 대표작을 더 단단하게 만들고 싶다면{' '}
|
||||
<Link to="/learn/git"><strong>Git과 협업</strong></Link> 코스에서 커밋 메시지와
|
||||
저장소 관리 습관을, <Link to="/learn/coding"><strong>코딩 기초</strong></Link>{' '}
|
||||
코스에서 트러블슈팅에 쓸 실력을 이어서 다져 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
456
frontend/src/pages/courses/ReactIntroPage.jsx
Normal file
456
frontend/src/pages/courses/ReactIntroPage.jsx
Normal file
@ -0,0 +1,456 @@
|
||||
// 이 파일이 하는 일: "React 입문" 코스 — 화면이 왜 '상태의 함수'인지에서 출발해
|
||||
// 컴포넌트·JSX·props/state·이벤트·목록 렌더링·useEffect까지, 우리 학습 플랫폼
|
||||
// 프론트엔드 코드를 실제 교재 삼아 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
// (이 파일 자체가 React 컴포넌트라서, "React를 설명하는 React 코드"라는 재귀 개그가 성립해요.)
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 예제 코드 상수들 ──
|
||||
|
||||
const CODE_STATE_FN = `React의 핵심 아이디어 딱 한 줄:
|
||||
|
||||
화면(UI) = f(상태)
|
||||
|
||||
옛날 방식 (직접 조작): React 방식 (선언):
|
||||
────────────────────── ──────────────────────
|
||||
"버튼 눌리면 저 글자를 "count가 3이면 화면엔
|
||||
찾아서 3으로 바꿔" '3'이 보여야 해" 라고
|
||||
(내가 화면을 일일이 수정) 선언만 하면, 바뀐 부분은
|
||||
React가 알아서 갱신
|
||||
|
||||
비유: 옛날 방식은 칠판에 쓴 글씨를 지우개로 지우고 다시 쓰는 것.
|
||||
React는 "정답지(상태)"만 고치면 칠판(화면)이 스스로 따라 바뀌는 마법 칠판.`;
|
||||
|
||||
const CODE_COMPONENT = `// 컴포넌트 = 내가 만든 나만의 HTML 태그 (레고 블록 한 개)
|
||||
function Badge({ label }) {
|
||||
return <span className="badge">{label}</span>;
|
||||
}
|
||||
|
||||
// 한 번 만들면 어디서든 조립!
|
||||
function ProfileCard() {
|
||||
return (
|
||||
<div className="card">
|
||||
<h3>김미림</h3>
|
||||
<Badge label="3학년" />
|
||||
<Badge label="프론트엔드" /> {/* 같은 블록, 다른 글자 */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 우리 플랫폼 화면도 전부 이런 블록들의 조립이에요:
|
||||
// <App> → <Layout> → <DashboardPage> → <Card> → <Badge> ...`;
|
||||
|
||||
const CODE_JSX_RULES = `JSX 필수 규칙 4가지 — 처음에 100% 한 번씩 틀리는 것들!
|
||||
|
||||
1) 태그는 반드시 닫는다
|
||||
<img src="..."> ❌ HTML에선 됐지만
|
||||
<img src="..." /> ✅ JSX에선 셀프 클로징 필수
|
||||
|
||||
2) class 대신 className
|
||||
<div class="card"> ❌ class는 자바스크립트 예약어
|
||||
<div className="card"> ✅
|
||||
|
||||
3) 중괄호 { } = 자바스크립트 구멍
|
||||
<p>안녕, {name}님! 내년엔 {grade + 1}학년</p>
|
||||
→ 따옴표 안이 아니라 중괄호 안이 '살아있는 코드'
|
||||
|
||||
4) 형제 태그는 하나로 감싼다
|
||||
return <h1>제목</h1><p>내용</p> ❌ 둘을 동시에 반환 불가
|
||||
return <><h1>제목</h1><p>내용</p></> ✅ <>...</> (Fragment)로 포장`;
|
||||
|
||||
const CODE_PROPS_STATE = `props — 부모가 자식에게 주는 선물 state — 내가 관리하는 내 소지품
|
||||
───────────────────────────── ─────────────────────────────
|
||||
부모 → 자식, 한 방향으로만 전달 컴포넌트 자신이 만들고 바꿈
|
||||
받은 쪽에서 바꿀 수 없음(읽기 전용) 바꾸면 화면이 다시 그려짐!
|
||||
"이름표에 이 글자를 써 줘" "지금 몇 번 클릭됐는지 기억해 줘"
|
||||
|
||||
// props: 부모가 값을 내려 줌
|
||||
<Badge label="3학년" />
|
||||
|
||||
// state: useState로 내 상태 만들기
|
||||
import { useState } from 'react';
|
||||
|
||||
function LikeButton() {
|
||||
const [count, setCount] = useState(0); // [현재값, 바꾸는 함수]
|
||||
|
||||
return (
|
||||
<button onClick={() => setCount(count + 1)}>
|
||||
좋아요 {count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
⚠ count = count + 1 처럼 직접 바꾸면 화면이 안 바뀌어요.
|
||||
반드시 setCount()를 불러야 React가 "아, 다시 그려야겠구나" 알아챕니다.`;
|
||||
|
||||
const CODE_EVENTS = `// 이벤트 처리 — HTML의 onclick과 비슷하지만, 문자열이 아니라 '함수'를 건네요
|
||||
function LoginForm() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault(); // 폼의 기본 동작(새로고침) 막기
|
||||
alert(email + ' 확인!');
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="이메일"
|
||||
/>
|
||||
<button type="submit">확인</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// 포인트: onClick={handleSubmit} ✅ 함수를 '건네줌' (눌리면 실행해 줘)
|
||||
// onClick={handleSubmit()} ❌ 지금 당장 '실행'해 버림 (초보 함정 1위!)`;
|
||||
|
||||
const CODE_LIST_KEY = `// 목록 렌더링 — 배열.map()으로 데이터를 JSX 블록으로 '변신'시키기
|
||||
const courses = [
|
||||
{ id: 1, title: '네트워크의 이해' },
|
||||
{ id: 2, title: 'React 입문' },
|
||||
{ id: 3, title: 'Docker 첫걸음' },
|
||||
];
|
||||
|
||||
function CourseList() {
|
||||
return (
|
||||
<ul>
|
||||
{courses.map((c) => (
|
||||
<li key={c.id}>{c.title}</li> // ← key 필수!
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// key는 왜? React가 목록을 다시 그릴 때 "누가 누구인지" 알아보는 이름표예요.
|
||||
// 출석부에 이름 없이 '앞에서 3번째 애'로만 부르면, 자리만 바뀌어도
|
||||
// 다 헷갈리죠. 학번(id)으로 부르면 자리를 바꿔도 정확히 그 사람.
|
||||
// 그래서 key={index}(자리 번호)보다 key={c.id}(학번)가 좋습니다.`;
|
||||
|
||||
const CODE_USE_EFFECT = `// useEffect — "화면을 그린 다음에 이 일을 해 줘" (우리 대시보드 실제 패턴)
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function DashboardPage() {
|
||||
const [stats, setStats] = useState(null); // ① 처음엔 데이터 없음
|
||||
|
||||
useEffect(() => { // ② 화면이 먼저 그려진 '뒤에'
|
||||
fetch('/api/dashboard/stats') // 서버에 데이터 요청
|
||||
.then((res) => res.json())
|
||||
.then((data) => setStats(data)); // ③ 도착하면 state에 저장
|
||||
}, []); // ④ [] = 처음 한 번만 실행
|
||||
|
||||
if (!stats) return <p>불러오는 중...</p>; // ⑤ 데이터 오기 전 화면
|
||||
return <p>이번 주 학습 {stats.hours}시간!</p>; // ⑥ 도착 후 자동 재렌더!
|
||||
|
||||
// 흐름: 빈 화면 렌더 → 요청 출발 → 응답 도착 → setStats →
|
||||
// "상태가 바뀌었네?" → React가 화면을 다시 그림. 끝.
|
||||
}
|
||||
|
||||
// 마지막 [](의존성 배열)이 실행 타이밍 스위치:
|
||||
// [] → 처음 등장할 때 한 번만 (데이터 로딩의 단골)
|
||||
// [userId] → userId가 바뀔 때마다 다시
|
||||
// 없음 → 매 렌더마다 (거의 실수! 무한 요청 폭탄 주의)`;
|
||||
|
||||
const CODE_FOLDER = `우리 프론트엔드 폴더 — 이제 다르게 보일 거예요 (frontend/src/)
|
||||
|
||||
src/
|
||||
├── pages/ ← 화면 한 장 = 컴포넌트 한 개 (지금 이 페이지도 여기!)
|
||||
│ ├── DashboardPage.jsx 섹션 7의 useEffect 패턴이 진짜로 있음
|
||||
│ ├── NetworkPage.jsx "네트워크의 이해" 코스
|
||||
│ └── courses/
|
||||
│ └── ReactIntroPage.jsx ← 지금 읽고 있는 바로 이 파일!
|
||||
├── components/ ← 여러 페이지가 같이 쓰는 레고 블록 (섹션 2의 Badge·Card)
|
||||
├── App.jsx ← react-router-dom으로 "URL → 어느 페이지" 연결
|
||||
└── main.jsx ← React를 index.html에 꽂아 넣는 시동 버튼
|
||||
|
||||
읽는 순서 추천: main.jsx → App.jsx(라우팅) → pages 하나 → 그 안의 components.
|
||||
큰 데서 작은 데로, 조립도 → 블록 순서로 읽으면 길을 잃지 않아요.`;
|
||||
|
||||
const CODE_PRACTICE = `STUDY.md 미니 과제 3종 — 오늘 배운 걸 손으로 굳히기
|
||||
|
||||
과제 A. 나만의 Badge 만들기 (섹션 2·4)
|
||||
components/에 MyBadge.jsx를 만들고, props로 label과 color를
|
||||
받아 표시. 아무 페이지에나 <MyBadge label="연습중" /> 붙여 보기.
|
||||
|
||||
과제 B. 좋아요 버튼 (섹션 4·5)
|
||||
useState + onClick으로 누를 때마다 숫자가 올라가는 버튼.
|
||||
심화: 10 이상이면 "🔥 인기!" 글자가 나타나게 (조건부 렌더링).
|
||||
|
||||
과제 C. 내 코스 목록 (섹션 6)
|
||||
배열 상수를 만들어 .map()으로 <li> 목록 렌더링.
|
||||
key를 일부러 빼고 저장 → 브라우저 콘솔(F12)의 경고를 직접 목격!
|
||||
|
||||
제출: Gitea(edu.awesomedevapp.com)에 브랜치 만들어 커밋 & PR.
|
||||
멘토가 코드 리뷰로 피드백을 답니다 — 실무 흐름 그대로!`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
// (섹션 2에서 배우는 내용을 이 파일이 스스로 실천하고 있어요.)
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '왜 React인가' },
|
||||
{ n: 2, label: '컴포넌트 = 레고' },
|
||||
{ n: 3, label: 'JSX 규칙' },
|
||||
{ n: 4, label: 'props와 state' },
|
||||
{ n: 5, label: '이벤트 처리' },
|
||||
{ n: 6, label: '목록과 key' },
|
||||
{ n: 7, label: 'useEffect 한 입' },
|
||||
{ n: 8, label: '폴더 구조 + 실습' },
|
||||
];
|
||||
|
||||
export default function ReactIntroPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 소프트웨어</div>
|
||||
<h1>React 입문<br />— 화면은 상태의 함수다</h1>
|
||||
<p>
|
||||
지금 보고 있는 이 학습 플랫폼의 화면 전부가 React로 만들어졌어요.
|
||||
"화면 = 상태의 함수"라는 한 문장에서 출발해, 컴포넌트·props·state·useEffect까지 —
|
||||
<strong> 우리 코드베이스를 교재 삼아</strong> React의 뼈대를 세워 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 90분</span>
|
||||
<span className="chip">실습 과제 3개</span>
|
||||
<span className="chip">선수 지식: HTML·JS 기초</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="왜 React인가" sub="화면 = f(상태), 이 한 줄이 전부">
|
||||
<p>
|
||||
자바스크립트만으로도 화면을 바꿀 수 있어요. 문제는 <strong>규모</strong>입니다.
|
||||
버튼 하나가 화면 다섯 군데를 바꿔야 한다면? "어느 글자를 찾아서, 뭘로 바꾸고,
|
||||
저 목록도 갱신하고..." — 화면을 <strong>직접 조작</strong>하는 코드는 페이지가
|
||||
커질수록 스파게티가 돼요.
|
||||
</p>
|
||||
<p>
|
||||
React의 답은 발상의 전환이에요. 화면을 조작하지 말고, <strong>"상태가 이러면
|
||||
화면은 이렇게 생겨야 한다"고 선언만 하자.</strong> 상태(데이터)가 바뀌면 React가
|
||||
바뀐 부분만 골라 다시 그려 줍니다. 우리는 정답지만 고치면 돼요.
|
||||
</p>
|
||||
<Code>{CODE_STATE_FN}</Code>
|
||||
<div className="tip">
|
||||
<b>미리 보는 큰 그림</b> 이 코스 전체가 이 한 줄의 각주예요.
|
||||
컴포넌트(2)는 f를 쪼개는 법, state(4)는 '상태' 그 자체,
|
||||
useEffect(7)는 상태를 서버에서 채우는 법입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="컴포넌트 = 레고 블록" sub="우리 플랫폼의 Badge·Card가 살아있는 실례">
|
||||
<p>
|
||||
React 앱은 거대한 한 덩어리가 아니라 <strong>레고 블록(컴포넌트)의 조립</strong>이에요.
|
||||
컴포넌트는 "JSX를 반환하는 자바스크립트 함수" — 즉, <strong>내가 만든 나만의
|
||||
HTML 태그</strong>입니다. 한 번 만들면 <span className="icode"><Badge /></span>처럼
|
||||
몇 번이고 꽂아 쓸 수 있죠.
|
||||
</p>
|
||||
<Code>{CODE_COMPONENT}</Code>
|
||||
<p>
|
||||
멀리 갈 것 없이 <strong>우리 플랫폼이 실례</strong>예요. 대시보드의 코스 카드,
|
||||
"수강 중" 뱃지, 위쪽 내비게이션 — 전부{' '}
|
||||
<span className="icode">frontend/src/components/</span>에 사는 블록들이고,
|
||||
페이지마다 재조립되고 있을 뿐입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 에디터로{' '}
|
||||
<span className="icode">frontend/src/components/</span> 폴더를 열어
|
||||
<span className="icode"> Badge.jsx</span>와 <span className="icode">Card.jsx</span>를
|
||||
읽어 보세요. "함수 하나 + return 속 JSX" 구조가 위 예제와 똑같은지 확인!
|
||||
그리고 대시보드 화면에서 그 블록이 몇 번 재사용되는지 세어 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="JSX 규칙" sub="HTML을 닮았지만 HTML이 아니다">
|
||||
<p>
|
||||
컴포넌트가 반환하는 <span className="icode"><div>...</div></span> 부분이{' '}
|
||||
<strong>JSX</strong>예요. HTML처럼 생겼지만 사실은 <strong>자바스크립트 속에 태그를
|
||||
쓰게 해 주는 특수 문법</strong>이라, HTML과 미묘하게 다른 규칙이 몇 개 있어요.
|
||||
이걸 모르면 첫날부터 빨간 에러의 늪에 빠집니다.
|
||||
</p>
|
||||
<Code>{CODE_JSX_RULES}</Code>
|
||||
<p>
|
||||
특히 3번, <strong>중괄호</strong>가 JSX의 심장이에요. 따옴표 안 글자는 그냥 글자지만
|
||||
중괄호 안은 <strong>살아있는 자바스크립트</strong> — 변수·계산식·함수 호출이 전부
|
||||
들어갑니다. "정적인 문서(HTML)"와 "살아있는 코드(JS)"를 잇는 창문인 셈이죠.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>에러 메시지 번역기</b> <span className="icode">Adjacent JSX elements must be
|
||||
wrapped...</span>가 뜨면 4번 규칙 위반(형제 태그를 안 감쌈),{' '}
|
||||
<span className="icode">Unexpected token</span>이 태그 근처에서 뜨면 대개 1번(안 닫은
|
||||
태그)이에요. 이 두 개만 알아도 초반 에러의 절반은 해결됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="props와 state" sub="부모가 준 선물 vs 내가 관리하는 소지품">
|
||||
<p>
|
||||
컴포넌트를 움직이는 데이터는 딱 두 종류예요. <strong>props</strong>는 부모 컴포넌트가
|
||||
자식에게 내려 주는 <strong>선물</strong> — 받아서 쓸 수는 있지만 내 마음대로 바꿀 수
|
||||
없어요(선물에 낙서하면 안 되죠). <strong>state</strong>는 컴포넌트가 스스로 갖고
|
||||
관리하는 <strong>소지품</strong> — 바꿀 수 있고, 바꾸는 순간 화면이 다시 그려집니다.
|
||||
</p>
|
||||
<Code>{CODE_PROPS_STATE}</Code>
|
||||
<p>
|
||||
<span className="icode">useState(0)</span>이 돌려주는 건 <strong>[현재값, 바꾸는
|
||||
함수]</strong> 쌍이에요. 값을 직접 고치지 않고 굳이 함수를 거치는 이유는 딱 하나 —
|
||||
그 함수를 불러야 React가 <strong>"상태가 바뀌었으니 화면을 다시 그리자"</strong>는
|
||||
신호를 받기 때문입니다. 섹션 1의 "마법 칠판"을 울리는 종이 바로{' '}
|
||||
<span className="icode">setCount</span>예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>구분 공식</b> "이 값을 부모가 정해 주나?" → props.
|
||||
"이 값이 사용자 행동(클릭·입력)으로 변하나?" → state.
|
||||
우리 플랫폼의 코스 카드로 치면 코스 제목은 props, "찜" 하트의 켜짐/꺼짐은 state.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="이벤트 처리" sub="클릭·입력에 반응하기 — 함수를 '건네주는' 감각">
|
||||
<p>
|
||||
버튼 클릭, 글자 입력 같은 사용자 행동을 <strong>이벤트</strong>라고 해요.
|
||||
React에선 <span className="icode">onClick</span>·<span className="icode">onChange</span>{' '}
|
||||
같은 속성에 <strong>함수 자체를 건네줍니다</strong>. "지금 실행해"가 아니라
|
||||
"이 일이 생기면 <strong>그때</strong> 실행해 줘" — 비상벨 옆에 매뉴얼을 걸어 두는
|
||||
것과 같아요. 벨이 울릴 때 읽는 거지, 걸자마자 읽는 게 아니죠.
|
||||
</p>
|
||||
<Code>{CODE_EVENTS}</Code>
|
||||
<p>
|
||||
입력창 패턴도 눈여겨보세요. <span className="icode">value</span>는 state에서 오고,{' '}
|
||||
<span className="icode">onChange</span>가 state를 갱신하고, 갱신된 state가 다시
|
||||
화면에 — 데이터가 <strong>한 방향으로 도는 순환</strong>이에요. 우리 플랫폼의
|
||||
로그인 화면, 과제 제출 폼이 전부 이 패턴 하나로 만들어져 있습니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>초보 함정 1위</b> <span className="icode">onClick={'{handleSubmit()}'}</span>처럼
|
||||
괄호를 붙이면 <strong>렌더링되는 순간 즉시 실행</strong>돼 버려요. 건네줄 땐 괄호 없이
|
||||
이름만 — <span className="icode">onClick={'{handleSubmit}'}</span>. "실행 결과"가 아니라
|
||||
"함수 그 자체"를 주는 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="목록 렌더링과 key" sub="배열을 화면으로 — 그리고 이름표의 정체">
|
||||
<p>
|
||||
코스 목록, 과제 목록, 랭킹... 실제 화면의 절반은 <strong>목록</strong>이에요.
|
||||
React에선 배열의 <span className="icode">.map()</span>으로 데이터 하나하나를
|
||||
JSX 블록으로 변신시킵니다. "데이터 배열 → 화면 블록 배열"의 공장 라인이라고
|
||||
생각하면 돼요.
|
||||
</p>
|
||||
<Code>{CODE_LIST_KEY}</Code>
|
||||
<p>
|
||||
<span className="icode">key</span>는 장식이 아니라 <strong>React의 출석부</strong>예요.
|
||||
목록이 바뀔 때(추가·삭제·정렬) React는 key로 "이 블록이 아까 그 블록인지"를
|
||||
알아봅니다. key가 없거나 자리 번호(index)면, 순서만 바뀌어도 엉뚱한 항목을
|
||||
다시 그리거나 입력값이 뒤섞이는 유령 버그가 생겨요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 플랫폼에서 <span className="kbd">F12</span> →{' '}
|
||||
<span className="kbd">Console</span> 탭을 열어 두고 코스 목록 페이지를 새로고침해
|
||||
보세요. 경고가 없다면 key가 잘 달린 거예요. 그다음 에디터에서 목록을 그리는
|
||||
컴포넌트를 찾아 <span className="icode">key=</span>에 뭘 넣었는지 확인 — 십중팔구
|
||||
DB가 준 <span className="icode">id</span>일 겁니다. (일부러 key를 지우고 경고를
|
||||
목격하는 건 섹션 8의 과제 C!)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="useEffect 한 입" sub="우리 대시보드가 데이터를 불러오는 진짜 방법">
|
||||
<p>
|
||||
지금까지의 컴포넌트는 자기 안의 데이터만 그렸어요. 그런데 진짜 데이터는{' '}
|
||||
<strong>서버(Spring Boot + PostgreSQL)</strong>에 있죠. "화면을 먼저 그리고,
|
||||
그린 <strong>다음에</strong> 서버에 다녀와서, 도착하면 다시 그린다" — 이
|
||||
타이밍을 관리하는 도구가 <span className="icode">useEffect</span>입니다.
|
||||
</p>
|
||||
<Code>{CODE_USE_EFFECT}</Code>
|
||||
<p>
|
||||
식당에 비유하면: 일단 자리에 앉히고(빈 화면 렌더), 주방에 주문을 넣고(fetch),
|
||||
음식이 나오면 상을 차리는(setStats → 재렌더) 순서예요. 손님을 문밖에 세워 두고
|
||||
음식이 다 될 때까지 기다리게 하지 않는 것 — 그래서 "불러오는 중..." 화면이
|
||||
꼭 필요한 겁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 두 단계로 해부해 봅시다. ① 에디터에서{' '}
|
||||
<span className="icode">frontend/src/pages/DashboardPage.jsx</span>를 열어 위 예제와
|
||||
같은 useEffect + fetch 패턴을 직접 찾아보세요. ② 브라우저에서 대시보드를 열고{' '}
|
||||
<span className="kbd">F12</span> → <span className="kbd">Network</span> 탭 →
|
||||
새로고침 — 화면이 뜨고 <strong>잠깐 뒤에</strong>{' '}
|
||||
<span className="icode">/api/...</span> 요청이 출발하는 순서를 눈으로 확인!
|
||||
"렌더 먼저, 데이터 나중"이 실화임을 목격하게 됩니다.
|
||||
</div>
|
||||
<div className="warn">
|
||||
<b>의존성 배열 [] 을 빼먹으면</b> 렌더될 때마다 fetch → setStats → 재렌더 → 또
|
||||
fetch... <strong>무한 요청 루프</strong>가 됩니다. 서버에 DDoS를 거는 셈이니,
|
||||
useEffect를 쓸 땐 마지막 배열부터 확인하는 습관을!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="폴더 구조 다시 보기 + 실습" sub="이제 우리 코드가 읽힌다 — STUDY.md 과제로 마무리">
|
||||
<p>
|
||||
첫 출근날 봤을 땐 암호 같았을 <span className="icode">frontend/src/</span> 폴더,
|
||||
이제 다시 봅시다. 오늘 배운 개념이 폴더 하나하나에 그대로 대응돼요.
|
||||
</p>
|
||||
<Code>{CODE_FOLDER}</Code>
|
||||
<p>
|
||||
개념은 손으로 굳혀야 내 것이 됩니다. 저장소 루트의 <strong>STUDY.md</strong>에
|
||||
있는 미니 과제 3종으로 마무리하세요 — 전부 오늘 배운 섹션의 직행 응용이에요.
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE}</Code>
|
||||
<p>
|
||||
로컬 실행은 <span className="icode">frontend/</span>에서{' '}
|
||||
<span className="icode">npm install</span> 후 <span className="icode">npm run dev</span> —
|
||||
코드를 저장하는 순간 브라우저가 즉시 갱신되는 것(HMR)도 함께 느껴 보세요.
|
||||
"저장 → 새로고침" 없이 개발하는 이 경험이 React 개발의 일상입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>막혔을 때</b> 에러 메시지를 그대로 복사해 검색하기 전에, 먼저 섹션 3의
|
||||
"에러 메시지 번역기"와 섹션 5의 "초보 함정"을 다시 읽어 보세요. 입문 단계
|
||||
에러의 대부분은 그 안에 있어요. 그래도 막히면 멘토에게 — 단, "어디까지 시도했는지"를
|
||||
함께 들고 가면 배우는 속도가 두 배가 됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>⚛️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 "화면 = 상태의 함수"라는 문장을 컴포넌트·JSX·props/state·이벤트·목록·useEffect라는
|
||||
여섯 개의 단어로 설명할 수 있게 됐어요. 그리고 무엇보다 — <strong>우리 플랫폼의
|
||||
프론트 코드가 읽히기 시작했을 겁니다.</strong> STUDY.md 과제 3종을 Gitea에 PR로
|
||||
제출하고, 다음은 그 화면이 부르는 서버 쪽 —{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서 요청이
|
||||
달리는 길을, 이어지는 백엔드 코스에서 응답을 만드는 Spring Boot를 만나 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
374
frontend/src/pages/courses/ServerAnatomyPage.jsx
Normal file
374
frontend/src/pages/courses/ServerAnatomyPage.jsx
Normal file
@ -0,0 +1,374 @@
|
||||
// 이 파일이 하는 일: "서버란 무엇인가" 코스 — 서버가 특별한 컴퓨터가 아니라
|
||||
// '요청을 기다리는 프로그램'이라는 사실에서 출발해, 웹서버/WAS/DB 3계층과
|
||||
// 우리 회사(AWESOMEDEV)의 실제 구성(Caddy → Spring Boot → PostgreSQL),
|
||||
// 동시 접속 처리와 확장(스케일업/아웃)까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_WHAT_IS_SERVER = `서버(server) = serve(대접하다) + er(하는 자)
|
||||
→ "요청이 오면 응답을 대접하는 프로그램"
|
||||
|
||||
[클라이언트] ── "이 페이지 주세요" (요청) ──> [서버]
|
||||
[클라이언트] <── "여기요, 200 OK" (응답) ── [서버]
|
||||
|
||||
핵심 3가지:
|
||||
1) 서버는 '기계'가 아니라 '역할'이에요. 프로그램이 그 역할을 맡습니다.
|
||||
2) 서버 프로그램은 특정 포트를 열고 하루 종일 '기다리는' 게 일이에요.
|
||||
3) 여러분의 노트북도 서버 프로그램을 켜는 순간 서버가 됩니다.
|
||||
|
||||
식당 비유: 서버 = 주문을 기다리는 직원.
|
||||
손님(클라이언트)이 없으면 그냥 대기 중이고,
|
||||
주문(요청)이 들어오는 순간 일해서 음식(응답)을 내놓습니다.`;
|
||||
|
||||
const CODE_THREE_TIER = `웹 서비스의 3계층 — 식당 한 곳에 비유하면:
|
||||
|
||||
[웹서버] [WAS] [DB]
|
||||
홀 직원 주방 냉장고·창고
|
||||
───────── ───────── ─────────
|
||||
손님 맞이 요리(비즈니스 재료(데이터)
|
||||
주문 접수 로직) 담당 보관·꺼내기
|
||||
완성된 반찬은
|
||||
바로 서빙
|
||||
(정적 파일)
|
||||
|
||||
우리 스택으로 번역하면:
|
||||
웹서버 = Caddy (HTTPS 처리, 요청 안내, 정적 파일 서빙)
|
||||
WAS = Spring Boot (로그인 검사, 데이터 가공, JSON 응답 조립)
|
||||
DB = PostgreSQL (회원·코스·진도 데이터를 표(테이블)로 보관)
|
||||
|
||||
왜 셋으로 나눌까?
|
||||
→ 역할이 다르면 잘하는 도구도 다르고, 하나가 바빠져도
|
||||
그 층만 따로 보강할 수 있기 때문이에요. (섹션 6에서 다시!)`;
|
||||
|
||||
const CODE_OUR_STACK = `여러분이 이 페이지를 여는 순간, 서울 EC2 서버 안에서 벌어지는 일:
|
||||
|
||||
인터넷
|
||||
│ https://edu.awesomedevapp.com (443 포트)
|
||||
▼
|
||||
[Caddy] ── "HTTPS 자물쇠 풀기 + 어디로 보낼지 안내"
|
||||
│
|
||||
├─ /assets/... 같은 정적 파일 요청 → 파일을 바로 건네줌 (React 빌드 결과물)
|
||||
│
|
||||
└─ /api/... 요청 → 뒤의 Spring Boot로 전달
|
||||
▼
|
||||
[Spring Boot] ── "누구지? 로그인 토큰 확인. 무슨 데이터가 필요하지?"
|
||||
│ SQL 질의 (5432 포트)
|
||||
▼
|
||||
[PostgreSQL] ── "요청한 데이터, 표에서 찾아서 여기요"
|
||||
|
||||
응답은 이 길을 거꾸로: DB → Spring Boot(JSON 조립) → Caddy → 브라우저.
|
||||
이 전부가 Docker 컨테이너로 포장되어 AWS EC2(서울) 한 대 위에서 돌아갑니다.
|
||||
코드가 태어나는 곳은 Gitea(edu.awesomedevapp.com의 Git 서버)고요.`;
|
||||
|
||||
const CODE_STATIC_DYNAMIC = `정적 파일 vs 동적 응답 — 자판기와 주방의 차이
|
||||
|
||||
정적(static) 동적(dynamic)
|
||||
────────────────── ──────────────────
|
||||
미리 만들어 둔 파일을 그대로 요청이 올 때마다 그 자리에서
|
||||
건네줌 새로 만들어서 건네줌
|
||||
예) HTML, CSS, JS, 이미지 예) "내 진도율", "내 이름이 든 화면"
|
||||
누가 요청해도 내용이 같음 누가·언제 요청하냐에 따라 다름
|
||||
자판기: 버튼 누르면 그대로 나옴 주방: 주문 듣고 지금 요리함
|
||||
Caddy가 바로 처리 (빠름!) Spring Boot + DB가 처리
|
||||
|
||||
이 코스 페이지의 글 자체는 정적이지만,
|
||||
로그인 후 보이는 "○○님, 진도 40%"는 동적 응답이에요.
|
||||
한 화면 안에 둘이 섞여 있는 게 보통입니다.`;
|
||||
|
||||
const CODE_CONCURRENCY = `동시 접속 — 손님 30명이 한꺼번에 들어오면?
|
||||
|
||||
나쁜 식당: 직원 1명이 1번 손님 요리가 끝날 때까지
|
||||
2번 손님을 쳐다도 안 봄 → 줄이 밀림
|
||||
|
||||
서버의 해법:
|
||||
1) 스레드(thread) — 직원을 여러 명 두기.
|
||||
Spring Boot는 기본으로 요청 처리 직원을 수백 명(스레드 풀)
|
||||
대기시켜 두고, 요청마다 한 명씩 붙입니다.
|
||||
2) 커넥션 풀 — 주방↔창고(DB) 사이 통로를 미리 몇 개 뚫어 두고
|
||||
돌려쓰기. 매번 새로 뚫으면 그게 더 오래 걸리거든요.
|
||||
|
||||
그래도 한계는 있어요:
|
||||
직원(스레드)이 다 바쁘면 새 손님은 대기열에서 기다리고,
|
||||
대기열마저 차면 "지금은 못 받아요"(에러)가 납니다.
|
||||
→ 그때 필요한 게 다음 섹션, '서버 키우기'입니다.`;
|
||||
|
||||
const CODE_SCALING = `서버가 힘들어할 때, 두 가지 처방
|
||||
|
||||
스케일 업(Scale Up) 스케일 아웃(Scale Out)
|
||||
"더 힘센 한 대" "여러 대로 나누기"
|
||||
────────────────── ──────────────────
|
||||
CPU·메모리를 좋은 걸로 교체 같은 서버를 여러 대 두고
|
||||
(EC2라면 인스턴스 타입 변경) 앞에 '교통정리 담당'
|
||||
(로드 밸런서)을 세움
|
||||
식당 비유: 주방장을 식당 비유: 지점을 냄
|
||||
슈퍼 주방장으로 교체
|
||||
장점: 코드 수정 없이 간단 장점: 한 대가 죽어도 나머지가 버팀
|
||||
단점: 비용이 가파르게 늘고, 단점: 서버들끼리 상태(세션 등)를
|
||||
아무리 좋아도 '한 대'의 공유하는 설계가 필요해서
|
||||
한계와 한 대가 죽으면 구조가 복잡해짐
|
||||
전부 멈추는 위험은 그대로
|
||||
|
||||
지금 우리 플랫폼은 EC2 한 대 = 스케일 업으로 충분한 단계.
|
||||
사용자가 늘면 스케일 아웃을 고민하게 되고, 그때 3계층으로
|
||||
나눠 둔 설계(섹션 2)가 빛을 발합니다 — WAS만 여러 대로 늘리면 되니까요.`;
|
||||
|
||||
const CODE_LOCALHOST = `# 내 PC를 서버로 만드는 데 필요한 명령, 단 한 줄:
|
||||
# (mirim-app/frontend 폴더에서)
|
||||
npm run dev
|
||||
|
||||
VITE ready in 400 ms
|
||||
➜ Local: http://localhost:5173/ ← 이 줄이 "저 지금 서버예요" 선언!
|
||||
|
||||
# localhost = "내 PC 자신"을 가리키는 특별한 이름 (IP로는 127.0.0.1)
|
||||
# 5173 = Vite 개발 서버가 열고 기다리는 포트(방 번호)
|
||||
|
||||
# 브라우저에서 http://localhost:5173 접속
|
||||
# → 브라우저(클라이언트)가 내 PC(서버)에 요청
|
||||
# → 같은 컴퓨터 안에서 요청과 응답이 오간 것!
|
||||
|
||||
# 서버가 '기다리는 중'이라는 증거:
|
||||
# 터미널을 그대로 두면 커서가 안 돌아와요. 프로그램이 끝난 게 아니라
|
||||
# 다음 요청을 기다리는 중이거든요. Ctrl+C 를 누르면 서버가 내려가고,
|
||||
# 그 순간 브라우저를 새로고침하면 "연결할 수 없음" — 서버의 생사를
|
||||
# 직접 쥐어 본 겁니다.`;
|
||||
|
||||
const CODE_PORT_CHECK = `# 내 PC에서 지금 '기다리는 중'인 서버들을 구경하기 (PowerShell):
|
||||
netstat -ano | findstr LISTENING
|
||||
|
||||
TCP 0.0.0.0:135 ... LISTENING ...
|
||||
TCP 127.0.0.1:5173 ... LISTENING 1234 ← npm run dev가 켠 서버!
|
||||
|
||||
# LISTENING = "포트 열고 요청 기다리는 중"이라는 뜻.
|
||||
# npm run dev를 켠 상태와 끈 상태에서 각각 실행해서
|
||||
# 5173 줄이 나타났다 사라지는 걸 확인해 보세요.
|
||||
# 맨 끝 숫자(PID)는 그 포트를 잡고 있는 프로그램의 번호예요 —
|
||||
# 작업 관리자 [세부 정보] 탭에서 같은 PID를 찾으면 node.exe가 보입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '서버의 정체' },
|
||||
{ n: 2, label: '3계층 구조' },
|
||||
{ n: 3, label: '우리 서버 해부' },
|
||||
{ n: 4, label: '정적 vs 동적' },
|
||||
{ n: 5, label: '동시 접속' },
|
||||
{ n: 6, label: '스케일 업/아웃' },
|
||||
{ n: 7, label: '내 PC로 서버 실습' },
|
||||
];
|
||||
|
||||
export default function ServerAnatomyPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: '서버 = 비싼 기계'라는 오해를 첫 문장부터 깨고 시작 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>서버란 무엇인가<br />— 요청을 기다리는 프로그램의 세계</h1>
|
||||
<p>
|
||||
서버는 서버실의 비싼 기계가 아니라, <strong>요청을 기다렸다가 응답하는 프로그램</strong>이에요.
|
||||
그 정체를 파헤치고, 지금 이 페이지를 여러분에게 보내 준 우리 회사 서버의 속을 열어 본 다음,
|
||||
마지막엔 <strong>여러분의 PC를 직접 서버로</strong> 만들어 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="서버의 정체 — 기계가 아니라 '기다리는 프로그램'" sub="내 노트북도 지금 당장 서버가 될 수 있다">
|
||||
<p>
|
||||
"서버"라고 하면 서버실에서 불빛을 깜빡이는 커다란 기계가 떠오르죠?
|
||||
반은 맞고 반은 틀려요. 그 기계는 서버 <strong>프로그램을 돌리기 위한 컴퓨터</strong>일 뿐이고,
|
||||
진짜 서버는 그 안에서 <strong>포트를 열고 요청을 기다리는 프로그램</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_WHAT_IS_SERVER}</Code>
|
||||
<p>
|
||||
그래서 브라우저처럼 <strong>먼저 요청을 보내는 쪽</strong>을 클라이언트(client, 손님),
|
||||
<strong> 기다렸다가 응답하는 쪽</strong>을 서버라고 불러요. 이건 프로그램의
|
||||
<strong> 역할 이름</strong>이지 기계의 등급이 아니에요 — 여러분의 노트북에 서버 프로그램을
|
||||
켜면 그 순간 노트북이 서버가 되고, 슈퍼컴퓨터라도 요청만 보내고 있으면 클라이언트입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>미리보기</b> "내 PC가 서버가 된다"는 말, 못 믿겠다면 섹션 7에서 직접 확인하게 됩니다.
|
||||
사실 여러분이 <span className="icode">npm run dev</span>를 칠 때마다 이미 서버를 켜고 있었어요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="웹서버 · WAS · DB — 3계층 구조" sub="홀 직원, 주방, 냉장고가 각자 잘하는 일">
|
||||
<p>
|
||||
웹 서비스 하나는 보통 프로그램 하나가 아니라 <strong>역할이 다른 세 프로그램의 팀</strong>이에요.
|
||||
손님을 맞는 <strong>웹서버</strong>, 요리(계산·판단)를 하는 <strong>WAS</strong>
|
||||
(Web Application Server), 재료(데이터)를 보관하는 <strong>DB</strong>(데이터베이스)죠.
|
||||
</p>
|
||||
<Code>{CODE_THREE_TIER}</Code>
|
||||
<p>각 층이 하는 일을 조금 더 자세히 보면 이래요.</p>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>웹서버</strong> — 인터넷에서 오는 요청을 가장 먼저 받아요. HTTPS 암호를 풀고,
|
||||
이미지·CSS처럼 미리 만들어 둔 파일은 직접 건네주고, 계산이 필요한 요청만 뒤로 넘깁니다.
|
||||
현관에서 손님을 맞고 간단한 반찬은 바로 내주는 홀 직원이에요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>WAS</strong> — "이 학생의 진도율은?" 같은 질문에 답하려면 로그인 확인, DB 조회,
|
||||
계산, JSON 조립이 필요해요. 이 <strong>비즈니스 로직</strong>을 담당하는 게 주방, WAS입니다.
|
||||
우리 회사에선 Spring Boot가 이 역할이에요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>DB</strong> — 데이터를 표(테이블) 형태로 안전하게 보관하고, 요청하면 빠르게
|
||||
찾아 주는 창고예요. 서버가 꺼져도 데이터는 남아야 하니까, 보관은 전문가(PostgreSQL)에게 맡깁니다.
|
||||
</li>
|
||||
</ol>
|
||||
<div className="warn">
|
||||
<b>왜 주방(WAS)이 냉장고(DB) 역할까지 하면 안 될까?</b> — 기술적으로는 WAS가 메모리에
|
||||
데이터를 들고 있을 수도 있어요. 하지만 WAS가 재시작되면 메모리는 통째로 사라집니다.
|
||||
"역할 분리"는 멋 부리기가 아니라 <strong>데이터를 잃지 않기 위한 생존 전략</strong>이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="우리 서버 해부 — Caddy → Spring Boot → PostgreSQL" sub="지금 이 페이지가 여러분에게 도착한 실제 경로">
|
||||
<p>
|
||||
이제 3계층을 <strong>우리 회사의 진짜 서버</strong>에 겹쳐 볼게요. 여러분이 이 학습 플랫폼에
|
||||
접속하면, AWS <strong>EC2 서울</strong> 서버 안에서 이런 릴레이가 벌어집니다.
|
||||
</p>
|
||||
<Code>{CODE_OUR_STACK}</Code>
|
||||
<p>
|
||||
여기서 <strong>Docker</strong>의 역할도 짚고 갈게요. Caddy·Spring Boot·PostgreSQL은 각각
|
||||
설치법도 설정법도 다른 프로그램인데, Docker는 이들을 <strong>규격 컨테이너</strong>에
|
||||
하나씩 포장해 줍니다. 어떤 컴퓨터에서든 똑같이 실행되는 도시락 통이라, "제 PC에서는
|
||||
됐는데요..." 문제를 크게 줄여 줘요.
|
||||
</p>
|
||||
<p>
|
||||
그리고 이 구조에서 React는 어디 있냐고요? 여러분이 짠 React 코드는 빌드를 거쳐
|
||||
<strong> 정적 파일(HTML·JS·CSS)</strong>이 되고, Caddy가 그 파일을 서빙해요. 즉 React
|
||||
앱은 서버가 아니라 <strong>서버가 배달해 준 뒤 브라우저 안에서 실행되는 프로그램</strong>입니다.
|
||||
이 구분이 다음 섹션의 주제예요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="정적 파일 vs 동적 응답" sub="자판기에서 뽑을 것인가, 주방에서 만들 것인가">
|
||||
<Code>{CODE_STATIC_DYNAMIC}</Code>
|
||||
<p>
|
||||
이 구분이 중요한 이유는 <strong>비용</strong>이에요. 정적 파일은 자판기처럼 꺼내 주기만
|
||||
하면 되니 웹서버가 순식간에 수백 개도 처리해요. 반면 동적 응답은 매번 주방(WAS)과
|
||||
창고(DB)까지 다녀와야 하니 훨씬 비쌉니다. 그래서 좋은 서비스는 <strong>정적으로 될 일은
|
||||
최대한 정적으로</strong> 처리하고, 꼭 필요한 것만 동적으로 만들어요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 우리 플랫폼에서 <span className="kbd">F12</span> →
|
||||
<span className="kbd">Network</span> 탭 → <span className="kbd">F5</span> 새로고침.
|
||||
목록에서 <span className="icode">.js</span>·<span className="icode">.css</span>·이미지
|
||||
요청(정적)과 <span className="icode">/api/</span>로 시작하는 요청(동적)을 구분해 보세요.
|
||||
같은 화면 한 장이 정적·동적 응답의 <strong>합작품</strong>이라는 걸 눈으로 확인할 수 있어요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="동시 접속 — 손님이 한꺼번에 몰려오면" sub="스레드 풀과 커넥션 풀, 서버의 인력 관리">
|
||||
<p>
|
||||
우리 반 30명이 <strong>동시에</strong> 이 페이지를 열면 서버는 어떻게 될까요?
|
||||
한 명씩 순서대로 처리한다면 30번째 친구는 한참을 기다려야겠죠. 그래서 서버는
|
||||
<strong> 여러 요청을 겹쳐서</strong> 처리하는 장치들을 갖고 있어요.
|
||||
</p>
|
||||
<Code>{CODE_CONCURRENCY}</Code>
|
||||
<p>
|
||||
정리하면 — 서버의 성능은 "얼마나 빨리 하나를 처리하느냐"만이 아니라
|
||||
<strong> "동시에 몇 개를 받아 내느냐"</strong>로도 측정돼요. 수강신청 사이트가
|
||||
정각에 뻗는 것도, 대기열(스레드·커넥션)이 감당 못 할 만큼 요청이 몰렸기 때문입니다.
|
||||
그럼 감당이 안 될 땐 어떻게 할까요? 다음 섹션이 그 답이에요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="스케일 업 vs 스케일 아웃" sub="주방장을 바꿀 것인가, 지점을 낼 것인가">
|
||||
<Code>{CODE_SCALING}</Code>
|
||||
<p>
|
||||
실무의 감각은 이래요 — <strong>작을 땐 스케일 업이 정답</strong>입니다. 단순하고,
|
||||
빠르고, 구조를 바꿀 필요가 없거든요. 우리 플랫폼도 지금은 EC2 한 대로 충분해요.
|
||||
하지만 사용자가 수만 명이 되면 아무리 좋은 한 대로도 버틸 수 없고, 그 한 대가
|
||||
멈추면 서비스 전체가 멈춘다는 위험도 커집니다. 그때 <strong>스케일 아웃</strong>으로
|
||||
넘어가요.
|
||||
</p>
|
||||
<p>
|
||||
그리고 여기서 섹션 2의 복선이 회수됩니다 — 3계층으로 <strong>역할을 나눠 둔 덕분에</strong>,
|
||||
바쁜 층(보통 WAS)만 여러 대로 늘리고 DB는 그대로 두는 식의 <strong>부분 확장</strong>이
|
||||
가능해요. 처음부터 한 덩어리로 만들었다면 통째로 복제하는 수밖에 없었겠죠.
|
||||
설계는 미래의 나를 위한 보험입니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습 — 내 PC를 서버로 만들기" sub="npm run dev, 사실 여러분은 매일 서버를 켜고 있었다">
|
||||
<p>
|
||||
대망의 실습이에요. 준비물은 여러분이 늘 쓰는 그 명령어 하나 —
|
||||
다만 오늘은 <strong>"서버를 켠다"는 자각</strong>을 갖고 다시 봅니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 터미널에서 <span className="icode">mirim-app/frontend</span> 폴더로
|
||||
이동해 아래를 실행하고, 주석의 관찰 포인트를 <strong>하나씩 전부</strong> 따라가 보세요.
|
||||
특히 마지막 — 서버를 끄고 새로고침해서 "연결할 수 없음"을 보는 순간이 오늘의 하이라이트입니다.
|
||||
</div>
|
||||
<Code>{CODE_LOCALHOST}</Code>
|
||||
<p>
|
||||
방금 벌어진 일을 정리하면: <strong>Vite 개발 서버</strong>(node 프로그램)가 내 PC의
|
||||
5173번 포트를 열고 기다렸고, 브라우저가 <span className="icode">localhost:5173</span>으로
|
||||
요청을 보냈고, 서버가 페이지를 응답했어요. 클라이언트와 서버가 <strong>같은 컴퓨터 안에</strong>
|
||||
있었을 뿐, 섹션 1의 그림과 완전히 같은 구조입니다.
|
||||
</p>
|
||||
<p>
|
||||
한 걸음 더 — 내 PC 안에서 지금 이 순간에도 기다리고 있는 서버들을 구경해 볼까요?
|
||||
</p>
|
||||
<Code>{CODE_PORT_CHECK}</Code>
|
||||
<div className="warn">
|
||||
<b>주의</b> <span className="icode">netstat</span>은 구경만 하세요. 모르는 포트를 잡고 있는
|
||||
프로그램을 함부로 종료하면 Windows 시스템 기능이 멈출 수 있어요. 관찰은 자유, 종료는 금지!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🖥️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 서버가 <strong>요청을 기다리는 프로그램</strong>이라는 것, 웹 서비스가
|
||||
<strong> 웹서버·WAS·DB 세 팀의 릴레이</strong>(우리 회사에선 Caddy → Spring Boot →
|
||||
PostgreSQL)라는 것, 그리고 몰려드는 요청을 <strong>스레드 풀로 버티고 스케일 업/아웃으로
|
||||
키운다</strong>는 것까지 알게 됐어요. 무엇보다, 내 PC로 서버를 직접 켜고 꺼 봤죠.
|
||||
다음은 그 릴레이의 마지막 주자 — 데이터를 보관하는 창고의 속을 여는{' '}
|
||||
<Link to="/learn/database"><strong>데이터베이스 기초</strong></Link> 코스로 이어 가세요.
|
||||
멘토 과제: 오늘 배운 3계층 그림을 보지 않고 종이에 그려서 멘토에게 설명해 보기!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
382
frontend/src/pages/courses/SpringIntroPage.jsx
Normal file
382
frontend/src/pages/courses/SpringIntroPage.jsx
Normal file
@ -0,0 +1,382 @@
|
||||
// 이 파일이 하는 일: "Spring Boot 입문" 코스 — 스프링이 대신 해주는 일(DI)에서 출발해
|
||||
// 어노테이션 읽는 법, Controller→Service→Repository 계층 여행, application.yml,
|
||||
// JPA 맛보기, 그리고 우리 백엔드에 API 하나를 직접 추가하는 실습까지 7개 섹션으로 안내한다.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 정적 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 자바 코드 예시는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 코드 예시 · 텍스트 다이어그램 상수들 ──
|
||||
|
||||
const CODE_NO_SPRING = `// 스프링이 없다면 — 필요한 재료를 전부 '내가' 만들어야 해요.
|
||||
public class AuthService {
|
||||
// DB 연결 만들고, 리포지토리 만들고, 암호화 도구 만들고...
|
||||
private final UserRepository userRepository =
|
||||
new UserRepository(new DataSource("jdbc:postgresql://...", "id", "pw"));
|
||||
private final PasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
// 재료가 하나 바뀌면? 이걸 만든 모든 파일을 고쳐야 합니다. 끔찍하죠.
|
||||
}`;
|
||||
|
||||
const CODE_WITH_SPRING = `// 스프링이 있다면 — "필요해요"라고 선언만 하면 주방(컨테이너)이 가져다줘요.
|
||||
@Service
|
||||
public class AuthService {
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder encoder;
|
||||
|
||||
// 생성자에 적어 두면 스프링이 알아서 넣어(주입해) 줍니다.
|
||||
public AuthService(UserRepository userRepository, PasswordEncoder encoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.encoder = encoder;
|
||||
}
|
||||
}`;
|
||||
|
||||
const CODE_ANNOTATIONS = `우리 백엔드에서 매일 만나는 어노테이션 5총사
|
||||
|
||||
@RestController "나는 요청을 받는 카운터 직원이에요. 응답은 JSON으로 드려요."
|
||||
@Service "나는 주방에서 실제 요리(비즈니스 로직)를 하는 요리사예요."
|
||||
@Repository "나는 창고(DB) 담당이에요. 재료를 꺼내오고 넣어요."
|
||||
@Component "특별한 역할은 없지만 나도 스프링이 관리해 주세요." (위 셋의 부모 격)
|
||||
@Configuration "나는 주방 설비 설치 담당 — 빈(Bean)을 직접 만들어 등록해요."
|
||||
|
||||
공통점: 클래스 위에 붙이면 스프링이 그 클래스를 '빈(Bean)'으로 만들어
|
||||
컨테이너(주방)에 등록하고, 필요한 곳에 배달(주입)해 줍니다.`;
|
||||
|
||||
const CODE_CONSTRUCTOR_DI = `// 생성자 주입 — 우리 회사 표준 방식이에요.
|
||||
@RestController
|
||||
public class AuthController {
|
||||
private final AuthService authService; // final! 한 번 받으면 안 바뀜
|
||||
|
||||
public AuthController(AuthService authService) { // 생성자가 하나면
|
||||
this.authService = authService; // @Autowired 생략 가능
|
||||
}
|
||||
}
|
||||
|
||||
// 필드에 @Autowired를 직접 붙이는 방식(필드 주입)도 돌아가긴 하지만,
|
||||
// final을 못 쓰고 테스트하기 어려워서 요즘은 생성자 주입이 정석입니다.`;
|
||||
|
||||
const CODE_LAYER_TRIP = `로그인 요청 한 번의 여행 — POST /api/auth/login 이 도착하면:
|
||||
|
||||
[브라우저]
|
||||
│ POST /api/auth/login { "email": "...", "password": "..." }
|
||||
▼
|
||||
① AuthController (현관 카운터)
|
||||
│ "주문 받았어요!" — 요청 JSON을 DTO로 받아서 검사만 하고
|
||||
▼ 바로 주방에 넘깁니다. 카운터는 요리하지 않아요.
|
||||
② AuthService (주방)
|
||||
│ 비밀번호 대조, 토큰 발급 같은 '진짜 일'은 전부 여기서.
|
||||
▼ 재료가 필요하면 창고 담당을 부릅니다.
|
||||
③ UserRepository (창고)
|
||||
│ SELECT * FROM users WHERE email = ? ← DB에서 재료 꺼내오기
|
||||
▼
|
||||
[PostgreSQL]
|
||||
|
||||
응답은 이 길을 거꾸로: 창고 → 주방 → 카운터 → 200 OK + JSON.
|
||||
각 층이 자기 일만 하니까, 문제가 생겨도 "어느 층이 범인인지" 금방 찾아요.`;
|
||||
|
||||
const CODE_YML = `# application.yml — 코드 밖에 두는 '설정 메모장'
|
||||
server:
|
||||
port: 8080 # 이 서버가 문을 여는 포트
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: \${DB_URL:jdbc:postgresql://localhost:5432/mirim}
|
||||
username: \${DB_USER:mirim}
|
||||
password: \${DB_PASSWORD} # 비밀번호는 기본값 없이 환경변수로만!
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update # 엔티티 보고 테이블 자동 생성/수정
|
||||
|
||||
# \${환경변수:기본값} 문법이 핵심!
|
||||
# 내 PC에선 기본값으로 돌고, 운영 EC2에선 환경변수가 덮어씁니다.
|
||||
# 같은 코드로 어디서든 도는 비결이 이거예요.`;
|
||||
|
||||
const CODE_JPA = `// JPA — 메서드 '이름'이 곧 SQL이 되는 마법
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
// → SELECT * FROM users WHERE email = ?
|
||||
|
||||
boolean existsByNickname(String nickname);
|
||||
// → SELECT COUNT(*) > 0 FROM users WHERE nickname = ?
|
||||
|
||||
List<User> findByRoleOrderByCreatedAtDesc(String role);
|
||||
// → SELECT * FROM users WHERE role = ? ORDER BY created_at DESC
|
||||
}
|
||||
// 구현 코드가 한 줄도 없죠? 스프링이 메서드 이름을 '읽고'
|
||||
// SQL을 대신 써 줍니다. 규칙: findBy + 필드명 + (조건/정렬)`;
|
||||
|
||||
const CODE_PRACTICE_API = `// 실습: 서버의 상태를 알려주는 초간단 API 만들기
|
||||
// 위치: backend/src/main/java/.../web/HelloController.java (새 파일)
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/api/hello")
|
||||
public Map<String, String> hello() {
|
||||
return Map.of(
|
||||
"message", "안녕하세요, AWESOMEDEV 수습 여러분!",
|
||||
"from", "내가 만든 첫 스프링 API"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 이게 전부예요. 서버 재시작 후 브라우저에서
|
||||
// http://localhost:8080/api/hello 를 열어 보세요.
|
||||
// JSON이 보이면 — 방금 백엔드 개발자가 된 겁니다.`;
|
||||
|
||||
const CODE_PRACTICE_NEXT = `한 단계 더 (STUDY 과제 연계) — 파라미터 받아 보기
|
||||
|
||||
@GetMapping("/api/hello/{name}")
|
||||
public Map<String, String> helloName(@PathVariable String name) {
|
||||
return Map.of("message", name + "님, 환영합니다!");
|
||||
}
|
||||
|
||||
// http://localhost:8080/api/hello/미림 으로 접속하면?
|
||||
// URL의 '미림'이 name 변수로 들어옵니다.
|
||||
// 도전 과제: @RequestParam으로 ?lang=en 을 받아서
|
||||
// 영어 인사도 되게 만들어 보세요. 완성하면 Gitea에 커밋!`;
|
||||
|
||||
const CODE_QUIZ = `셀프 체크 5문항 — 답을 말로 설명할 수 있으면 통과!
|
||||
|
||||
Q1. "의존성 주입(DI)"을 식당 주방 비유로 설명해 보세요.
|
||||
힌트: 요리사가 직접 장을 보러 가나요? (섹션 1)
|
||||
|
||||
Q2. @RestController와 @Service의 역할 차이는?
|
||||
힌트: 카운터 직원과 요리사 (섹션 2·3)
|
||||
|
||||
Q3. Controller에 비즈니스 로직을 쓰면 왜 안 좋을까요?
|
||||
힌트: 카운터에서 요리를 하면 주문은 누가 받죠? (섹션 3)
|
||||
|
||||
Q4. DB 비밀번호를 application.yml에 그대로 적으면 왜 위험할까요?
|
||||
힌트: 이 파일은 Git에 올라갑니다 (섹션 4)
|
||||
|
||||
Q5. findByEmailAndRole(String email, String role) 은
|
||||
어떤 SQL이 될까요? 직접 써 보세요. (섹션 5)
|
||||
|
||||
→ 막히는 문항은 해당 섹션으로 돌아가서 코드를 소리 내어 읽어 보세요.
|
||||
"코드를 말로 번역"할 수 있으면 진짜 이해한 거예요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '스프링이 해주는 일' },
|
||||
{ n: 2, label: '어노테이션 읽기' },
|
||||
{ n: 3, label: '계층 구조 여행' },
|
||||
{ n: 4, label: 'application.yml' },
|
||||
{ n: 5, label: 'JPA 한 입' },
|
||||
{ n: 6, label: '실습: 첫 API' },
|
||||
{ n: 7, label: '정리 퀴즈' },
|
||||
];
|
||||
|
||||
export default function SpringIntroPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 소프트웨어</div>
|
||||
<h1>Spring Boot 입문<br />— 우리 백엔드가 돌아가는 원리</h1>
|
||||
<p>
|
||||
자바 코드 몇 줄이 어떻게 로그인·회원가입 같은 서비스가 되는지, 우리 플랫폼의
|
||||
백엔드를 직접 열어 보며 배웁니다. 마지막 섹션에선 진짜로 <strong>내 손으로 API
|
||||
하나를 추가</strong>해 봐요 — 읽기만 하는 코스가 아닙니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 90분</span>
|
||||
<span className="chip">실습 2개 + 도전 과제</span>
|
||||
<span className="chip">선수 지식: 자바 문법 기초</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="스프링이 해주는 일 — 객체 조립(DI)" sub="요리사는 요리만, 장보기는 주방이">
|
||||
<p>
|
||||
식당 주방을 상상해 보세요. 요리사가 요리할 때마다 직접 마트에 가서 재료를 사 오고,
|
||||
칼을 벼리고, 가스까지 설치해야 한다면? 요리할 시간이 없겠죠. 좋은 주방은
|
||||
<strong> 요리사가 "재료 주세요" 하면 필요한 게 손에 들어오는</strong> 곳이에요.
|
||||
</p>
|
||||
<p>
|
||||
코드도 똑같아요. 객체가 일하려면 다른 객체(재료)가 필요한데, 그걸 매번 직접
|
||||
<span className="icode">new</span>로 만들면 이렇게 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_NO_SPRING}</Code>
|
||||
<p>
|
||||
스프링은 이 "장보기"를 통째로 대신해 줘요. 앱이 켜질 때 필요한 객체들을 미리 만들어
|
||||
<strong> 컨테이너</strong>(주방 창고)에 보관해 두고, "이거 필요해요"라고 선언한 곳에
|
||||
<strong> 알아서 넣어 줍니다</strong>. 이게 <strong>의존성 주입</strong>(DI,
|
||||
Dependency Injection)이에요. 이름은 어렵지만 뜻은 "필요한 재료를 주방이 가져다준다",
|
||||
그게 전부입니다.
|
||||
</p>
|
||||
<Code>{CODE_WITH_SPRING}</Code>
|
||||
<div className="tip">
|
||||
<b>왜 이게 좋은가요?</b> 재료(예: DB 연결 방식)가 바뀌어도 주방 설정 한 곳만 고치면
|
||||
돼요. 요리사(각 클래스)들은 코드 한 줄 안 바꿔도 됩니다. 부품을 갈아 끼우기 쉬운
|
||||
코드 — 이게 스프링이 20년 넘게 살아남은 이유예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="어노테이션 읽기" sub="@ 붙은 한 줄이 스프링에게 보내는 쪽지">
|
||||
<p>
|
||||
스프링 코드를 처음 열면 <span className="icode">@</span>로 시작하는 단어가 잔뜩
|
||||
보여요. 겁먹지 마세요 — 어노테이션은 <strong>스프링에게 보내는 짧은 쪽지</strong>일
|
||||
뿐입니다. "이 클래스는 이런 역할이니 이렇게 관리해 주세요"라는 메모예요.
|
||||
</p>
|
||||
<Code>{CODE_ANNOTATIONS}</Code>
|
||||
<p>
|
||||
그리고 재료를 받는 방법, 즉 주입 방식 중에서 우리는 <strong>생성자 주입</strong>을
|
||||
씁니다. 생성자의 파라미터에 필요한 것을 적어 두면 스프링이 객체를 만들 때 채워 줘요.
|
||||
</p>
|
||||
<Code>{CODE_CONSTRUCTOR_DI}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 백엔드 저장소(edu.awesomedevapp.com의 Gitea)에서
|
||||
<span className="icode"> AuthController.java</span>를 열어 보세요. 클래스 위의
|
||||
<span className="icode"> @RestController</span>, 생성자에서 받는
|
||||
<span className="icode"> final</span> 필드들 — 방금 배운 모양 그대로인지 눈으로
|
||||
확인해 보세요. "읽을 수 있는 코드"가 하나 생기는 순간입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="계층 구조 여행 — Controller → Service → Repository" sub="로그인 요청 하나를 끝까지 따라가기">
|
||||
<p>
|
||||
백엔드는 층이 나뉜 식당이에요. <strong>카운터(Controller)</strong>는 주문만 받고,
|
||||
<strong> 주방(Service)</strong>은 요리만 하고, <strong>창고(Repository)</strong>는
|
||||
재료 관리만 합니다. 여러분이 우리 플랫폼에 로그인하는 순간, 요청은 이 세 층을
|
||||
차례로 여행해요.
|
||||
</p>
|
||||
<Code>{CODE_LAYER_TRIP}</Code>
|
||||
<p>
|
||||
핵심은 <strong>각 층이 자기 일만 한다</strong>는 것. 카운터가 요리까지 하면 주문이
|
||||
밀리고, 요리사가 창고 정리까지 하면 음식이 늦어요. 코드도 같습니다 — Controller에
|
||||
비즈니스 로직을 쓰기 시작하면, 나중에 "로그인 규칙만 바꾸고 싶은데" 할 때 어디를
|
||||
고쳐야 할지 아무도 몰라요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>수습 단골 실수</b> — Controller에서 Repository를 바로 부르는 코드.
|
||||
당장은 돌아가지만, 검증·로그 같은 "주방에서 할 일"이 카운터에 쌓이기 시작해요.
|
||||
Controller → Service → Repository, 한 방향으로만 흐르게 유지하세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="application.yml — 설정은 코드 밖으로" sub="같은 코드가 내 PC에서도, 서울 EC2에서도 도는 비결">
|
||||
<p>
|
||||
DB 주소, 포트 번호, 비밀번호… 이런 값을 자바 코드에 직접 적으면 환경이 바뀔 때마다
|
||||
코드를 고쳐야 해요. 그래서 스프링은 설정을 <strong>application.yml</strong>이라는
|
||||
별도 파일에 몰아둡니다. 요리 레시피(코드)와 "오늘의 가스 밸브 위치"(설정)를 분리하는
|
||||
거예요.
|
||||
</p>
|
||||
<Code>{CODE_YML}</Code>
|
||||
<p>
|
||||
<span className="icode">{'${DB_URL:기본값}'}</span> 문법을 눈여겨보세요.
|
||||
<strong> 환경변수가 있으면 그걸 쓰고, 없으면 기본값</strong>이라는 뜻이에요. 덕분에
|
||||
내 PC에선 로컬 PostgreSQL, 운영 서울 EC2에선 진짜 DB로 — 코드 수정 없이 환경변수만
|
||||
바꿔 배포합니다. 우리 Docker 배포가 정확히 이 원리로 굴러가요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>절대 규칙</b> — 진짜 비밀번호·키는 yml에 기본값으로 적어서 Git에 올리지 마세요.
|
||||
Git 이력은 지워지지 않아요. 비밀값은 환경변수(.env)로만, 이건 회사 보안 규칙이기도
|
||||
합니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="JPA 한 입 — 메서드 이름이 SQL로" sub="구현 없이 인터페이스만 썼는데 DB가 조회되는 이유">
|
||||
<p>
|
||||
창고(Repository) 층에서 SQL을 매번 손으로 쓰면 지루하고 실수도 잦아요.
|
||||
<strong> JPA</strong>는 자바 객체와 DB 테이블을 이어 주는 통역사예요. 그중 제일
|
||||
신기한 기능이 이겁니다 — <strong>메서드 이름만 규칙대로 지으면, 스프링이 이름을 읽고
|
||||
SQL을 대신 써 줘요.</strong>
|
||||
</p>
|
||||
<Code>{CODE_JPA}</Code>
|
||||
<p>
|
||||
<span className="icode">findBy</span> 뒤에 필드명을 붙이면 WHERE 조건이 되고,
|
||||
<span className="icode"> And</span>·<span className="icode">OrderBy</span> 같은
|
||||
단어로 조건을 이어 붙일 수 있어요. 물론 복잡한 쿼리는 직접 SQL을 쓰기도 하지만,
|
||||
단순 조회의 8할은 이 "이름 짓기"로 끝납니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>주의할 점 하나</b> — 편한 만큼, JPA가 뒤에서 <strong>어떤 SQL을 만드는지</strong>
|
||||
모르면 느린 코드를 쓰게 돼요. 로그에 SQL을 찍어 보는 습관
|
||||
(<span className="icode">show-sql: true</span>)을 들이면, 편리함과 이해를 둘 다
|
||||
가져갈 수 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="실습 — 우리 백엔드에 첫 API 추가하기" sub="읽었으면 이제 쓸 차례 (STUDY 과제 연계)">
|
||||
<p>
|
||||
지금까지 배운 걸 전부 씁니다. 컨트롤러 파일 하나를 새로 만들어서, 브라우저로 접속하면
|
||||
JSON 인사말이 돌아오는 API를 여는 거예요. 코드는 딱 이만큼입니다.
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE_API}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 백엔드 프로젝트를 열고 위 파일을 만든 뒤 서버를 실행하세요.
|
||||
브라우저에서 <span className="icode">localhost:8080/api/hello</span>를 열어 JSON이
|
||||
보이면 성공! 그다음 <span className="kbd">F12</span> → <span className="kbd">Network</span>
|
||||
탭에서 방금 요청의 <strong>Status 200</strong>과 응답 본문을 확인해 보세요.
|
||||
네트워크 코스에서 배운 "요청과 응답"을 이번엔 <strong>내가 만든 서버</strong>로 보는
|
||||
거예요.
|
||||
</div>
|
||||
<p>여기까지 됐다면, 한 걸음 더 나가 봅시다.</p>
|
||||
<Code>{CODE_PRACTICE_NEXT}</Code>
|
||||
<p>실습이 막힐 때 점검 순서는 이렇게 해요.</p>
|
||||
<ol className="olist">
|
||||
<li>서버가 켜져 있나요? 터미널에 에러 없이 <span className="icode">Started ...</span> 로그가 떴는지 확인.</li>
|
||||
<li>404가 나오면 — URL 오타이거나 <span className="icode">@GetMapping</span> 경로가 다른 거예요.</li>
|
||||
<li>500이 나오면 — 서버 터미널의 에러 로그를 <strong>위에서부터</strong> 읽으세요. 첫 줄에 답이 있어요.</li>
|
||||
<li>그래도 막히면 멘토에게 — 단, "안 돼요" 말고 <strong>"뭘 했고, 뭘 기대했고, 뭐가 나왔는지"</strong> 세 가지를 들고 가세요.</li>
|
||||
</ol>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="정리 퀴즈 — 셀프 체크 5문항" sub="설명할 수 있어야 아는 것">
|
||||
<Code>{CODE_QUIZ}</Code>
|
||||
<div className="tip">
|
||||
<b>공부 팁</b> 퀴즈보다 좋은 복습은 <strong>우리 실제 코드 읽기</strong>예요.
|
||||
Gitea에서 백엔드 저장소를 열고, 아무 Controller나 골라 오늘 배운 어노테이션과
|
||||
계층 흐름을 찾아보세요. 교과서 예제가 아니라 <strong>지금 돌아가는 서비스</strong>가
|
||||
우리 교재입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🌱 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 스프링이 객체를 조립해 주는 원리(DI), 어노테이션 읽는 법, 요청이 세 층을
|
||||
여행하는 길, 설정과 JPA까지 — 우리 백엔드의 뼈대를 전부 훑었어요. 심지어 API도
|
||||
하나 만들었죠. 다음은 그 데이터가 잠드는 곳 —{' '}
|
||||
<Link to="/learn/database"><strong>데이터베이스 기초</strong></Link> 코스에서
|
||||
PostgreSQL과 테이블 설계를 이어서 배우고, 실습에서 만든 API는{' '}
|
||||
<Link to="/study"><strong>STUDY 과제</strong></Link>로 제출해 멘토 리뷰를 받아
|
||||
보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
470
frontend/src/pages/courses/SqlIntermediatePage.jsx
Normal file
470
frontend/src/pages/courses/SqlIntermediatePage.jsx
Normal file
@ -0,0 +1,470 @@
|
||||
// 이 파일이 하는 일: "SQL 중급" 코스 — SELECT를 뗀 사람이 다음으로 만나는 벽,
|
||||
// 집계(GROUP BY)·HAVING·서브쿼리·NULL·인덱스·EXPLAIN·트랜잭션을 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (GROUP BY 집계 → HAVING → 서브쿼리 → NULL의 함정 → 인덱스 → EXPLAIN → 트랜잭션/ACID → 종합 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// SQL 예제는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
// 선수 코스: SQL 입문 (SELECT, WHERE, ORDER BY를 알고 있다고 가정)
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── SQL 예제 · 결과 표 상수들 ──
|
||||
|
||||
const CODE_GROUP_BY = `-- 우리 학습 플랫폼 DB의 제출 테이블(submission)이 이렇게 생겼다고 해요:
|
||||
-- id | student_id | week | score | submitted_at
|
||||
|
||||
-- "주차별로 제출이 몇 건이었지?" — GROUP BY 없이는 못 세요.
|
||||
SELECT week, COUNT(*) AS 제출수, AVG(score) AS 평균점수
|
||||
FROM submission
|
||||
GROUP BY week
|
||||
ORDER BY week;
|
||||
|
||||
week | 제출수 | 평균점수
|
||||
------+--------+----------
|
||||
1 | 28 | 87.5
|
||||
2 | 26 | 82.1
|
||||
3 | 19 | 79.3 ← 3주차부터 제출이 줄고 있네요?!
|
||||
|
||||
-- GROUP BY week = "week 값이 같은 행끼리 한 팀으로 묶어라"
|
||||
-- COUNT(*) = 팀마다 행이 몇 개인지
|
||||
-- AVG(score) = 팀마다 score의 평균`;
|
||||
|
||||
const CODE_GROUP_BY_RULE = `-- GROUP BY의 철칙: SELECT에는 '묶은 기준 컬럼'과 '집계 함수'만!
|
||||
|
||||
SELECT week, student_id, COUNT(*) -- ❌ 에러!
|
||||
FROM submission
|
||||
GROUP BY week;
|
||||
|
||||
-- ERROR: column "submission.student_id" must appear in the
|
||||
-- GROUP BY clause or be used in an aggregate function
|
||||
|
||||
-- 왜? 1주차 팀에는 학생이 28명 들어 있는데,
|
||||
-- "1주차의 student_id"라고 하면 28명 중 누구를 보여 달라는 건지
|
||||
-- DB가 알 수 없기 때문이에요. 반 전체를 한 줄로 요약하면서
|
||||
-- "그 줄의 이름은?" 하고 묻는 것과 같아요 — 답이 하나가 아니죠.`;
|
||||
|
||||
const CODE_HAVING = `-- "제출이 20건 넘는 주차만 보여줘" — 묶은 결과를 거르고 싶다면 HAVING!
|
||||
|
||||
SELECT week, COUNT(*) AS 제출수
|
||||
FROM submission
|
||||
WHERE score IS NOT NULL -- ① 묶기 전에 행을 거르는 건 WHERE
|
||||
GROUP BY week -- ② 묶고
|
||||
HAVING COUNT(*) > 20 -- ③ 묶은 '팀'을 거르는 건 HAVING
|
||||
ORDER BY week;
|
||||
|
||||
WHERE = 입장 검사 (팀을 만들기 전에 한 명씩 거름)
|
||||
HAVING = 팀 심사 (팀이 다 만들어진 뒤 팀 단위로 거름)
|
||||
|
||||
-- WHERE COUNT(*) > 20 이라고 쓰면 에러예요.
|
||||
-- 입장 검사 시점에는 아직 팀이 없어서 팀원 수를 셀 수 없거든요.`;
|
||||
|
||||
const CODE_SUBQUERY = `-- "평균보다 점수가 높은 제출만 보고 싶다"
|
||||
-- 평균이 몇 점인지 먼저 알아야겠죠? 쿼리 안에 쿼리를 넣으면 됩니다.
|
||||
|
||||
SELECT student_id, week, score
|
||||
FROM submission
|
||||
WHERE score > (SELECT AVG(score) FROM submission);
|
||||
-- └── 이 괄호가 먼저 실행돼 숫자 하나(예: 83.2)가 되고,
|
||||
-- 바깥 쿼리는 WHERE score > 83.2 처럼 동작해요.
|
||||
|
||||
-- IN과 함께 쓰면 "목록" 서브쿼리도 가능:
|
||||
-- "3주차에 제출한 학생들의 모든 제출을 보여줘"
|
||||
SELECT * FROM submission
|
||||
WHERE student_id IN (SELECT student_id FROM submission WHERE week = 3);
|
||||
|
||||
-- 요리로 치면 서브쿼리는 '미리 만들어 두는 육수'예요.
|
||||
-- 본 요리(바깥 쿼리)를 시작하기 전에 먼저 우려내서 재료로 씁니다.`;
|
||||
|
||||
const CODE_NULL = `-- NULL = "값이 없음"이 아니라 "모름". 이 차이가 사고를 부릅니다.
|
||||
|
||||
SELECT NULL = NULL; -- 결과: NULL (true가 아님!)
|
||||
-- "모르는 값"과 "모르는 값"이 같은지는... 그것도 모르죠.
|
||||
|
||||
-- 함정 ①: = 로는 NULL을 못 잡아요
|
||||
SELECT * FROM submission WHERE score = NULL; -- ❌ 항상 0건
|
||||
SELECT * FROM submission WHERE score IS NULL; -- ⭕ 미채점 제출 조회
|
||||
|
||||
-- 함정 ②: 집계 함수는 NULL을 조용히 건너뜁니다
|
||||
-- 제출 5건, score가 (90, 80, NULL, 70, NULL)일 때
|
||||
SELECT COUNT(*) FROM submission; -- 5 (행 전부)
|
||||
SELECT COUNT(score) FROM submission; -- 3 (NULL 제외!)
|
||||
SELECT AVG(score) FROM submission; -- 80 (240÷3, 240÷5가 아님!)
|
||||
|
||||
-- 함정 ③: NULL이 낀 계산은 통째로 NULL
|
||||
SELECT 100 + NULL; -- NULL. "100 + 모름 = 모름"
|
||||
|
||||
-- 구급약: COALESCE(값, 대체값) — NULL이면 대체값을 쓴다
|
||||
SELECT COALESCE(score, 0) FROM submission; -- 미채점을 0점으로 취급`;
|
||||
|
||||
const CODE_INDEX = `-- 인덱스 = 책 뒤의 '찾아보기'
|
||||
|
||||
인덱스 없이 검색 인덱스로 검색
|
||||
────────────────── ──────────────────
|
||||
책을 1쪽부터 끝까지 찾아보기에서 "트랜잭션 → 214쪽"
|
||||
한 장씩 넘기며 확인 확인하고 그 쪽으로 바로 이동
|
||||
(풀 스캔, Seq Scan) (Index Scan)
|
||||
|
||||
-- 학생 번호로 제출을 자주 찾는다면:
|
||||
CREATE INDEX idx_submission_student ON submission (student_id);
|
||||
|
||||
-- 이제 WHERE student_id = 7 검색이 수십만 행에서도 즉시!
|
||||
|
||||
단, 공짜가 아니에요:
|
||||
1) 인덱스도 디스크를 차지한다 (찾아보기 페이지만큼 책이 두꺼워짐)
|
||||
2) INSERT/UPDATE 때마다 인덱스도 고쳐야 해서 쓰기가 느려짐
|
||||
(본문을 고칠 때마다 찾아보기도 다시 만드는 셈)
|
||||
|
||||
→ "자주 검색하는 컬럼에만" 만드는 게 원칙.
|
||||
모든 컬럼에 인덱스를 걸면 읽기 조금 빨라지고 쓰기는 확 느려져요.`;
|
||||
|
||||
const CODE_EXPLAIN = `-- 내 쿼리가 인덱스를 타는지 안 타는지, DB에게 직접 물어보세요.
|
||||
-- 쿼리 앞에 EXPLAIN만 붙이면 '실행 계획서'를 보여줍니다.
|
||||
|
||||
EXPLAIN SELECT * FROM submission WHERE student_id = 7;
|
||||
|
||||
-- 인덱스가 없을 때:
|
||||
Seq Scan on submission (cost=0.00..431.00 rows=12 ...)
|
||||
Filter: (student_id = 7)
|
||||
→ "처음부터 끝까지 다 읽겠음" (책 한 장씩 넘기기)
|
||||
|
||||
-- CREATE INDEX 후 다시 실행하면:
|
||||
Index Scan using idx_submission_student on submission (...)
|
||||
Index Cond: (student_id = 7)
|
||||
→ "찾아보기로 바로 가겠음"
|
||||
|
||||
-- cost의 두 번째 숫자(431.00)가 대략적인 '예상 수고'예요.
|
||||
-- 쿼리가 느릴 때 제일 먼저 할 일 = EXPLAIN 붙여서 Seq Scan 찾기!`;
|
||||
|
||||
const CODE_TRANSACTION = `-- 트랜잭션 = "전부 성공 아니면 전부 취소"로 묶는 작업 단위
|
||||
|
||||
-- 예: 제출을 옮기는 두 문장이 '한 몸'이어야 한다면
|
||||
BEGIN; -- 트랜잭션 시작
|
||||
UPDATE submission SET week = 4 WHERE id = 101;
|
||||
UPDATE submission SET week = 4 WHERE id = 102;
|
||||
COMMIT; -- 둘 다 확정!
|
||||
-- 중간에 문제가 생기면 COMMIT 대신:
|
||||
ROLLBACK; -- 둘 다 없던 일로
|
||||
|
||||
ACID — 트랜잭션이 지키는 4가지 약속
|
||||
────────────────────────────────────
|
||||
A tomicity 원자성: 전부 되거나 전부 안 되거나 (반만 없음)
|
||||
C onsistency 일관성: 끝난 뒤에도 규칙(제약조건)이 안 깨짐
|
||||
I solation 격리성: 동시에 실행돼도 서로 안 섞임
|
||||
D urability 지속성: COMMIT했으면 정전이 나도 남는다
|
||||
|
||||
-- 계좌이체가 단골 비유죠: "내 돈 빠짐 + 친구 돈 늘어남"이
|
||||
-- 반쪽만 실행되면 대참사. 그래서 은행은 전부 트랜잭션입니다.`;
|
||||
|
||||
const CODE_PRACTICE = `-- 미션: "학생별 제출 수" 집계 쿼리를 완성하세요.
|
||||
-- 조건: ① 채점 완료(score가 NULL 아님)만 센다
|
||||
-- ② 제출 3건 이상인 학생만 보여준다
|
||||
-- ③ 제출 많은 순으로 정렬
|
||||
|
||||
SELECT student_id,
|
||||
______(*) AS 제출수, -- (a) 세는 함수는?
|
||||
ROUND(AVG(score), 1) AS 평균점수
|
||||
FROM submission
|
||||
WHERE score IS ______ NULL -- (b) NULL이 '아님'은?
|
||||
GROUP BY ______ -- (c) 무엇으로 묶어야 할까?
|
||||
______ COUNT(*) >= 3 -- (d) 묶은 뒤 거르는 키워드는?
|
||||
ORDER BY 제출수 DESC;
|
||||
|
||||
-- 기대 결과 (여러분 DB의 실제 숫자와는 다를 수 있어요):
|
||||
student_id | 제출수 | 평균점수
|
||||
------------+--------+----------
|
||||
3 | 5 | 91.2
|
||||
7 | 4 | 85.0
|
||||
12 | 3 | 78.6`;
|
||||
|
||||
const CODE_PRACTICE_ANSWER = `-- 정답 (풀어 본 뒤에만 펼쳐 보기!):
|
||||
-- (a) COUNT (b) NOT (c) student_id (d) HAVING
|
||||
|
||||
SELECT student_id,
|
||||
COUNT(*) AS 제출수,
|
||||
ROUND(AVG(score), 1) AS 평균점수
|
||||
FROM submission
|
||||
WHERE score IS NOT NULL
|
||||
GROUP BY student_id
|
||||
HAVING COUNT(*) >= 3
|
||||
ORDER BY 제출수 DESC;
|
||||
|
||||
-- 여기까지 술술 읽힌다면, 이 한 문장에
|
||||
-- 오늘 배운 집계·NULL·HAVING이 전부 들어 있다는 걸 눈치챘을 거예요.`;
|
||||
|
||||
const CODE_PSQL = `# 우리 플랫폼처럼 Docker로 PostgreSQL을 띄웠다면, 컨테이너 안 psql로 접속:
|
||||
docker exec -it <postgres컨테이너이름> psql -U <사용자> -d <DB이름>
|
||||
|
||||
# 접속되면 프롬프트가 이렇게 바뀌어요:
|
||||
mydb=#
|
||||
|
||||
# 몸풀기 — 테이블 목록 보기:
|
||||
\\dt
|
||||
|
||||
# 오늘 배운 것 바로 실험:
|
||||
EXPLAIN SELECT * FROM submission WHERE student_id = 7;
|
||||
|
||||
# 나갈 때는:
|
||||
\\q`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'GROUP BY와 집계' },
|
||||
{ n: 2, label: 'HAVING' },
|
||||
{ n: 3, label: '서브쿼리 맛보기' },
|
||||
{ n: 4, label: 'NULL의 함정' },
|
||||
{ n: 5, label: '인덱스' },
|
||||
{ n: 6, label: 'EXPLAIN' },
|
||||
{ n: 7, label: '트랜잭션과 ACID' },
|
||||
{ n: 8, label: '종합 실습' },
|
||||
];
|
||||
|
||||
export default function SqlIntermediatePage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 서버와 데이터</div>
|
||||
<h1>SQL 중급<br />— 세고, 묶고, 빨라지기</h1>
|
||||
<p>
|
||||
SELECT로 데이터를 <strong>꺼내는</strong> 법을 배웠다면, 이제
|
||||
<strong> 요약하고(집계) 빠르게 만드는(인덱스)</strong> 법을 배울 차례예요.
|
||||
우리 학습 플랫폼의 제출(submission) 데이터를 예제 삼아, 실무에서 매일 쓰는
|
||||
일곱 가지 무기를 하나씩 손에 쥐어 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 90분</span>
|
||||
<span className="chip">실습 2개 포함</span>
|
||||
<span className="chip">선수: SQL 입문</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="GROUP BY와 집계 함수" sub="행을 '팀'으로 묶어 요약하기">
|
||||
<p>
|
||||
SQL 입문에서 배운 SELECT는 행을 <strong>한 줄씩 그대로</strong> 보여줬어요.
|
||||
그런데 실무에서 진짜 자주 받는 질문은 이런 거예요 — <strong>"주차별 제출이 몇 건이야?"</strong>,
|
||||
"학생별 평균 점수는?". 행 하나하나가 아니라 <strong>묶음의 요약</strong>을 묻는 질문이죠.
|
||||
이때 쓰는 게 <span className="icode">GROUP BY</span>와 집계 함수입니다.
|
||||
</p>
|
||||
<p>
|
||||
반 전체 시험지를 <strong>주차별로 쌓아 나눈 다음</strong>, 더미마다 "몇 장?"(COUNT),
|
||||
"평균 몇 점?"(AVG) 하고 포스트잇 한 장씩 붙이는 장면을 상상하세요.
|
||||
시험지 수십 장이 더미당 <strong>요약 한 줄</strong>로 바뀝니다.
|
||||
</p>
|
||||
<Code>{CODE_GROUP_BY}</Code>
|
||||
<p>
|
||||
자주 쓰는 집계 함수는 다섯 개예요 — <span className="icode">COUNT</span>(개수),{' '}
|
||||
<span className="icode">AVG</span>(평균), <span className="icode">SUM</span>(합계),{' '}
|
||||
<span className="icode">MAX</span>/<span className="icode">MIN</span>(최대/최소).
|
||||
그리고 GROUP BY에는 초보자의 8할이 걸려 넘어지는 철칙이 하나 있습니다.
|
||||
</p>
|
||||
<Code>{CODE_GROUP_BY_RULE}</Code>
|
||||
<div className="tip">
|
||||
<b>기억법</b> GROUP BY를 쓰는 순간, 결과의 한 줄은 더 이상 '행'이 아니라
|
||||
<strong> '팀의 요약'</strong>이에요. SELECT에 쓸 수 있는 건 팀 이름(묶은 컬럼)과
|
||||
팀 성적표(집계 함수)뿐 — 팀원 개인 정보(그 외 컬럼)는 못 씁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="HAVING" sub="묶은 '팀'을 거르는 두 번째 필터">
|
||||
<p>
|
||||
"제출 수가 20건 넘는 주차<strong>만</strong>" 보고 싶으면 어떻게 할까요?
|
||||
<span className="icode"> WHERE COUNT(*) > 20</span>을 떠올렸다면 아주 자연스러운
|
||||
생각인데, 아쉽게도 에러가 납니다. WHERE는 <strong>묶기 전</strong>에 행 하나하나를
|
||||
검사하는 입장 검사라서, 그 시점엔 아직 셀 팀이 없거든요.
|
||||
팀을 만든 <strong>뒤에</strong> 팀 단위로 거르는 문이 따로 있어요 —{' '}
|
||||
<span className="icode">HAVING</span>입니다.
|
||||
</p>
|
||||
<Code>{CODE_HAVING}</Code>
|
||||
<p>
|
||||
정리하면 실행 순서는 <strong>WHERE(행 거르기) → GROUP BY(묶기) →
|
||||
HAVING(팀 거르기) → ORDER BY(정렬)</strong>. 동아리 오디션으로 치면
|
||||
WHERE는 <strong>지원 자격 심사</strong>(개인별), HAVING은 팀을 짠 뒤의{' '}
|
||||
<strong>팀 예선</strong>(팀별)이에요. 심사 대상이 다르니 문도 다른 거죠.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 실수</b> HAVING에 아무 조건이나 넣을 수 있다고 WHERE 대신 HAVING만
|
||||
쓰는 습관은 금물이에요. 행 단위 조건(<span className="icode">score IS NOT NULL</span> 같은)은
|
||||
WHERE에 두는 게 맞고, 보통 더 빠릅니다 — 묶기 전에 미리 걸러내면 묶을 양 자체가 줄거든요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="서브쿼리 맛보기" sub="쿼리 안의 쿼리 — 미리 우려내는 육수">
|
||||
<p>
|
||||
"<strong>평균보다</strong> 높은 점수의 제출만 보여줘"라는 요청을 받았다고 해요.
|
||||
문제는 평균이 몇 점인지 <strong>쿼리를 돌려 봐야 안다</strong>는 것. 두 번 나눠 실행해도
|
||||
되지만, SQL은 쿼리 안에 괄호로 <strong>다른 쿼리를 통째로</strong> 넣는 걸 허용합니다.
|
||||
이게 <strong>서브쿼리</strong>예요.
|
||||
</p>
|
||||
<Code>{CODE_SUBQUERY}</Code>
|
||||
<p>
|
||||
괄호 안이 <strong>먼저</strong> 실행돼 값(또는 목록)이 되고, 바깥 쿼리가 그 값을 재료로
|
||||
씁니다. 라면을 끓이기 전에 육수부터 우려내는 것과 같아요 — 육수(서브쿼리)가 준비돼야
|
||||
본 요리(바깥 쿼리)가 시작되죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>여기까지만</b> 서브쿼리는 JOIN과 섞이면 훨씬 깊은 세계가 열리는데,
|
||||
이번 코스에선 "괄호 안이 먼저 실행돼 재료가 된다"는 감각만 가져가면 충분해요.
|
||||
결과가 <strong>값 하나</strong>면 <span className="icode">=</span>·<span className="icode">></span>와,{' '}
|
||||
<strong>목록</strong>이면 <span className="icode">IN</span>과 짝지어 쓴다 — 이 두 패턴이 시작점입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="NULL의 함정" sub="'없음'이 아니라 '모름' — 그래서 위험한 값">
|
||||
<p>
|
||||
<span className="icode">NULL</span>은 0도, 빈 문자열도 아니에요.
|
||||
<strong> "값을 모름"</strong>입니다. 답안지의 빈칸이 "0점"이 아니라
|
||||
"아직 채점 안 됨"인 것처럼요. 이 미묘한 차이가 실무에서 진짜 버그를 만듭니다 —
|
||||
그것도 에러 없이 <strong>조용히 틀린 숫자</strong>를 내놓는 최악의 방식으로요.
|
||||
</p>
|
||||
<Code>{CODE_NULL}</Code>
|
||||
<p>
|
||||
특히 함정 ②를 다시 보세요. <span className="icode">AVG(score)</span>가 미채점(NULL)을
|
||||
빼고 평균을 낸다는 건, <strong>"미채점을 0점으로 칠 거냐 말 거냐"를 여러분이 정해야
|
||||
한다</strong>는 뜻이에요. DB는 물어보지 않고 그냥 빼 버리니까요. 통계 숫자가 이상하게
|
||||
나올 때 첫 번째 용의자는 언제나 NULL입니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>실전 체크리스트</b> 집계 쿼리를 쓰기 전에 스스로에게 물어보세요 —
|
||||
① 이 컬럼에 NULL이 있을 수 있나? ② 있다면 세어야 하나, 빼야 하나?
|
||||
③ 빼면 안 되면 <span className="icode">COALESCE</span>로 무슨 값으로 바꿀 건가?
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="인덱스 — 책의 '찾아보기'" sub="왜 빨라질까, 그리고 왜 남용하면 안 될까">
|
||||
<p>
|
||||
500쪽짜리 전공 책에서 '트랜잭션'이 나온 쪽을 찾는다고 해봐요. 방법은 두 가지 —
|
||||
1쪽부터 한 장씩 넘기거나, 책 뒤의 <strong>찾아보기(index)</strong>에서
|
||||
"트랜잭션 → 214쪽"을 보고 바로 펴거나. DB의 <strong>인덱스</strong>가 정확히
|
||||
후자입니다. 컬럼 값들을 <strong>미리 정렬해 둔 별도의 목록</strong>을 만들어 놓고,
|
||||
검색할 때 그 목록으로 위치를 바로 찾아가는 거예요.
|
||||
</p>
|
||||
<Code>{CODE_INDEX}</Code>
|
||||
<p>
|
||||
그럼 "전부 인덱스 걸면 되겠네?" — 안 됩니다. 찾아보기가 붙은 책은
|
||||
<strong> 본문을 고칠 때마다 찾아보기도 고쳐야</strong> 하잖아요.
|
||||
데이터를 넣고 고칠 때마다(INSERT/UPDATE) 인덱스 유지 비용이 들어서,
|
||||
인덱스가 많을수록 <strong>쓰기가 느려집니다</strong>. 읽기와 쓰기의 거래(trade-off)예요.
|
||||
그래서 원칙은 하나 — <strong>WHERE나 JOIN에 자주 등장하는 컬럼에만</strong> 겁니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>이미 하나 갖고 있어요</b> PRIMARY KEY(보통 <span className="icode">id</span>)에는
|
||||
PostgreSQL이 <strong>자동으로 인덱스</strong>를 만들어 줘요.
|
||||
<span className="icode"> WHERE id = 101</span>이 항상 빠른 이유가 이겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="EXPLAIN 훑어보기" sub="DB가 쓴 '실행 계획서' 읽는 법">
|
||||
<p>
|
||||
인덱스를 만들었는데 진짜 쓰이고 있을까요? 감으로 짐작할 필요 없어요.
|
||||
쿼리 앞에 <span className="icode">EXPLAIN</span>을 붙이면 PostgreSQL이
|
||||
"이 쿼리를 <strong>이런 방법으로 실행할 계획</strong>입니다" 하고 계획서를
|
||||
보여줍니다. 내비게이션이 출발 전에 경로를 미리 보여주는 것과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_EXPLAIN}</Code>
|
||||
<p>
|
||||
지금 단계에선 두 단어만 구분하면 충분해요 —{' '}
|
||||
<span className="icode">Seq Scan</span>(순차 스캔)이 보이면 "책을 한 장씩 넘기는 중",{' '}
|
||||
<span className="icode">Index Scan</span>이 보이면 "찾아보기를 쓰는 중".
|
||||
느린 쿼리를 만나면 <strong>EXPLAIN 붙이기 → Seq Scan 찾기 → 그 컬럼에 인덱스
|
||||
걸까 고민하기</strong> — 이 3단 콤보가 백엔드 개발자의 기본기입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>알아두기</b> 데이터가 아주 적을 땐 인덱스가 있어도 DB가 일부러 Seq Scan을
|
||||
택하기도 해요. 두 쪽짜리 유인물은 찾아보기 없이 그냥 넘겨 보는 게 빠르니까요.
|
||||
"Seq Scan = 무조건 나쁨"이 아니라 <strong>"데이터가 많은데 Seq Scan = 점검 신호"</strong>로 읽으세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="트랜잭션과 ACID 한 입" sub="'전부 아니면 전무'라는 약속">
|
||||
<p>
|
||||
여러 문장이 <strong>한 몸처럼</strong> 실행돼야 할 때가 있어요. 계좌이체가 대표 선수죠 —
|
||||
"내 잔액 감소"와 "상대 잔액 증가" 중 <strong>하나만</strong> 실행되고 서버가 꺼지면
|
||||
돈이 증발합니다. 이런 참사를 막는 장치가 <strong>트랜잭션</strong>이에요.
|
||||
<span className="icode"> BEGIN</span>으로 묶기 시작해서 <span className="icode">COMMIT</span>(확정)
|
||||
또는 <span className="icode">ROLLBACK</span>(전부 취소)으로 끝냅니다.
|
||||
</p>
|
||||
<Code>{CODE_TRANSACTION}</Code>
|
||||
<p>
|
||||
우리 스택에서도 매일 쓰이고 있어요 — Spring Boot에서{' '}
|
||||
<span className="icode">@Transactional</span>이 붙은 메서드는 안의 DB 작업 전체가
|
||||
트랜잭션 하나로 묶여서, 중간에 예외가 터지면 <strong>자동으로 ROLLBACK</strong>됩니다.
|
||||
어노테이션 한 줄 뒤에서 오늘 배운 BEGIN/COMMIT이 돌고 있는 거예요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>주의</b> COMMIT하기 전의 변경은 <strong>나에게만</strong> 보여요(격리성).
|
||||
psql에서 UPDATE를 했는데 웹 화면에 반영이 안 된다면, COMMIT을 안 했을 가능성부터
|
||||
의심하세요. 트랜잭션을 열어 둔 채 자리를 뜨는 건 금고 문을 열어 두고 퇴근하는 것과 같습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="종합 실습 — '학생별 제출 수' 완성하기" sub="오늘 배운 무기 전부 꺼내기">
|
||||
<p>
|
||||
멘토가 이런 요청을 했다고 해요 — <strong>"성실하게 제출하는 학생이 누군지 보고 싶어.
|
||||
채점 끝난 것만 세서, 3건 이상 낸 학생을 제출 많은 순으로 뽑아줘."</strong>
|
||||
이 한 문장 안에 NULL 처리(4섹션), GROUP BY(1섹션), HAVING(2섹션)이 전부 들어 있어요.
|
||||
빈칸 네 개를 채워 쿼리를 완성해 보세요.
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 빈칸을 종이나 메모장에 먼저 채운 뒤, 아래 정답과
|
||||
비교하세요. 하나라도 틀렸다면 해당 섹션으로 돌아가 그 부분만 다시 읽고 오기 —
|
||||
그게 가장 빠른 복습 경로예요.
|
||||
</div>
|
||||
<Code>{CODE_PRACTICE_ANSWER}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 이제 진짜 DB에서 돌려 봅시다. 우리 플랫폼은
|
||||
PostgreSQL을 Docker로 띄우죠 — 컨테이너 속 <span className="icode">psql</span>에
|
||||
접속해서 완성한 쿼리와 <span className="icode">EXPLAIN</span>을 직접 실행해 보세요.
|
||||
(운영 DB 말고 <strong>내 로컬 개발 DB</strong>에서만! SELECT와 EXPLAIN은 안전하지만,
|
||||
습관을 처음부터 바르게.)
|
||||
</div>
|
||||
<Code>{CODE_PSQL}</Code>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🗃️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 데이터를 <strong>묶어서 요약하고</strong>(GROUP BY·HAVING),
|
||||
NULL에 속지 않고, 인덱스와 EXPLAIN으로 <strong>왜 느린지 진단</strong>하고,
|
||||
트랜잭션으로 <strong>안전하게 바꾸는</strong> 법까지 알게 됐어요.
|
||||
다음 걸음은 두 갈래 — 여러 테이블을 이어 붙이는 <strong>JOIN</strong>을 파고들거나,{' '}
|
||||
<Link to="/learn/backend"><strong>백엔드 기초</strong></Link> 코스에서
|
||||
Spring Boot가 이 쿼리들을 어떻게 대신 써 주는지(JPA) 이어서 배워 보세요.
|
||||
완성한 실습 쿼리는 과제 게시판에 제출해 멘토 피드백을 받아 보는 것, 잊지 말고요!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
368
frontend/src/pages/courses/TcpUdpPage.jsx
Normal file
368
frontend/src/pages/courses/TcpUdpPage.jsx
Normal file
@ -0,0 +1,368 @@
|
||||
// 이 파일이 하는 일: "TCP와 UDP" 코스 — 데이터가 패킷으로 쪼개지는 순간부터
|
||||
// 3-way 핸드셰이크, 순서 보장·재전송, UDP의 속도 철학, 포트 번호, netstat 실습,
|
||||
// "채팅은 TCP·게임은 UDP" 토론까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_PACKET = `큰 데이터는 통째로 못 가요 — 패킷(작은 택배 상자)으로 쪼개서 보냅니다.
|
||||
|
||||
[10MB짜리 사진 한 장]
|
||||
│ 잘게 쪼개기 (보통 상자 하나 ≈ 1,500바이트!)
|
||||
▼
|
||||
┌────┐┌────┐┌────┐ ┌────┐
|
||||
│ #1 ││ #2 ││ #3 │ ... │#7000│ ← 상자마다 번호표(순서 번호)
|
||||
└────┘└────┘└────┘ └────┘
|
||||
│ 각자 알아서 길을 찾아 출발 (경로가 서로 달라도 OK)
|
||||
▼
|
||||
[받는 쪽] 번호 순서대로 다시 쌓기 → 원래 사진 복원!
|
||||
|
||||
왜 쪼갤까?
|
||||
1) 거대한 트럭 한 대가 길을 독점하면 다른 차(남의 데이터)가 못 지나가요.
|
||||
2) 상자 하나가 분실되면 그 상자만 다시 보내면 돼요. 통째로 다시 안 보내도 됨!`;
|
||||
|
||||
const CODE_HANDSHAKE = `TCP 3-way 핸드셰이크 — 연결 맺기 연극 (등장인물: 내 PC, 서버)
|
||||
|
||||
내 PC 서버
|
||||
│ │
|
||||
│── ① SYN ────────────────────────────▶│
|
||||
│ "저기요, 대화 좀 할 수 있을까요?" │
|
||||
│ │
|
||||
│◀─────────────────────── ② SYN+ACK ──│
|
||||
│ "네 들려요! 저도 준비됐어요. 그쪽도 │
|
||||
│ 제 목소리 들리나요?" │
|
||||
│ │
|
||||
│── ③ ACK ────────────────────────────▶│
|
||||
│ "잘 들려요! 그럼 시작할게요." │
|
||||
│ │
|
||||
│========= 연결 수립! 데이터 전송 시작 =====│
|
||||
|
||||
세 번 오가야 하는 이유: 양쪽 모두
|
||||
"내 말이 상대에게 닿는다 + 상대 말이 나에게 닿는다"
|
||||
두 가지를 전부 확인해야 안심하고 보낼 수 있으니까요.`;
|
||||
|
||||
const CODE_RETRANSMIT = `순서 보장 + 재전송 — TCP가 '확실한 택배'인 이유
|
||||
|
||||
보내기: #1 #2 #3 #4 #5
|
||||
도착: #1 #2 ✕ #4 #5 ← #3이 길에서 실종!
|
||||
|
||||
받는 쪽: "#3 못 받았어요. 다시 보내 주세요." (ACK로 알려줌)
|
||||
보내는 쪽: #3만 골라서 재전송 → #3 도착!
|
||||
|
||||
받는 쪽 조립: #1 #2 #3 #4 #5 ← 늦게 온 #3을 제자리에 끼워 넣음
|
||||
→ 원본과 100% 동일한 데이터 완성. 이게 TCP의 약속이에요.
|
||||
|
||||
대가(트레이드오프):
|
||||
- 확인 답장(ACK)을 기다리는 시간
|
||||
- 재전송하는 동안 뒤 상자들이 대기하는 시간
|
||||
→ 확실한 만큼 '느려질 수 있다'는 비용을 냅니다.`;
|
||||
|
||||
const CODE_UDP = `UDP — 인사도 확인도 없이, 그냥 던진다!
|
||||
|
||||
TCP (전화 통화) UDP (확성기 방송)
|
||||
────────────────────── ──────────────────────
|
||||
연결부터 맺음 (3-way) 연결? 그런 거 없음
|
||||
받았는지 확인(ACK) 확인 안 함
|
||||
분실 시 재전송 분실돼도 그냥 다음 것 전송
|
||||
순서 맞춰 조립 온 순서대로 그냥 사용
|
||||
헤더 20바이트~ 헤더 딱 8바이트
|
||||
|
||||
UDP가 빠른 진짜 이유 = "안 하는 일이 많아서"예요.
|
||||
생방송에서 0.5초 전 화면 조각이 늦게 오면?
|
||||
→ 기다렸다 틀면 방송이 계속 밀려요. 버리고 다음 장면!
|
||||
게임에서 0.1초 전 캐릭터 위치가 유실되면?
|
||||
→ 재전송받아 봤자 이미 과거. 최신 위치가 더 소중!`;
|
||||
|
||||
const CODE_PORTS = `잘 알려진 포트 — 전부 우리 회사(AWESOMEDEV)에서 매일 쓰는 것들!
|
||||
|
||||
포트 프로토콜 무엇 우리 회사에서는
|
||||
────────────────────────────────────────────────────────
|
||||
22 TCP SSH AWS EC2(서울) 서버에 원격 접속할 때
|
||||
80 TCP HTTP Caddy가 받아서 443으로 돌려보냄(리다이렉트)
|
||||
443 TCP HTTPS 학습 플랫폼·Gitea(edu.awesomedevapp.com) 접속
|
||||
5432 TCP PostgreSQL 백엔드(Spring Boot)가 DB랑 대화하는 문
|
||||
8080 TCP HTTP(대체) Spring Boot 개발 서버의 단골 기본 포트
|
||||
|
||||
공통점 발견? 전부 TCP예요.
|
||||
접속·웹·DB는 "한 바이트도 틀리면 안 되는" 일이라서요.
|
||||
(0~1023 = 잘 알려진 포트, 49152~ = 임시 포트: 내 PC가
|
||||
서버에 접속할 때 자동으로 배정받는 '내 쪽 방 번호'입니다.)`;
|
||||
|
||||
const CODE_NETSTAT = `# 내 PC가 '지금' 맺고 있는 TCP 연결 전부 보기 (Windows 터미널에서):
|
||||
netstat -ano | findstr ESTABLISHED
|
||||
|
||||
프로토콜 로컬 주소 원격 주소 상태 PID
|
||||
TCP 192.168.0.5:52310 x.x.x.x:443 ESTABLISHED 1234
|
||||
TCP 192.168.0.5:52488 y.y.y.y:443 ESTABLISHED 5678
|
||||
...
|
||||
|
||||
읽는 법:
|
||||
- 로컬 주소의 52310 = 내 쪽 임시 포트 (자동 배정!)
|
||||
- 원격 주소의 :443 = 상대 서버의 HTTPS 방 번호
|
||||
- ESTABLISHED = 3-way 악수가 끝나고 연결이 살아 있다는 뜻!
|
||||
|
||||
# PID(맨 오른쪽 숫자)가 어떤 프로그램인지 알아내기:
|
||||
tasklist | findstr 1234
|
||||
→ chrome.exe ... ← "아, 이 연결은 브라우저가 만든 거구나!"`;
|
||||
|
||||
const CODE_DEBATE = `토론거리 — "왜 채팅은 TCP, 게임은 UDP일까?"
|
||||
|
||||
상황 1) 채팅 메시지 "내일 3시에 보자"에서 '3'이 유실된다면?
|
||||
→ "내일 시에 보자" ... 약속이 통째로 어긋나요.
|
||||
→ 한 글자도 잃으면 안 됨 = 느려도 TCP.
|
||||
|
||||
상황 2) 게임에서 상대 캐릭터의 0.1초 전 좌표가 유실된다면?
|
||||
→ 어차피 0.1초 뒤 좌표가 곧 도착. 옛날 좌표는 쓸모없음.
|
||||
→ 빠른 최신 정보가 생명 = 유실 감수하고 UDP.
|
||||
|
||||
더 깊은 토론 질문 (멘토·동기들과 이야기해 보세요):
|
||||
Q1. 게임의 '아이템 구매' 버튼도 UDP로 보내도 될까요?
|
||||
(힌트: 결제가 유실되면...?)
|
||||
Q2. 영상통화 중 '화면'과 '채팅창'이 같이 있다면 각각 뭘 쓸까요?
|
||||
Q3. 요즘 웹의 HTTP/3는 오히려 UDP 위에 지어졌어요.
|
||||
"믿을 수 없는 UDP 위에 믿을 수 있는 층을 직접 쌓는" 발상 —
|
||||
왜 그런 선택을 했을지 상상해 보세요.
|
||||
|
||||
정답보다 중요한 것: "이 데이터는 유실돼도 되는가?"를
|
||||
스스로 물어보는 습관입니다. 그게 프로토콜 선택의 전부예요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '패킷 — 쪼개고 조립하기' },
|
||||
{ n: 2, label: '3-way 핸드셰이크' },
|
||||
{ n: 3, label: '순서 보장과 재전송' },
|
||||
{ n: 4, label: 'UDP는 왜 빠른가' },
|
||||
{ n: 5, label: '포트 번호 총정리' },
|
||||
{ n: 6, label: 'netstat 실습' },
|
||||
{ n: 7, label: '토론: 채팅 vs 게임' },
|
||||
];
|
||||
|
||||
export default function TcpUdpPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 다루는 두 주인공 소개 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>TCP와 UDP<br />— 확실한 택배와 빠른 방송</h1>
|
||||
<p>
|
||||
인터넷의 데이터 배송에는 딱 두 가지 철학이 있어요 — "느려도 한 개도 잃지 않겠다"는
|
||||
TCP와 "몇 개 잃어도 빠르게 가겠다"는 UDP. 이 코스에서는 두 방식이 실제로 어떻게
|
||||
움직이는지 따라가고, <strong>내 PC의 진짜 연결 목록</strong>을 직접 열어 봅니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 토론 1개</span>
|
||||
<span className="chip">선수 코스: 네트워크의 이해</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="패킷 — 쪼개고, 번호 붙이고, 다시 조립하기" sub="이삿짐 트럭이 아니라 택배 상자 수천 개">
|
||||
<p>
|
||||
사진 한 장, 영상 한 편 — 뭐든 네트워크를 지날 때는 <strong>패킷</strong>이라는
|
||||
작은 조각으로 쪼개져서 갑니다. 이삿짐을 트럭 한 대에 전부 싣는 게 아니라,
|
||||
택배 상자 수천 개에 <strong>번호표를 붙여</strong> 나눠 보내는 거예요.
|
||||
상자들이 서로 다른 길로 가도, 도착지에서 번호 순서대로 쌓으면 원본이 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_PACKET}</Code>
|
||||
<p>
|
||||
그런데 여기서 진짜 질문이 시작돼요. <strong>상자가 길에서 사라지면? 순서가 뒤죽박죽
|
||||
도착하면?</strong> 이 문제를 "끝까지 책임진다"가 TCP, "신경 안 쓰고 계속 던진다"가
|
||||
UDP입니다. 이제 한 명씩 만나 볼게요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>미리 감 잡기</b> 우리 학습 플랫폼의 페이지 하나도 패킷 수십~수백 개로 나뉘어
|
||||
도착한 거예요. 지금 보고 있는 이 글자들도 상자에 담겨 왔다가 방금 조립된 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="TCP 3-way 핸드셰이크" sub="데이터를 보내기 전, 세 마디 악수">
|
||||
<p>
|
||||
TCP는 데이터를 보내기 전에 반드시 <strong>연결부터 맺어요</strong>. 전화를 걸면
|
||||
"여보세요?" — "네, 여보세요!" — "아 들리시죠? 저 말할게요"처럼, 서로 목소리가
|
||||
닿는지 확인하고 나서야 본론을 시작하죠. 이 세 마디를
|
||||
<strong> 3-way 핸드셰이크</strong>(세 번의 악수)라고 부릅니다.
|
||||
</p>
|
||||
<Code>{CODE_HANDSHAKE}</Code>
|
||||
<div className="olist">
|
||||
<ol>
|
||||
<li><strong>SYN</strong> — 내 PC가 "대화 시작하고 싶어요" 하고 손을 내밀어요.</li>
|
||||
<li><strong>SYN+ACK</strong> — 서버가 "네 잘 받았어요(ACK), 저도 시작할게요(SYN)" 하고 맞잡아요.</li>
|
||||
<li><strong>ACK</strong> — 내 PC가 "확인했어요!" 하고 악수를 완성해요. 이제 데이터 출발!</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
왜 두 번이 아니라 세 번일까요? 두 번만 오가면 <strong>서버는</strong> 자기 답장이
|
||||
내 PC에 닿았는지 알 수 없어요. 양쪽 모두 "보내기·받기가 다 된다"를 확인하려면
|
||||
최소 세 번이 필요합니다. 브라우저에 <span className="icode">https://edu.awesomedevapp.com</span>을
|
||||
치는 순간, 이 악수가 서버의 443번 방 앞에서 눈 깜짝할 새 벌어져요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 오해</b> 핸드셰이크는 '로그인'이 아니에요. 아이디·비밀번호와 무관하게,
|
||||
"회선 양쪽이 서로 통한다"는 것만 확인하는 <strong>배송 준비 절차</strong>입니다.
|
||||
로그인은 그 위에서 오가는 데이터(HTTP)의 일이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="순서 보장과 재전송" sub="잃어버리면 다시, 뒤섞이면 제자리에">
|
||||
<p>
|
||||
연결을 맺었으면 이제 상자를 보냅니다. TCP의 자랑은 여기서부터예요.
|
||||
받는 쪽은 상자를 받을 때마다 <strong>"몇 번까지 잘 받았어요"</strong> 하고
|
||||
영수증(ACK)을 보내 줘요. 그래서 보내는 쪽은 어떤 상자가 실종됐는지 바로 알고,
|
||||
<strong> 그 상자만 골라 다시</strong> 보냅니다.
|
||||
</p>
|
||||
<Code>{CODE_RETRANSMIT}</Code>
|
||||
<p>
|
||||
순서가 뒤섞여 도착해도 문제없어요 — 상자마다 붙은 번호 덕분에 받는 쪽이
|
||||
<strong> 제자리에 끼워 넣어</strong> 조립하니까요. 그래서 TCP 위에서는 파일이 절대
|
||||
중간이 빠진 채 완성되지 않습니다. Gitea에서 <span className="icode">git clone</span>을
|
||||
받을 때 코드가 한 글자도 안 깨지는 이유, 백엔드가 PostgreSQL에서 읽어 온 데이터가
|
||||
정확한 이유가 전부 이 재전송·순서 보장이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>기억할 한 줄</b> TCP의 신뢰성은 공짜가 아니에요. "확인하고, 기다리고, 다시 보내는"
|
||||
시간을 지불합니다. 이 비용을 못 견디는 서비스들이 다음 섹션의 주인공을 찾아가요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="UDP는 왜 빠른가" sub="비결은 '안 하는 일이 많다'는 것">
|
||||
<p>
|
||||
UDP는 TCP가 하는 일을 거의 다 <strong>생략</strong>해요. 악수도 없고, 영수증도 없고,
|
||||
재전송도 없어요. 확성기 방송처럼 그냥 계속 내보낼 뿐이죠. 무책임해 보이지만,
|
||||
어떤 데이터에게는 이게 <strong>최고의 배려</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_UDP}</Code>
|
||||
<p>
|
||||
생방송을 생각해 보세요. 0.5초 전 화면 조각이 유실됐다고 재전송을 기다리면,
|
||||
방송은 그만큼 계속 <strong>밀려요</strong>. 화면이 잠깐 뭉개져도 다음 장면으로
|
||||
넘어가는 게 시청자에게 낫죠. 게임도 같아요 — 0.1초 전 상대 위치를 되살려 봤자
|
||||
이미 과거입니다. <strong>"낡은 정보를 정확히 받기 vs 최신 정보를 빨리 받기"</strong>에서
|
||||
후자를 고른 서비스들이 UDP를 씁니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>주의</b> "UDP = 항상 빠르고 좋음"이 아니에요. 유실을 감수할 수 없는 데이터
|
||||
(파일, 결제, 메시지)에 UDP를 쓰면 깨진 결과를 얻습니다. 속도는 <strong>신뢰성을
|
||||
포기한 대가</strong>로 얻은 것임을 잊지 마세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="포트 번호 총정리" sub="우리가 매일 두드리는 방 번호들">
|
||||
<p>
|
||||
"네트워크의 이해" 코스에서 <strong>IP는 건물 주소, 포트는 방 번호</strong>라고
|
||||
배웠죠. TCP·UDP 헤더에 이 방 번호가 적혀 있어서, 도착한 패킷이 어느 프로그램의
|
||||
문을 두드릴지 정해집니다. 유명한 서비스들은 방 번호가 <strong>전 세계 공통으로
|
||||
예약</strong>되어 있어요 — 그리고 그 목록이 곧 우리 회사 인프라 지도입니다.
|
||||
</p>
|
||||
<Code>{CODE_PORTS}</Code>
|
||||
<p>
|
||||
예를 들어 여러분이 EC2 서버에 <span className="icode">ssh</span>로 들어가면 22번,
|
||||
이 플랫폼에 접속하면 443번, 백엔드가 DB에 질의하면 5432번 문이 열려요.
|
||||
로컬에서 Spring Boot를 <span className="icode">./mvnw spring-boot:run</span>으로 띄우면
|
||||
"내 PC 건물의 8080호"에 세 들어 사는 셈이고, 그래서 브라우저에
|
||||
<span className="icode">localhost:8080</span>을 치는 거예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 로컬에서 백엔드를 켜 둔 상태로 브라우저에
|
||||
<span className="icode">localhost:8080</span>을 쳐 보고, 백엔드를 끈 뒤 다시 쳐 보세요.
|
||||
꺼진 뒤엔 "연결할 수 없음"이 떠요 — <strong>그 방에 아무도 안 산다</strong>는 뜻이죠.
|
||||
같은 건물(내 PC)이라도 방(포트)마다 세입자(프로그램)가 따로라는 걸 몸으로 확인하는 겁니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="netstat 실습 — 내 PC의 연결 명단 열람" sub="지금 이 순간의 3-way 악수 결과들">
|
||||
<p>
|
||||
이론은 충분해요. 이제 내 PC가 <strong>지금 실제로 맺고 있는 TCP 연결들</strong>을
|
||||
직접 열어 봅시다. <span className="icode">netstat</span>은 "network statistics"의
|
||||
줄임말로, 연결 명단을 보여 주는 터미널 명령이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 브라우저로 우리 플랫폼(<span className="icode">edu.awesomedevapp.com</span>)을
|
||||
열어 둔 채, 터미널에서 아래 명령을 실행해 보세요. <span className="kbd">Win</span>+<span className="kbd">R</span> →
|
||||
<span className="icode">cmd</span> → 엔터로 터미널을 열 수 있어요.
|
||||
</div>
|
||||
<Code>{CODE_NETSTAT}</Code>
|
||||
<div className="olist">
|
||||
<ol>
|
||||
<li>원격 주소가 <span className="icode">:443</span>인 줄을 찾아보세요 — 전부 HTTPS 연결이에요.</li>
|
||||
<li>로컬 주소의 5만 번대 포트에 주목 — 내 쪽 <strong>임시 포트</strong>가 자동 배정된 거예요.</li>
|
||||
<li><span className="icode">tasklist</span>로 PID의 정체를 밝혀 보세요. 대부분 브라우저일 거예요.</li>
|
||||
<li>브라우저를 전부 끄고 다시 실행 — <span className="icode">ESTABLISHED</span> 줄이 확 줄어드는 걸 관찰!</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
여기서 <span className="icode">ESTABLISHED</span> 한 줄 한 줄이 섹션 2에서 배운
|
||||
3-way 악수가 <strong>성공적으로 끝난 흔적</strong>입니다. 참고로 UDP는 연결이라는
|
||||
개념이 없어서 이 명단에 '상태'가 안 떠요 — 명단 자체가 TCP의 철학을 보여 주는 셈이죠.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="토론: 왜 채팅은 TCP, 게임은 UDP인가" sub="프로토콜 선택은 결국 '유실돼도 되는가'라는 질문">
|
||||
<p>
|
||||
마지막 섹션은 정답 암기가 아니라 <strong>토론</strong>이에요. 두 상황을 놓고
|
||||
"이 데이터가 유실되면 무슨 일이 벌어지나?"를 따져 보면, 프로토콜 선택의
|
||||
기준이 스스로 보이기 시작합니다.
|
||||
</p>
|
||||
<Code>{CODE_DEBATE}</Code>
|
||||
<p>
|
||||
우리 플랫폼도 같은 기준으로 만들어졌어요. 로그인, 과제 제출, 게시글 저장 —
|
||||
전부 <strong>한 바이트도 잃으면 안 되는</strong> 데이터라서 HTTPS(TCP 위)로
|
||||
오갑니다. 여러분이 나중에 실시간 기능(화상 멘토링, 라이브 코딩 중계 같은)을
|
||||
설계하게 되면, 그때 비로소 UDP 계열을 꺼내 들 차례가 오는 거예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>공부 팁</b> Q1~Q3에 대한 내 생각을 말로 정리해서 멘토나 동기에게 설명해 보세요.
|
||||
"게임인데 왜 이건 TCP지?"라고 <strong>반박당해 보는 것</strong>이 이 코스 최고의
|
||||
마무리 복습입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📦 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 데이터가 상자(패킷)로 쪼개지고, 세 번의 악수로 길을 열고, 잃어버리면
|
||||
다시 보내는 <strong>TCP</strong>와 — 그 모든 걸 생략하고 속도를 얻은
|
||||
<strong> UDP</strong>를 구분할 수 있어요. 그리고 22·80·443·5432·8080이라는
|
||||
방 번호가 우리 회사 어디에서 쓰이는지도요. 아직
|
||||
<Link to="/learn/network"><strong> 네트워크의 이해</strong></Link> 코스를 안 봤다면
|
||||
먼저 다녀오세요 — IP·DNS·HTTP가 이 코스의 앞 이야기입니다. 다 봤다면
|
||||
netstat 실습 결과를 캡처해서 멘토에게 공유하는 것으로 이번 코스 과제를 마무리하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
351
frontend/src/pages/courses/ToolDbeaverPage.jsx
Normal file
351
frontend/src/pages/courses/ToolDbeaverPage.jsx
Normal file
@ -0,0 +1,351 @@
|
||||
// 이 파일이 하는 일: "DBeaver로 DB 들여다보기" 코스 — 지금까지 명령어로만 만나던
|
||||
// PostgreSQL을 GUI 도구(DBeaver)로 열어 보는 부록 코스. 설치 → 로컬 DB 연결 →
|
||||
// ERD → 데이터 보기 → SQL 편집기 → CSV 내보내기까지 7개 섹션으로 안내하는 정적 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip, warn 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 접속 정보·SQL 같은 고정 텍스트는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 코드 상수들 ──
|
||||
|
||||
const CODE_WHY_GUI = `psql (터미널) DBeaver (GUI)
|
||||
──────────────────────── ────────────────────────
|
||||
글자로만 대화 표·그림으로 대화
|
||||
테이블 목록? → \\dt 입력 왼쪽 트리에서 폴더처럼 클릭
|
||||
데이터 보기? → SELECT 입력 테이블 더블클릭 → 엑셀처럼 표가 뜸
|
||||
구조 파악? → 머릿속으로 상상 ERD 탭 → 관계도가 그림으로 짠!
|
||||
|
||||
→ 둘 다 결국 같은 DB에 말을 겁니다. DBeaver도 속으로는
|
||||
SQL을 만들어 보내요. 리모컨이 생겼다고 TV가 바뀌진 않죠.`;
|
||||
|
||||
const CODE_CONNECTION = `우리 로컬 개발 DB 접속 정보 (docker-compose.yml에 있는 그 값!)
|
||||
|
||||
Host : localhost ← 내 PC에서 돌고 있으니까
|
||||
Port : 5432 ← PostgreSQL의 기본 방 번호
|
||||
Database : mirim
|
||||
Username : mirim
|
||||
Password : mirim123 ← 로컬 개발용이라 공개돼 있는 값이에요
|
||||
|
||||
※ 연결 전에 DB 컨테이너가 켜져 있어야 해요:
|
||||
프로젝트 폴더에서 docker compose up -d
|
||||
확인은 docker ps (mirim-postgres가 보이면 OK)`;
|
||||
|
||||
const CODE_TREE = `연결에 성공하면 왼쪽 '데이터베이스 탐색기'에 이런 트리가 생겨요:
|
||||
|
||||
mirim (연결 이름)
|
||||
└── Databases
|
||||
└── mirim
|
||||
└── Schemas
|
||||
└── public ← 우리 테이블은 다 여기!
|
||||
├── Tables
|
||||
│ ├── users (수습 개발자 계정)
|
||||
│ ├── assignment (주차별 과제)
|
||||
│ └── submission (과제 제출물)
|
||||
├── Views
|
||||
└── ...
|
||||
|
||||
→ 폴더 탐색기 감각 그대로예요. 테이블을 더블클릭하면
|
||||
오른쪽에 탭이 열립니다.`;
|
||||
|
||||
const CODE_TABLE_TABS = `테이블을 더블클릭하면 열리는 탭 3총사:
|
||||
|
||||
Properties(속성) : 열 이름·타입·NOT NULL — 테이블의 '설계도'
|
||||
Data(데이터) : 실제 행들이 엑셀처럼 표로 — 오늘의 주인공
|
||||
ER Diagram : 이 테이블과 연결된 이웃 테이블 관계도
|
||||
|
||||
Data 탭에서 되는 것들:
|
||||
· 열 머리글 클릭 → 그 열 기준 정렬
|
||||
· 열 머리글 우클릭 → Filter → 조건 걸어 골라 보기
|
||||
· 셀 더블클릭 → 값 수정 (엔터만으론 저장 안 됨!)
|
||||
· 아래쪽 Save(💾) → 그때서야 진짜 DB에 반영`;
|
||||
|
||||
const CODE_SQL_EDITOR = `-- SQL 편집기(Ctrl+])에 이걸 붙여넣고,
|
||||
-- 실행할 줄에 커서를 두고 Ctrl+Enter!
|
||||
|
||||
-- 1) 수습 개발자 명단 (SQL 입문 코스에서 본 그 쿼리)
|
||||
SELECT name, track FROM users;
|
||||
|
||||
-- 2) 1주차 과제만 골라 보기
|
||||
SELECT title FROM assignment WHERE week = 1;
|
||||
|
||||
-- 3) 제출물이 몇 개나 쌓였나?
|
||||
SELECT count(*) FROM submission;
|
||||
|
||||
-- 결과가 아래쪽에 표로 뜨고, 그 표도 Data 탭처럼
|
||||
-- 정렬·필터가 됩니다. 터미널 psql과 비교하면 신세계죠?`;
|
||||
|
||||
const CODE_EXPORT = `데이터 내보내기 — 결과를 CSV 파일로 들고 나가기
|
||||
|
||||
방법: Data 탭(또는 쿼리 결과 표)에서
|
||||
우클릭 → Export data → CSV 선택 → 저장 위치 지정 → 완료
|
||||
|
||||
CSV가 뭐냐면:
|
||||
name,track
|
||||
김수습,frontend
|
||||
이신입,backend
|
||||
→ 쉼표로 구분된 그냥 텍스트 파일. 그래서 엑셀·구글시트가
|
||||
바로 열 수 있고, 멘토에게 "이번 주 제출 현황"을 표로
|
||||
공유할 때 딱 좋아요.`;
|
||||
|
||||
const CODE_PRACTICE_ME = `실습 ②: 내 계정 행을 DB에서 직접 찾아라!
|
||||
|
||||
1) users 테이블 더블클릭 → Data 탭
|
||||
2) username 열 머리글 우클릭 → Filter → Custom Filter
|
||||
조건: username = '내아이디'
|
||||
(또는 SQL 편집기에서)
|
||||
SELECT * FROM users WHERE username = '내아이디';
|
||||
3) 내 행이 딱 한 줄 나오면 성공!
|
||||
4) 그 화면을 스크린샷(Win+Shift+S) → 과제 게시판에 제출
|
||||
|
||||
관찰 포인트: password_hash 열에 내 비밀번호가
|
||||
'그대로' 있나요? 아니죠 — 왜 암호화(해시)돼 있는지는
|
||||
SQL 입문 코스의 보안 이야기를 다시 읽어 보세요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'DBeaver가 뭐죠?' },
|
||||
{ n: 2, label: '설치하기' },
|
||||
{ n: 3, label: '로컬 DB 연결' },
|
||||
{ n: 4, label: 'ERD로 한눈에' },
|
||||
{ n: 5, label: '데이터 GUI로 보기' },
|
||||
{ n: 6, label: 'SQL 편집기' },
|
||||
{ n: 7, label: 'CSV 내보내기 & 실습' },
|
||||
];
|
||||
|
||||
export default function ToolDbeaverPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어떤 도구를, 왜 배우는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 부록 · 도구</div>
|
||||
<h1>DBeaver로 DB 들여다보기</h1>
|
||||
<p>
|
||||
지금까지 깜깜한 터미널에서 글자로만 대화하던 PostgreSQL에{' '}
|
||||
<strong>눈과 손</strong>을 달아 봅니다. DBeaver라는 무료 GUI 도구로 우리
|
||||
플랫폼의 진짜 테이블을 폴더처럼 열고, 표처럼 보고, 그림(ERD)으로 펼쳐 볼 거예요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 40분</span>
|
||||
<span className="chip">실습 2개 + 스크린샷 제출 1개</span>
|
||||
<span className="chip">준비물: Docker로 띄운 로컬 DB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="DBeaver가 뭐죠?" sub="DB에 눈을 달아 주는 만능 리모컨">
|
||||
<p>
|
||||
<Link to="/learn/sql"><strong>SQL 입문</strong></Link> 코스에서 우리는
|
||||
터미널로 DB와 대화했어요. 잘 통하긴 하는데, 매번 글자를 쳐야 하고 결과도
|
||||
글자로만 와서 <strong>전체 그림</strong>이 잘 안 보이죠. 라디오로 축구
|
||||
중계를 듣는 느낌이랄까요.
|
||||
</p>
|
||||
<p>
|
||||
<strong>DBeaver</strong>(디비버)는 DB를 화면으로 보여 주는 <strong>GUI
|
||||
클라이언트</strong>예요. 테이블은 폴더 트리로, 데이터는 엑셀 같은 표로,
|
||||
테이블 사이 관계는 그림으로 보여 줍니다. 라디오 중계가 TV 중계로 바뀌는 거죠.
|
||||
PostgreSQL뿐 아니라 거의 모든 DB에 꽂아 쓸 수 있어서, 실무에서 개발자들이
|
||||
정말 널리 쓰는 도구예요.
|
||||
</p>
|
||||
<Code>{CODE_WHY_GUI}</Code>
|
||||
<div className="tip">
|
||||
<b>오해 방지</b> DBeaver가 DB 그 자체는 아니에요. DB(PostgreSQL)는 우리
|
||||
Docker 컨테이너 안에서 계속 돌고 있고, DBeaver는 거기에 <strong>접속해서
|
||||
보여 주기만</strong> 하는 창문입니다. 창문을 닫아도(DBeaver 종료) 방(DB)은
|
||||
그대로예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="설치하기" sub="Community 에디션 — 무료면 충분해요">
|
||||
<p>
|
||||
DBeaver는 <strong>Community 에디션이 무료</strong>고, 우리가 배울 기능은
|
||||
전부 무료판에 들어 있어요. 결제 화면이 보인다면 유료판(PRO) 페이지로 잘못
|
||||
들어간 거니까 뒤로 나오세요.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>공식 사이트(dbeaver.io)에서 <strong>Community Edition</strong> → Windows 설치 파일을 내려받아요.</li>
|
||||
<li>설치 마법사는 기본값으로 <span className="kbd">Next</span>만 눌러도 됩니다.</li>
|
||||
<li>첫 실행 때 "샘플 DB를 만들까요?" 하고 물으면 <strong>No</strong> — 우리는 진짜 우리 DB에 붙을 거니까요.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>포터블 팁</b> 학교 PC처럼 설치가 제한된 환경이면 zip(포터블) 버전을 받아
|
||||
압축만 풀어도 실행돼요. 단, Java로 만들어진 프로그램이라 첫 실행이 조금
|
||||
느린 건 정상입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="우리 로컬 PostgreSQL에 연결하기" sub="host·port·db·user — 네 칸만 채우면 끝">
|
||||
<p>
|
||||
연결(Connection)은 DB에 <strong>전화를 거는 일</strong>이에요. 어느 건물
|
||||
(host)의 몇 호(port)로 걸어서, 어떤 방(database)에, 누구(user)라고 밝히고
|
||||
들어갈지 — 네트워크 코스에서 배운 IP·포트가 여기서 그대로 쓰입니다.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>먼저 DB가 켜져 있는지 확인 — 프로젝트 폴더에서 <span className="icode">docker compose up -d</span>.</li>
|
||||
<li>DBeaver에서 왼쪽 위 <strong>플러그 모양(새 연결)</strong> 아이콘 클릭 → <strong>PostgreSQL</strong> 선택.</li>
|
||||
<li>아래 접속 정보를 그대로 입력해요.</li>
|
||||
<li><span className="kbd">Test Connection</span> 클릭 — 처음엔 "드라이버를 내려받을까요?" 하고 물어요. 이건 DBeaver가 PostgreSQL과 대화할 통역사를 데려오는 과정이니 <strong>Download</strong>.</li>
|
||||
<li>초록 체크 "Connected"가 뜨면 <span className="kbd">Finish</span>!</li>
|
||||
</ol>
|
||||
<Code>{CODE_CONNECTION}</Code>
|
||||
<Code>{CODE_TREE}</Code>
|
||||
<div className="warn">
|
||||
<b>연결이 안 될 때 3단 점검</b> ① <span className="icode">docker ps</span>에
|
||||
mirim-postgres가 있나요? (없으면 DB가 꺼진 것) ② Host를{' '}
|
||||
<span className="icode">localhost</span>로 썼나요? ③ 비밀번호 오타는 없나요?
|
||||
— 십중팔구 이 셋 중 하나예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="ERD 자동 생성 — 우리 스키마 한눈에" sub="테이블 관계도가 그림으로 짠!">
|
||||
<p>
|
||||
<Link to="/learn/data-modeling"><strong>데이터 모델링 기초</strong></Link>{' '}
|
||||
코스에서 ERD를 손으로 그려 봤죠? DBeaver는 그걸 <strong>자동으로</strong>{' '}
|
||||
그려 줍니다. 이미 존재하는 DB를 읽어서 지도를 만들어 주는 거예요 —
|
||||
처음 가 본 동네에서 지도 앱을 켜는 것과 같습니다.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>왼쪽 트리에서 <span className="icode">public</span> 스키마를 <strong>우클릭</strong> → <strong>View Diagram</strong> (또는 스키마 더블클릭 → <strong>ER Diagram</strong> 탭).</li>
|
||||
<li>테이블들이 상자로, 관계(외래키)가 선으로 그려져요.</li>
|
||||
<li>상자를 드래그해서 보기 좋게 배치해 보세요 — 배치만 바뀌고 DB는 안 바뀌니 마음껏!</li>
|
||||
</ol>
|
||||
<p>
|
||||
선 끝을 잘 보세요. <span className="icode">submission</span>에서{' '}
|
||||
<span className="icode">users</span>로, 또{' '}
|
||||
<span className="icode">assignment</span>로 선이 뻗어 있죠? "제출물 한 건은
|
||||
<strong> 어느 학생이, 어느 과제에</strong> 낸 것"이라는 관계가 그림 한 장에
|
||||
담겨 있는 거예요. 새 회사에 가서 처음 보는 DB를 파악할 때, 개발자들이 제일
|
||||
먼저 하는 일이 바로 이 ERD 열어 보기입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> ERD를 열고, 선(관계)을 하나 골라{' '}
|
||||
<strong>말로 설명</strong>해 보세요. "submission의 user_id가 users의 id를
|
||||
가리킨다 = 제출물마다 주인이 있다"처럼요. 세 테이블 관계를 전부 설명할 수
|
||||
있으면 이 섹션은 통과!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="테이블 데이터를 GUI로 보기" sub="더블클릭 한 번이면 엑셀처럼">
|
||||
<p>
|
||||
트리에서 <span className="icode">users</span> 테이블을 <strong>더블클릭</strong>해
|
||||
보세요. 오른쪽에 탭이 열리고, <strong>Data</strong> 탭을 누르면 실제 행들이
|
||||
표로 나타납니다. <span className="icode">SELECT * FROM users;</span>를 손으로
|
||||
치던 일이 클릭 한 번이 된 거예요.
|
||||
</p>
|
||||
<Code>{CODE_TABLE_TABS}</Code>
|
||||
<p>
|
||||
<strong>필터</strong>가 특히 물건이에요. 열 머리글을 우클릭해서 조건을 걸면
|
||||
— 예를 들어 assignment 테이블에서 <span className="icode">week = 1</span> —
|
||||
WHERE 절을 안 쓰고도 원하는 행만 골라 볼 수 있어요. 물론 속으로는 DBeaver가
|
||||
WHERE 절을 만들어 보내고 있고, 상단 필터 칸에 그 조건이 그대로 보입니다.
|
||||
GUI로 편하게 쓰면서 SQL도 눈으로 배우는 일석이조!
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>셀 수정은 신중하게</b> Data 탭에서 셀을 더블클릭하면 <strong>값이
|
||||
수정</strong>돼요. 저장(💾)을 눌러야 진짜 반영되긴 하지만, 로컬 DB라도
|
||||
함부로 고치면 백엔드가 이상하게 동작해서 "버그인 줄 알고 한참 헤매는" 일이
|
||||
생깁니다. 이 코스에서는 <strong>보기만</strong> 하고, 데이터 변경은 SQL
|
||||
입문 코스에서 배운 UPDATE로 의도를 갖고 하세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="SQL 편집기 — 배운 쿼리를 GUI에서" sub="Ctrl+] 로 열고 Ctrl+Enter로 실행">
|
||||
<p>
|
||||
GUI가 편해도 결국 개발자의 본 무기는 SQL이에요. DBeaver의 <strong>SQL
|
||||
편집기</strong>는 그 무기를 쓰기 제일 좋은 연습장입니다. 연결을 선택한 채{' '}
|
||||
<span className="kbd">Ctrl+]</span> (또는 상단 SQL Editor 메뉴)를 누르면
|
||||
빈 편집기가 열려요.
|
||||
</p>
|
||||
<Code>{CODE_SQL_EDITOR}</Code>
|
||||
<p>
|
||||
터미널 psql보다 좋은 점 세 가지 — ① 테이블·열 이름을 몇 글자만 치면{' '}
|
||||
<strong>자동완성</strong>이 떠요. ② 결과가 <strong>표</strong>로 나와서
|
||||
정렬·필터를 이어서 할 수 있어요. ③ 쿼리를 파일로 저장해 두고 다음에 또
|
||||
쓸 수 있어요. SQL 입문 코스의 연습 문제들을 여기서 다시 풀어 보면 속도가
|
||||
완전히 다를 거예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>실행 단축키 정리</b> 커서가 있는 문장 하나 실행 ={' '}
|
||||
<span className="kbd">Ctrl+Enter</span>, 편집기 전체 실행 ={' '}
|
||||
<span className="kbd">Alt+X</span>. 여러 문장을 써 놓고 한 줄씩 실행하며
|
||||
결과를 비교하는 게 학습에는 최고예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="CSV 내보내기 & 마무리 실습" sub="DB 밖으로 데이터 들고 나가기 + 내 계정 찾기">
|
||||
<p>
|
||||
조회한 데이터를 멘토나 팀에 공유할 땐 <strong>CSV</strong>로 내보내는 게
|
||||
정석이에요. CSV는 쉼표로 값을 구분한 순수 텍스트 파일이라 엑셀·구글시트가
|
||||
바로 열 수 있거든요. DB라는 창고에서 물건을 꺼내 택배 상자에 담는 과정이라고
|
||||
생각하면 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_EXPORT}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> SQL 편집기에서{' '}
|
||||
<span className="icode">SELECT name, track FROM users;</span>를 실행하고,
|
||||
결과 표를 우클릭 → Export data → <strong>CSV</strong>로 저장해 보세요.
|
||||
저장된 파일을 메모장으로 열어 "정말 쉼표로 구분된 텍스트"인지 눈으로
|
||||
확인하면 CSV의 정체가 완전히 이해됩니다.
|
||||
</div>
|
||||
<p>
|
||||
마지막은 제출 실습이에요. 이 코스에서 배운 걸 총동원해서{' '}
|
||||
<strong>내 계정 행</strong>을 DB에서 직접 찾아 보세요.
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE_ME}</Code>
|
||||
<div className="warn">
|
||||
<b>철칙: 운영 DB에는 연결하지 않는다</b> DBeaver에 접속 정보만 넣으면
|
||||
어떤 DB든 열 수 있어요 — 그래서 위험합니다. 우리가 연결해도 되는 건{' '}
|
||||
<strong>내 PC의 로컬 개발 DB뿐</strong>이에요. 운영 서버(EC2)의 DB 접속
|
||||
정보를 개인 PC의 DBeaver에 넣는 것은 <strong>금지</strong>입니다. 실수로
|
||||
운영 데이터를 한 줄만 고쳐도 실제 사용자에게 사고가 나고, 접속 정보가 내
|
||||
PC에 저장돼 유출 통로가 되기 때문이에요. 운영 DB 확인이 필요한 일이 생기면
|
||||
반드시 멘토에게 먼저 이야기하세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🦫 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 DB를 <strong>글자(psql)로도, 그림(DBeaver)으로도</strong> 다룰 수
|
||||
있게 됐어요 — 연결 설정, ERD 읽기, GUI 필터, SQL 편집기, CSV 내보내기까지.
|
||||
테이블 구조가 왜 이렇게 생겼는지 궁금해졌다면{' '}
|
||||
<Link to="/learn/data-modeling"><strong>데이터 모델링 기초</strong></Link>로,
|
||||
쿼리 실력을 더 올리고 싶다면 <Link to="/learn/sql"><strong>SQL 입문</strong></Link>을
|
||||
DBeaver로 다시 풀어 보세요. 스크린샷 실습 제출은{' '}
|
||||
<Link to="/assignments"><strong>과제 게시판</strong></Link>에서!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
349
frontend/src/pages/courses/ToolDockerPage.jsx
Normal file
349
frontend/src/pages/courses/ToolDockerPage.jsx
Normal file
@ -0,0 +1,349 @@
|
||||
// 이 파일이 하는 일: "Docker Desktop 사용법" 코스 — 설치와 첫 실행부터
|
||||
// 대시보드 읽는 법, GUI로 컨테이너 다루기, CLI 명령과의 짝, 우리 개발 DB 확인 루틴,
|
||||
// 용량 청소, 흔한 문제 해결까지 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_WSL2 = `Docker Desktop이 Windows에서 돌아가는 구조
|
||||
|
||||
[Docker Desktop 앱] ← 우리가 보는 예쁜 화면(GUI)
|
||||
│
|
||||
[WSL2 — Windows 속의 작은 리눅스] ← 진짜 엔진이 사는 곳
|
||||
│
|
||||
[컨테이너들] postgres, spring-boot, ...
|
||||
|
||||
→ Docker(컨테이너 기술)는 원래 리눅스에서 태어났어요.
|
||||
그래서 Windows에서는 WSL2라는 "리눅스 방"을 하나 만들고
|
||||
그 안에서 엔진을 돌립니다. 설치할 때 WSL2를 함께 켜는 이유!`;
|
||||
|
||||
const CODE_DASHBOARD = `Docker Desktop 왼쪽 메뉴 — 세 개만 알면 90%는 끝
|
||||
|
||||
탭 비유 여기서 하는 일
|
||||
─────────────────────────────────────────────────────
|
||||
Containers 지금 돌아가는 기계 시작/중지, 로그 보기, 포트 확인
|
||||
Images 기계의 설계도 창고 받아 둔 이미지 목록, 용량, 삭제
|
||||
Volumes 기계 밖 데이터 금고 DB 데이터처럼 "지워지면 안 되는" 것
|
||||
|
||||
핵심 관계: 이미지(설계도)로 컨테이너(기계)를 찍어내고,
|
||||
소중한 데이터는 볼륨(금고)에 따로 보관해요.
|
||||
컨테이너를 지워도 볼륨이 살아 있으면 데이터는 안전합니다.`;
|
||||
|
||||
const CODE_CONTAINER_ROW = `Containers 탭의 한 줄을 읽는 법
|
||||
|
||||
이름 이미지 상태 포트
|
||||
────────────────────────────────────────────────────
|
||||
mirim-db postgres:16 Running 5432:5432
|
||||
│ │
|
||||
초록 = 살아 있음 왼쪽(내 PC):오른쪽(컨테이너)
|
||||
회색 = 멈춰 있음 "내 PC의 5432 문을 두드리면
|
||||
컨테이너의 5432로 연결해 줄게"
|
||||
|
||||
한 줄에서 쓸 수 있는 버튼: ▶ 시작 · ■ 중지 · ⟳ 재시작 · 🗑 삭제
|
||||
이름을 클릭하면 → Logs(로그) / Inspect / Files 상세 화면이 열려요.`;
|
||||
|
||||
const CODE_GUI_CLI = `GUI 버튼과 터미널 명령은 같은 일을 하는 쌍둥이
|
||||
|
||||
GUI에서 하는 일 CLI 명령 (터미널에서)
|
||||
──────────────────────────────────────────────────────
|
||||
Containers 목록 보기 docker ps -a
|
||||
▶ 시작 버튼 docker start mirim-db
|
||||
■ 중지 버튼 docker stop mirim-db
|
||||
Logs 화면 열기 docker logs -f mirim-db
|
||||
Images 목록 보기 docker images
|
||||
🗑 이미지 삭제 docker rmi <이미지이름>
|
||||
|
||||
→ 버튼을 누를 때마다 "이건 CLI로 뭐였지?" 한 번씩 떠올려 보세요.
|
||||
나중에 GUI가 없는 서버(우리 EC2!)에 들어가면 CLI만 남거든요.
|
||||
Desktop은 훈련장, CLI는 실전이에요.`;
|
||||
|
||||
const CODE_COMPOSE_ROUTINE = `아침 개발 시작 루틴 — 우리 개발 DB 살아 있나?
|
||||
|
||||
# 1) 프로젝트 폴더에서 개발 인프라 올리기 (이미 떠 있으면 그대로 둠)
|
||||
docker compose up -d
|
||||
|
||||
# 2) 상태 확인
|
||||
docker compose ps
|
||||
|
||||
NAME IMAGE STATUS PORTS
|
||||
mirim-db postgres:16 Up 2 hours 0.0.0.0:5432->5432/tcp
|
||||
│
|
||||
"Up"이면 통과! "Exited"면 로그를 볼 차례:
|
||||
|
||||
# 3) 뭔가 이상할 때 — 로그 확인
|
||||
docker compose logs -f
|
||||
# (Docker Desktop에서는 Containers 탭 → 이름 클릭 → Logs 와 같아요)`;
|
||||
|
||||
const CODE_CLEANUP = `Docker 용량 청소 — 방 청소와 똑같아요
|
||||
|
||||
# 지금 Docker가 먹고 있는 용량 확인
|
||||
docker system df
|
||||
|
||||
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
||||
Images 12 3 8.4GB 5.1GB (60%) ← 회수 가능!
|
||||
Containers 5 2 120MB ...
|
||||
Volumes 4 2 1.2GB ...
|
||||
|
||||
# 안 쓰는 것들 한 번에 정리 (멈춘 컨테이너·안 쓰는 이미지 등)
|
||||
docker system prune
|
||||
|
||||
# 볼륨까지 지우는 -a·--volumes 옵션은 조심!
|
||||
# DB 데이터가 볼륨에 있다는 것, 기억하죠? (섹션 2)`;
|
||||
|
||||
const CODE_TROUBLE = `Docker Desktop 단골 고장 2가지 — 증상과 처방
|
||||
|
||||
증상 1) "Virtualization support not detected" / WSL 오류로 시작 실패
|
||||
원인 PC의 가상화(VT-x/SVM) 기능이 꺼져 있음
|
||||
처방 ① 작업 관리자(Ctrl+Shift+Esc) → 성능 → CPU → "가상화: 사용"인지 확인
|
||||
② "사용 안 함"이면 BIOS에서 켜야 함 (부팅 시 F2/Del → VT-x 또는 SVM 켜기)
|
||||
③ PowerShell(관리자)에서 wsl --update 후 재부팅
|
||||
|
||||
증상 2) PC가 갑자기 느려짐 / 팬이 비행기 이륙
|
||||
원인 WSL2(Vmmem 프로세스)가 메모리를 너무 많이 차지
|
||||
처방 내 폴더의 .wslconfig 파일로 상한선 정하기:
|
||||
|
||||
# C:\\Users\\<내이름>\\.wslconfig
|
||||
[wsl2]
|
||||
memory=4GB # WSL2가 쓸 수 있는 최대 메모리
|
||||
processors=2 # 사용할 CPU 코어 수
|
||||
|
||||
# 저장 후 PowerShell에서 wsl --shutdown → Docker Desktop 재시작`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '설치와 첫 실행' },
|
||||
{ n: 2, label: '대시보드 읽기' },
|
||||
{ n: 3, label: 'GUI로 컨테이너 다루기' },
|
||||
{ n: 4, label: 'GUI ↔ CLI 짝짓기' },
|
||||
{ n: 5, label: '개발 DB 확인 루틴' },
|
||||
{ n: 6, label: '용량 청소' },
|
||||
{ n: 7, label: '흔한 문제 해결' },
|
||||
];
|
||||
|
||||
export default function ToolDockerPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 도구를 왜 배우고, 어디까지 다루는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 부록 · 도구</div>
|
||||
<h1>Docker Desktop 사용법</h1>
|
||||
<p>
|
||||
Docker Desktop은 내 PC에서 돌아가는 컨테이너들을 <strong>눈으로 보고 버튼으로
|
||||
다루는</strong> 관제실이에요. 어썸데브에서 매일 쓰는 개발용 PostgreSQL이
|
||||
잘 살아 있는지 확인하고, 멈추고, 로그를 읽고, 용량을 청소하는 법까지 배웁니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 40분</span>
|
||||
<span className="chip">실습 2개 이상</span>
|
||||
<span className="chip">준비물: Docker Desktop 설치된 내 PC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="설치와 첫 실행" sub="WSL2라는 '리눅스 방'부터 이해하기">
|
||||
<p>
|
||||
Docker Desktop 설치 자체는 설치 파일을 받아 <strong>다음 → 다음</strong>만 누르면
|
||||
끝나요. 그런데 설치 중에 <strong>"Use WSL 2 instead of Hyper-V"</strong> 같은
|
||||
체크박스가 나와서 처음엔 당황합니다. 이게 뭔지부터 짚고 갈게요.
|
||||
</p>
|
||||
<Code>{CODE_WSL2}</Code>
|
||||
<p>
|
||||
컨테이너 기술은 리눅스의 기능을 쓰기 때문에, Windows에서는{' '}
|
||||
<strong>WSL2</strong>(Windows Subsystem for Linux)라는 작은 리눅스를 먼저 깔고
|
||||
그 안에서 Docker 엔진을 돌려요. 우리 학교 건물(Windows) 안에 실습실(리눅스 방)을
|
||||
하나 만들고, 기계는 전부 그 실습실 안에서 돌리는 셈이죠. 설치 순서는 이래요.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>PowerShell(관리자)에서 <span className="icode">wsl --install</span> 실행 후 재부팅 — WSL2 준비 끝.</li>
|
||||
<li>Docker Desktop 설치 파일 실행 — WSL2 옵션은 체크된 그대로 두기.</li>
|
||||
<li>설치 후 첫 실행 — 고래 아이콘이 작업 표시줄에 뜨고, 고래가 <strong>멈춰 있으면 준비 완료</strong>(움직이는 동안은 시동 중).</li>
|
||||
<li>회원가입/로그인 화면은 <strong>건너뛰어도(Skip)</strong> 모든 기능을 쓸 수 있어요.</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 터미널을 열고 <span className="icode">docker --version</span>을
|
||||
쳐 보세요. 버전이 찍히면 설치 성공! 이어서{' '}
|
||||
<span className="icode">docker run hello-world</span>를 실행하면 도커가
|
||||
이미지를 내려받아 첫 컨테이너를 돌리고 인사말을 출력해요 — 이 한 줄이
|
||||
"설계도 다운로드 → 기계 조립 → 가동"의 전 과정입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="대시보드 화면 읽기" sub="Containers · Images · Volumes — 기계, 설계도, 금고">
|
||||
<p>
|
||||
Docker Desktop을 열면 왼쪽에 메뉴가 주르륵 있는데, 수습 기간에 실제로 쓰는 건
|
||||
딱 세 개예요. 이 셋의 관계만 잡으면 나머지는 자연스럽게 따라옵니다.
|
||||
</p>
|
||||
<Code>{CODE_DASHBOARD}</Code>
|
||||
<p>
|
||||
비유하자면 <strong>이미지는 붕어빵 틀, 컨테이너는 붕어빵</strong>이에요. 틀 하나로
|
||||
붕어빵을 몇 개든 찍을 수 있듯, <span className="icode">postgres:16</span> 이미지
|
||||
하나로 DB 컨테이너를 여러 개 만들 수 있죠. 그리고 <strong>볼륨</strong>은 붕어빵
|
||||
안의 팥... 이 아니라(!) 붕어빵 기계 옆의 <strong>금고</strong>예요. 컨테이너는
|
||||
지웠다 다시 만드는 게 일상이라, DB 데이터처럼 잃으면 안 되는 것은 컨테이너
|
||||
바깥의 볼륨에 보관합니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>여기서 사고가 납니다</b> "컨테이너 지워도 다시 만들면 되지"는 맞는 말이지만,
|
||||
<strong> 볼륨까지 지우면 DB 데이터가 통째로 사라져요.</strong> Volumes 탭에서
|
||||
뭔가를 삭제하기 전엔 반드시 그 볼륨을 어떤 컨테이너가 쓰는지 확인하는 습관을!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="컨테이너 시작·중지·로그 보기" sub="GUI 버튼으로 기계 다루기">
|
||||
<p>
|
||||
Containers 탭의 한 줄 한 줄이 컨테이너 하나예요. 이 줄을 읽을 줄 알면
|
||||
"지금 뭐가 살아 있고, 어느 문(포트)으로 연결되는지"가 한눈에 보입니다.
|
||||
</p>
|
||||
<Code>{CODE_CONTAINER_ROW}</Code>
|
||||
<p>
|
||||
이 중 제일 자주 열게 될 화면은 <strong>Logs</strong>예요. 컨테이너의 일기장이라,
|
||||
"DB가 왜 안 뜨지?" 싶을 때 답의 90%가 여기 적혀 있어요. 에러가 나면 로그
|
||||
마지막 몇 줄부터 읽으세요 — 보통 제일 아래에 결정적 단서(예:{' '}
|
||||
<span className="icode">password authentication failed</span>)가 있습니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> Containers 탭에서 개발 DB 컨테이너를 찾아{' '}
|
||||
<strong>■ 중지</strong>를 눌러 보세요. 상태가 회색(Exited)으로 바뀌죠?
|
||||
그 상태에서 백엔드(Spring Boot)를 실행하면 DB 연결 에러가 나는 것까지 확인한 뒤,
|
||||
<strong> ▶ 시작</strong>으로 되살리고 Logs에서{' '}
|
||||
<span className="icode">database system is ready to accept connections</span>{' '}
|
||||
문장을 찾아보세요. "DB가 죽으면 무슨 일이 나는지"를 안전하게 체험하는 실습입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="GUI 버튼 = CLI 명령" sub="버튼 하나마다 짝이 되는 명령이 있어요">
|
||||
<p>
|
||||
Docker Desktop의 모든 버튼은 사실 터미널 명령을 대신 눌러 주는 것뿐이에요.
|
||||
자동변속기(GUI)로 운전을 배우더라도 수동변속기(CLI)의 원리를 알아야
|
||||
어떤 차든 몰 수 있는 것처럼, 버튼과 명령을 <strong>쌍으로</strong> 외워 두면
|
||||
나중에 서버에서도 당황하지 않습니다.
|
||||
</p>
|
||||
<Code>{CODE_GUI_CLI}</Code>
|
||||
<p>
|
||||
특히 <span className="icode">docker ps</span>(지금 도는 것만)와{' '}
|
||||
<span className="icode">docker ps -a</span>(멈춘 것까지 전부)의 차이는 면접에서도
|
||||
나올 만큼 기본이에요. Desktop의 Containers 탭은 기본이{' '}
|
||||
<span className="icode">-a</span> 쪽 — 멈춘 컨테이너도 회색으로 보여 줍니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 터미널에서 <span className="icode">docker ps</span>를 실행해
|
||||
나온 목록과 Docker Desktop의 Containers 탭을 <strong>나란히 놓고</strong> 비교해
|
||||
보세요. 이름·이미지·포트가 똑같이 보이나요? 이제 GUI에서 컨테이너 하나를 중지한 뒤
|
||||
다시 <span className="icode">docker ps</span> — 목록에서 사라지고,{' '}
|
||||
<span className="icode">docker ps -a</span>에는 남아 있는 걸 확인하면 완벽!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="우리 개발 DB 상태 확인 루틴" sub="출근(등교?)하면 제일 먼저 하는 일">
|
||||
<p>
|
||||
어썸데브의 로컬 개발 환경은 PostgreSQL 같은 인프라를{' '}
|
||||
<span className="icode">docker compose</span>로 띄워요. compose는 "컨테이너
|
||||
여러 대의 출석부" — <span className="icode">docker-compose.yml</span> 파일에
|
||||
적힌 명단대로 컨테이너들을 한 번에 올리고 내립니다. 그래서 개발을 시작하는
|
||||
아침 루틴이 이렇게 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_COMPOSE_ROUTINE}</Code>
|
||||
<ol className="olist">
|
||||
<li><span className="icode">docker compose up -d</span> — 출석부대로 전원 기상. <span className="icode">-d</span>는 "백그라운드에서"라는 뜻이라 터미널을 계속 안 잡아먹어요.</li>
|
||||
<li><span className="icode">docker compose ps</span> — 상태가 <strong>Up</strong>인지 눈으로 확인. Docker Desktop을 열어 초록 불을 확인해도 같아요.</li>
|
||||
<li>백엔드가 DB 연결 에러를 뱉으면? 십중팔구 이 루틴을 건너뛴 거예요. "코드 문제인가?" 고민하기 전에 <strong>DB부터 확인</strong>이 순서!</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 프로젝트 폴더에서 위 루틴 1→2번을 그대로 실행해 보고,
|
||||
Docker Desktop의 Containers 탭에서 같은 컨테이너가 초록 불로 떠 있는지
|
||||
교차 확인해 보세요. 포트 열의 <span className="icode">5432</span>가 PostgreSQL의
|
||||
방 번호라는 것, 네트워크 코스에서 배운 그 포트 맞아요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="용량 정리 — 이미지 청소" sub="Docker는 조용히 디스크를 먹어요">
|
||||
<p>
|
||||
Docker를 몇 달 쓰다 보면 C 드라이브가 슬금슬금 차오릅니다. 범인은 대부분{' '}
|
||||
<strong>안 쓰는 이미지</strong> — 한 번 받아 본 이미지, 버전 올리고 남은 옛날
|
||||
이미지가 창고에 계속 쌓이거든요. 옷장 정리처럼 주기적으로 비워 줘야 해요.
|
||||
</p>
|
||||
<Code>{CODE_CLEANUP}</Code>
|
||||
<p>
|
||||
GUI로 하려면 <strong>Images 탭</strong>에서 <span className="icode">Unused</span>{' '}
|
||||
필터를 걸고 안 쓰는 이미지를 골라 🗑 삭제하면 됩니다. Docker Desktop 설정의{' '}
|
||||
<strong>Resources → Disk usage</strong>에서 전체 사용량을 그래프로 볼 수도 있어요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>prune 옵션은 읽고 누르세요</b> <span className="icode">docker system prune</span>은
|
||||
안전한 편이지만, <span className="icode">--volumes</span>를 붙이면{' '}
|
||||
<strong>안 쓰는 볼륨(=DB 데이터가 들어 있을 수 있는 금고)까지</strong> 지웁니다.
|
||||
지우기 전에 뜨는 확인 문구를 꼭 읽는 습관 — 실무에서도 그대로 통하는 습관이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="흔한 문제 해결" sub="가상화 꺼짐 · 메모리 폭식 — 단골 두 녀석">
|
||||
<p>
|
||||
Docker Desktop이 말썽일 때, 원인은 놀랄 만큼 자주 이 둘 중 하나예요.
|
||||
증상 → 원인 → 처방 순서로 정리해 둘 테니, 막히면 이 섹션으로 돌아오세요.
|
||||
</p>
|
||||
<Code>{CODE_TROUBLE}</Code>
|
||||
<p>
|
||||
가상화는 "PC 안에 또 다른 컴퓨터(리눅스 방)를 돌려도 된다"는 CPU의 허가예요.
|
||||
공장에서 이 스위치를 꺼서 출고하는 PC가 많아, 처음 설치하는 날 한 번은
|
||||
꼭 만나는 문제입니다. 메모리 폭식은 WSL2가 "남는 메모리는 내가 다 써도
|
||||
되지?" 하는 성격이라 생기는 일 — <span className="icode">.wslconfig</span>로
|
||||
용돈(상한선)을 정해 주면 얌전해져요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>막혔을 때의 만능 3단 콤보</b> ① Docker Desktop 재시작 →
|
||||
② PowerShell에서 <span className="icode">wsl --shutdown</span> 후 다시 시작 →
|
||||
③ PC 재부팅. 이 셋으로 대부분 살아나요. 그래도 안 되면 에러 메시지를
|
||||
<strong> 그대로 복사해서</strong> 멘토에게 공유 — "안 돼요"보다 백 배 빠릅니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🐳 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 Docker Desktop이라는 관제실에서 컨테이너(기계)·이미지(설계도)·볼륨(금고)을
|
||||
구분해 읽고, 개발 DB를 <strong>올리고·확인하고·로그 보고·청소하는</strong> 하루
|
||||
루틴이 손에 익었어요. GUI 버튼마다 짝이 되는 CLI 명령까지 챙겼으니, 다음은
|
||||
이 컨테이너들이 실제 서버에서 어떻게 돌아가는지 —{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서 포트와
|
||||
서버의 큰 그림을 복습하고, 백엔드 과제에서 오늘 배운 DB 확인 루틴을
|
||||
매일 아침 직접 써먹어 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
368
frontend/src/pages/courses/ToolIntellijPage.jsx
Normal file
368
frontend/src/pages/courses/ToolIntellijPage.jsx
Normal file
@ -0,0 +1,368 @@
|
||||
// 이 파일이 하는 일: "IntelliJ 꿀팁 모음" 코스 — 어썸데브 백엔드(Spring Boot) 개발의
|
||||
// 주력 도구인 IntelliJ를 '그냥 에디터'에서 '내 손의 연장'으로 만드는 7개 섹션 학습 페이지.
|
||||
// (필수 단축키 → 자동완성 200% → Git·로컬 히스토리 → 디버거 → Spring 실행 구성 → DB 도구 → 실습 여행)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 정적 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 단축키 표·예시 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 표 · 예시 코드 상수들 ──
|
||||
|
||||
const CODE_SHORTCUTS = `외우면 개발 속도가 달라지는 핵심 단축키 (Windows 기준)
|
||||
|
||||
단축키 하는 일 비유하자면
|
||||
──────────────────────────────────────────────────────────────
|
||||
Shift Shift 뭐든지 검색 (Search Everywhere) 만능 검색창
|
||||
Alt + Enter 퀵픽스 — 빨간 줄 해결 제안 "선생님, 정답 알려주세요"
|
||||
Ctrl + 클릭 정의로 이동 (선언부로 점프) "이 메서드 어디서 왔어?"
|
||||
Ctrl + Alt + ← 점프하기 전 위치로 돌아가기 뒤로 가기 버튼
|
||||
Shift + F6 이름 바꾸기 (Rename 리팩터) 사용처까지 전부 개명
|
||||
Ctrl + Alt + L 코드 정렬 (Reformat) 방 청소 버튼
|
||||
Ctrl + / 한 줄 주석 토글 포스트잇 붙이기
|
||||
Ctrl + D 현재 줄 복제 복사+붙여넣기 한 방에
|
||||
Ctrl + Shift + F 프로젝트 전체에서 텍스트 검색 건물 전체 수색
|
||||
|
||||
→ 전부 외우려 하지 마세요. 위에서 4개(Shift Shift, Alt+Enter,
|
||||
Ctrl+클릭, Shift+F6)만 손에 익어도 상위 10% 사용자입니다.`;
|
||||
|
||||
const CODE_LIVE_TEMPLATE = `자주 쓰는 코드를 몇 글자로 소환하는 라이브 템플릿
|
||||
|
||||
입력 Tab 누르면 펼쳐지는 코드
|
||||
─────────────────────────────────────────────
|
||||
psvm → public static void main(String[] args) { }
|
||||
sout → System.out.println();
|
||||
fori → for (int i = 0; i < ; i++) { }
|
||||
iter → for (String item : items) { } ← 컬렉션 보고 알아서!
|
||||
|
||||
# 자동완성 자체의 꿀팁:
|
||||
# - Ctrl + Space : 기본 자동완성 (안 뜨면 강제 소환)
|
||||
# - Tab으로 확정하면 뒤에 있던 단어를 '덮어쓰기' — Enter는 '끼워넣기'
|
||||
# - 대문자만 쳐도 찾아줘요: "UDS" → UserDetailsService`;
|
||||
|
||||
const CODE_LOCAL_HISTORY = `커밋을 깜빡했어도 살릴 수 있다 — 로컬 히스토리(Local History)
|
||||
|
||||
IntelliJ는 파일을 저장할 때마다 몰래 스냅샷을 남겨요.
|
||||
Git 커밋과 별개로, IDE가 자동으로 적어 두는 '비밀 일기장'입니다.
|
||||
|
||||
사용법:
|
||||
파일에서 우클릭 → Local History → Show History
|
||||
→ 시간대별 변경 내역이 좌우 비교(diff)로 쫙 —
|
||||
→ 되돌리고 싶은 시점에서 Revert 클릭. 끝.
|
||||
|
||||
언제 목숨을 구해 주나:
|
||||
- 실수로 메서드를 통째로 지우고 저장까지 해버렸을 때
|
||||
- "아까 오전엔 됐는데..." 커밋은 안 해놨을 때
|
||||
- git reset을 잘못 쳐서 작업물이 증발한 것 같을 때
|
||||
|
||||
단, 보관 기간이 며칠 수준이에요. 일기장은 임시 보험일 뿐 —
|
||||
진짜 저장은 언제나 Git 커밋입니다. 자주 커밋하는 습관이 우선!`;
|
||||
|
||||
const CODE_GIT_PANEL = `IntelliJ 안에서 끝내는 Git 일과
|
||||
|
||||
Alt + 9 Git 도구창 — 커밋 이력(Log)을 그래프로 구경
|
||||
Ctrl + K 커밋 창 — 바뀐 파일 체크하고 메시지 쓰고 커밋
|
||||
Ctrl + Shift + K 푸시 — Gitea(edu.awesomedevapp.com)로 올리기
|
||||
|
||||
편집기 왼쪽 여백(거터)의 색 막대도 Git 정보예요:
|
||||
초록 = 새로 추가된 줄 파랑 = 수정된 줄 회색 화살표 = 삭제된 줄
|
||||
색 막대 클릭 → 원래 코드와 비교 + 그 자리에서 되돌리기(Rollback)까지!`;
|
||||
|
||||
const CODE_DEBUGGER = `System.out.println 지옥에서 탈출하는 디버거 3단계
|
||||
|
||||
1) 브레이크포인트 찍기
|
||||
의심 가는 줄의 줄 번호 옆(거터) 클릭 → 빨간 점
|
||||
= "여기서 일시정지!" 표시
|
||||
|
||||
2) 벌레 아이콘(Debug)으로 실행 (Shift + F9)
|
||||
빨간 점에 도착하면 프로그램이 얼음! 하고 멈춰요
|
||||
|
||||
3) 멈춘 세상 구경하기
|
||||
- Variables 창: 이 순간 모든 변수의 실제 값
|
||||
- F8 (Step Over): 한 줄씩 전진
|
||||
- F7 (Step Into): 호출된 메서드 안으로 잠입
|
||||
- F9 (Resume): 다음 빨간 점까지 다시 달리기
|
||||
- 변수 우클릭 → Add to Watches: 특정 값만 계속 감시
|
||||
|
||||
비유: println은 여행지에서 사진 몇 장 받아보는 것이고,
|
||||
디버거는 시간을 멈추고 현장을 직접 걸어다니는 겁니다.`;
|
||||
|
||||
const CODE_RUN_CONFIG = `Spring Boot 실행 구성(Run Configuration) 읽는 법
|
||||
|
||||
우측 상단 [BackendApplication ▾] [▶] [🐞] 이 한 줄이 실행 조종석:
|
||||
|
||||
BackendApplication ▾ ← 무엇을 실행할지 (실행 구성 선택)
|
||||
▶ Run ← 그냥 실행
|
||||
🐞 Debug ← 브레이크포인트가 작동하는 실행
|
||||
|
||||
구성 편집(Edit Configurations...)에서 자주 만지는 것:
|
||||
- Active profiles: local ← application-local.yml을 쓰겠다
|
||||
- Environment variables: ← DB 비밀번호 같은 값 주입
|
||||
DB_PASSWORD=...;JWT_SECRET=...
|
||||
|
||||
콘솔에 이 로그가 보이면 성공적으로 떠 있는 거예요:
|
||||
Tomcat started on port 8080
|
||||
Started BackendApplication in 3.2 seconds`;
|
||||
|
||||
const CODE_DB_TOOL = `IntelliJ 안에서 DB 구경하기 (Database 도구창)
|
||||
|
||||
1) 우측 사이드바 [Database] → [+] → Data Source → PostgreSQL
|
||||
2) Host/Port/User/Password 입력 (로컬 개발 DB 기준: localhost:5432)
|
||||
3) [Test Connection] 초록불 확인 → OK
|
||||
|
||||
이제 할 수 있는 것:
|
||||
- 테이블 더블클릭 → 데이터가 표(스프레드시트)처럼 좌르륵
|
||||
- 콘솔(Ctrl+Shift+F10 or New Query Console)에서 SQL 직접 실행:
|
||||
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
|
||||
- 테이블 우클릭 → Diagram → 테이블 관계도를 그림으로!
|
||||
|
||||
주의: 운영(AWS EC2) DB에는 함부로 연결하지 않아요.
|
||||
멘토와 함께, 읽기 전용 계정으로만. 로컬 Docker DB로 연습합시다.`;
|
||||
|
||||
const CODE_LOGIN_TOUR = `실습 코스: Ctrl+클릭으로 떠나는 '로그인 요청 한 바퀴' 여행
|
||||
|
||||
출발지: 우리 backend 프로젝트를 IntelliJ로 열기
|
||||
|
||||
① Shift Shift → "AuthController" (또는 "login") 검색 → Enter
|
||||
→ 로그인 API의 입구(컨트롤러)에 도착
|
||||
|
||||
② @PostMapping이 붙은 login 메서드에서
|
||||
호출하는 서비스 메서드를 Ctrl + 클릭
|
||||
→ 실제 로그인 검증 로직(서비스 계층)으로 순간이동
|
||||
|
||||
③ 그 안에서 리포지토리 메서드(findBy...)를 또 Ctrl + 클릭
|
||||
→ DB에게 물어보는 계층까지 도착. 컨트롤러 → 서비스 → 리포지토리,
|
||||
3층 구조를 발로 밟아본 셈!
|
||||
|
||||
④ 길을 잃었다면 Ctrl + Alt + ← 로 왔던 길 되돌아가기
|
||||
|
||||
⑤ 보너스: 서비스 메서드에 브레이크포인트를 찍고 Debug로 서버 실행
|
||||
→ 프론트에서 로그인 시도 → 요청이 빨간 점에서 얼음!
|
||||
→ Variables 창에서 방금 내가 입력한 아이디가 보이면 성공 🎉`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '필수 단축키' },
|
||||
{ n: 2, label: '자동완성 200%' },
|
||||
{ n: 3, label: 'Git + 로컬 히스토리' },
|
||||
{ n: 4, label: '디버거 실전' },
|
||||
{ n: 5, label: 'Spring 실행 구성' },
|
||||
{ n: 6, label: 'DB 도구 맛보기' },
|
||||
{ n: 7, label: '실습: 로그인 흐름 여행' },
|
||||
];
|
||||
|
||||
export default function ToolIntellijPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스로 무엇이 달라지는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 부록 · 도구</div>
|
||||
<h1>IntelliJ 꿀팁 모음</h1>
|
||||
<p>
|
||||
어썸데브 백엔드(Spring Boot) 개발의 주력 도구인 IntelliJ를 "코드 치는 메모장"에서
|
||||
<strong> "내 손의 연장"</strong>으로 업그레이드하는 코스예요. 단축키 몇 개, 습관 몇 개가
|
||||
쌓이면 같은 일을 절반의 시간에 하게 됩니다 — 특히 마지막 실습은 꼭 직접 해보세요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 포함</span>
|
||||
<span className="chip">준비물: IntelliJ + backend 프로젝트</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="필수 단축키 — 손이 마우스를 잊게 하라" sub="이 표에서 4개만 외워도 상위 10%">
|
||||
<p>
|
||||
IntelliJ 고수와 초보의 차이는 아는 기능의 수가 아니라 <strong>마우스에 손이 가는
|
||||
횟수</strong>예요. 메뉴를 뒤지는 3초가 하루에 수백 번 쌓이면 몇십 분이 됩니다.
|
||||
아래 표에서 <strong>위쪽 4개</strong>부터 몸에 붙이세요.
|
||||
</p>
|
||||
<Code>{CODE_SHORTCUTS}</Code>
|
||||
<p>
|
||||
특히 <span className="kbd">Shift</span> 두 번(Search Everywhere)은 만능 열쇠예요.
|
||||
클래스·파일·설정·액션 이름 뭐든 쳐 보세요 — "어디서 하더라?" 싶은 기능도 이름만 치면
|
||||
바로 실행됩니다. 그리고 <span className="kbd">Alt</span>+<span className="kbd">Enter</span>는
|
||||
빨간 줄(에러) 위에서 누르면 IntelliJ가 <strong>고치는 방법을 제안</strong>해 줘요 —
|
||||
import 추가, 메서드 자동 생성, 오타 수정까지. 막히면 일단 Alt+Enter, 습관으로 만드세요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>이름 바꾸기는 무조건 Shift+F6</b> 변수·메서드·클래스 이름을 손으로 하나하나
|
||||
찾아 바꾸면 반드시 하나를 놓쳐요. <span className="kbd">Shift</span>+<span className="kbd">F6</span>은
|
||||
그 이름을 쓰는 <strong>모든 곳</strong>을 IntelliJ가 찾아서 한 번에 바꿔 줍니다.
|
||||
이게 '리팩터링 도구'의 첫걸음이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="자동완성 200% 활용하기" sub="psvm, sout — 몇 글자로 코드 소환하기">
|
||||
<p>
|
||||
자동완성은 "철자 대신 쳐 주는 기능"이 아니라 <strong>코드를 통째로 소환하는
|
||||
마법 주문</strong>에 가까워요. 그 주문서가 <strong>라이브 템플릿</strong>(Live Template)입니다.
|
||||
약속된 몇 글자를 치고 <span className="kbd">Tab</span>을 누르면 정해진 코드 뭉치가 펼쳐져요.
|
||||
</p>
|
||||
<Code>{CODE_LIVE_TEMPLATE}</Code>
|
||||
<p>
|
||||
예를 들어 디버깅용 출력 한 줄이 필요하면 <span className="icode">sout</span> 네 글자 +
|
||||
Tab이면 끝이에요. <span className="icode">iter</span>는 더 똑똑해서, 바로 위에 선언된
|
||||
리스트를 보고 <strong>알아서 그 변수로 for문을 채워</strong> 줍니다.
|
||||
Settings → Editor → Live Templates에서 나만의 주문을 새로 등록할 수도 있어요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 아무 자바 파일에서 새 클래스를 하나 만들고,
|
||||
본문에 <span className="icode">psvm</span> + <span className="kbd">Tab</span>,
|
||||
이어서 그 안에 <span className="icode">sout</span> + <span className="kbd">Tab</span>을 쳐 보세요.
|
||||
main 메서드와 출력문이 순식간에 생기면 성공 — 방금 타자 수십 번을 아꼈습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="Git 통합 복습 + 로컬 히스토리" sub="커밋 안 했어도 복구된다는 사실, 알고 있었나요?">
|
||||
<p>
|
||||
Git 코스에서 터미널 명령을 배웠다면, IntelliJ에서는 그걸 <strong>화면으로</strong> 합니다.
|
||||
터미널이 익숙해진 뒤 IDE 통합을 쓰면 "지금 뭐가 바뀌었는지"가 눈에 보여서 실수가 확 줄어요.
|
||||
</p>
|
||||
<Code>{CODE_GIT_PANEL}</Code>
|
||||
<p>
|
||||
그리고 IntelliJ에는 Git과 별개인 <strong>비밀 보험</strong>이 하나 더 있어요 —
|
||||
바로 <strong>로컬 히스토리</strong>입니다. 커밋을 안 했어도, 파일을 저장할 때마다
|
||||
IDE가 스스로 스냅샷을 남겨 두거든요.
|
||||
</p>
|
||||
<Code>{CODE_LOCAL_HISTORY}</Code>
|
||||
<div className="warn">
|
||||
<b>보험은 보험일 뿐</b> 로컬 히스토리는 내 PC에만, 며칠만 남아요.
|
||||
PC가 바뀌거나 시간이 지나면 사라집니다. 팀과 공유되고 영원히 남는 진짜 기록은
|
||||
<strong> Git 커밋 + Gitea 푸시</strong>뿐이에요. "커밋은 자주, 푸시는 매일" 원칙은 그대로!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="디버거 실전 — println 지옥 탈출" sub="시간을 멈추고 변수 속을 들여다보기">
|
||||
<p>
|
||||
버그를 잡을 때 <span className="icode">System.out.println("여기 왔나?")</span>을
|
||||
도배해 본 적 있죠? 그건 여행지에서 <strong>사진 몇 장</strong> 받아 보는 방식이에요.
|
||||
디버거는 다릅니다 — <strong>시간을 멈추고 현장을 직접 걸어다니는</strong> 방식이거든요.
|
||||
</p>
|
||||
<Code>{CODE_DEBUGGER}</Code>
|
||||
<div className="olist">
|
||||
<ol>
|
||||
<li>의심 가는 줄에 <strong>브레이크포인트</strong>(빨간 점)를 찍고,</li>
|
||||
<li><strong>벌레 아이콘(Debug)</strong>으로 실행한 뒤,</li>
|
||||
<li>멈춘 순간의 <strong>Variables 창</strong>에서 변수의 진짜 값을 확인 —
|
||||
내 상상과 다른 값이 들어 있는 지점이 바로 버그의 현장입니다.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
<span className="kbd">F8</span>(한 줄씩)과 <span className="kbd">F7</span>(메서드 안으로)의
|
||||
차이만 익혀도 충분해요. 특정 변수를 계속 지켜보고 싶으면 우클릭 →
|
||||
<strong> Add to Watches</strong> — CCTV를 달아 두는 셈이죠.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>조건부 브레이크포인트</b> 반복문 1,000번 중 딱 문제 되는 한 번만 멈추고 싶다면?
|
||||
빨간 점을 우클릭하고 조건(예: <span className="icode">i == 743</span>)을 적으세요.
|
||||
그 조건이 참일 때만 멈춥니다. 디버거가 두 배로 강해지는 순간이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="Spring Boot 실행 구성" sub="우측 상단 그 초록 화살표, 제대로 알고 쓰기">
|
||||
<p>
|
||||
매일 누르는 우측 상단 <strong>초록 화살표(Run)</strong>의 정체는
|
||||
<strong> 실행 구성</strong>(Run Configuration)이에요. "무엇을, 어떤 설정으로,
|
||||
어떤 환경변수와 함께 실행할까"를 묶어 둔 <strong>출발 준비 체크리스트</strong>죠.
|
||||
</p>
|
||||
<Code>{CODE_RUN_CONFIG}</Code>
|
||||
<p>
|
||||
우리 회사 규칙과도 이어져요 — DB 비밀번호 같은 <strong>시크릿은 코드에 적지 않고
|
||||
환경변수로 주입</strong>합니다. 실행 구성의 Environment variables 칸이 바로 그 주입구예요.
|
||||
또 <strong>Active profiles</strong>에 <span className="icode">local</span>을 넣으면
|
||||
운영 설정이 아닌 내 PC용 설정(<span className="icode">application-local.yml</span>)으로 뜹니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>Run과 Debug는 형제지만 다릅니다</b> 브레이크포인트를 찍었는데 안 멈춘다면
|
||||
십중팔구 <strong>Run(▶)으로 실행</strong>한 경우예요. 빨간 점이 작동하려면 반드시
|
||||
<strong> Debug(벌레 아이콘)</strong>로 실행해야 합니다. 섹션 7 실습에서 써먹을 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="DB 도구 맛보기" sub="IntelliJ 안에서 PostgreSQL 테이블 구경하기">
|
||||
<p>
|
||||
백엔드를 짜다 보면 "지금 DB에 뭐가 들었지?"가 수시로 궁금해져요. 그때마다 터미널에서
|
||||
psql을 켜도 되지만, IntelliJ의 <strong>Database 도구창</strong>을 연결해 두면
|
||||
코드 옆에서 바로 테이블을 열어 볼 수 있습니다. 코드와 데이터를 <strong>한 화면에서</strong> —
|
||||
이게 통합(I)개발(D)환경(E)이라는 이름값이에요.
|
||||
</p>
|
||||
<Code>{CODE_DB_TOOL}</Code>
|
||||
<p>
|
||||
특히 <strong>Diagram 기능</strong>은 꼭 한번 열어 보세요. 테이블끼리 어떤 외래키로
|
||||
이어져 있는지 지도처럼 그려 줘서, 우리 데이터베이스 코스에서 배운 테이블 관계가
|
||||
한눈에 복습됩니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>안전 수칙</b> 연습은 언제나 <strong>내 PC의 Docker PostgreSQL</strong>로!
|
||||
운영(AWS EC2) DB 접속 정보는 아예 IntelliJ에 등록하지 않는 게 원칙이에요.
|
||||
필요할 땐 멘토와 함께, 읽기 전용으로만.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실습: Ctrl+클릭으로 로그인 흐름 여행" sub="배운 것 전부를 우리 backend에서 써먹기">
|
||||
<p>
|
||||
이제 종합 실습입니다. 우리 플랫폼 backend에서 <strong>로그인 요청 하나가 지나가는 길</strong>을
|
||||
Ctrl+클릭만으로 따라가 볼 거예요. 코드를 '읽는' 게 아니라 코드 속을 '여행'하는 감각 —
|
||||
이게 IntelliJ로 낯선 코드를 파악하는 표준 기술입니다.
|
||||
</p>
|
||||
<Code>{CODE_LOGIN_TOUR}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 위 여행 코스 ①~④를 지금 바로 해 보세요. 그리고 여유가 되면
|
||||
⑤까지 — 서비스 메서드에 브레이크포인트를 찍고 <strong>Debug로 서버를 띄운 뒤</strong>,
|
||||
브라우저에서 우리 플랫폼에 로그인해 보세요. 내가 누른 로그인 버튼이 지금 내 IntelliJ의
|
||||
빨간 점에서 멈춰 서는 순간, 프론트와 백엔드가 한 몸으로 이어지는 게 처음으로 '보입니다'.
|
||||
</div>
|
||||
<p>
|
||||
여행하다 길을 잃으면 당황하지 말고 <span className="kbd">Ctrl</span>+<span className="kbd">Alt</span>+<span className="kbd">←</span>.
|
||||
어디를 클릭해 들어왔든 왔던 길을 그대로 되짚어 줍니다. 낯선 코드베이스에서 이 두 개
|
||||
(들어가기: Ctrl+클릭 / 나오기: Ctrl+Alt+←)만 있으면 절대 미아가 되지 않아요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🧰 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분의 IntelliJ에는 만능 검색(<strong>Shift Shift</strong>),
|
||||
자동 수리(<strong>Alt+Enter</strong>), 코드 여행(<strong>Ctrl+클릭</strong>),
|
||||
시간 정지(<strong>디버거</strong>), 비밀 보험(<strong>로컬 히스토리</strong>)까지
|
||||
장착됐어요. 도구는 결국 쓰면서 늘어요 — 오늘 배운 단축키를{' '}
|
||||
<Link to="/learn/git"><strong>Git 코스</strong></Link>의 커밋 습관,{' '}
|
||||
<Link to="/learn/database"><strong>데이터베이스 코스</strong></Link>의 SQL 연습과
|
||||
묶어서, 다음 백엔드 과제에서 마우스 없이 하루를 버텨 보는 걸 첫 목표로 삼아 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
394
frontend/src/pages/courses/ToolPostmanPage.jsx
Normal file
394
frontend/src/pages/courses/ToolPostmanPage.jsx
Normal file
@ -0,0 +1,394 @@
|
||||
// 이 파일이 하는 일: "Postman으로 API 테스트" 코스 — 브라우저 화면 없이 백엔드 API를
|
||||
// 직접 두드리는 도구 Postman을 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (도구 소개 → 설치·화면 구성 → GET 첫걸음 → POST+로그인 → 컬렉션 → 환경변수 → 응답 검증 → 개발 흐름)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 요청/응답 예시는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 예시 상수들 ──
|
||||
|
||||
const CODE_WHY_POSTMAN = `평소의 여행 경로:
|
||||
[브라우저 화면] → 버튼 클릭 → [React 프론트] → fetch → [백엔드 API] → [DB]
|
||||
|
||||
Postman을 쓰면:
|
||||
[Postman] ──────────── 직행 ──────────→ [백엔드 API] → [DB]
|
||||
|
||||
프론트 화면이라는 '중간 단계'를 건너뛰고,
|
||||
백엔드의 문(엔드포인트)을 직접 두드려 봅니다.
|
||||
→ "버그가 프론트 탓인지 백엔드 탓인지" 를 가르는 가장 빠른 방법!`;
|
||||
|
||||
const CODE_UI_MAP = `Postman 화면 지도 (처음 열면 여기만 보면 돼요)
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ [Workspaces] [환경 선택 ▼] │ ← 오른쪽 위: 환경(섹션 6)
|
||||
├──────────┬──────────────────────────────────┤
|
||||
│ 사이드바 │ GET ▼ │ https://... 주소창 │ Send │ ← 요청 만드는 곳
|
||||
│ Collections│ ────────────────────────────────│
|
||||
│ (섹션 5) │ Params | Headers | Body | ... │ ← 요청에 딸려 보낼 것들
|
||||
│ │ ────────────────────────────────│
|
||||
│ History │ 응답 영역: Body / Status / Time │ ← 서버의 답장
|
||||
└──────────┴──────────────────────────────────┘
|
||||
|
||||
외울 건 딱 셋: ① 메서드+주소 ② Send 버튼 ③ 아래 응답 영역`;
|
||||
|
||||
const CODE_FIRST_GET = `첫 GET 요청 — 순서대로 따라 하기
|
||||
|
||||
1) 메서드 드롭다운: GET 선택 (기본값이라 그대로 두면 됨)
|
||||
2) 주소창에 입력: https://edu.awesomedevapp.com/api/health
|
||||
3) 파란 Send 버튼 클릭!
|
||||
|
||||
응답 영역에 이렇게 뜨면 성공:
|
||||
Status: 200 OK Time: 120ms Size: ...
|
||||
|
||||
{
|
||||
"status": "UP"
|
||||
}
|
||||
|
||||
→ 방금 여러분은 화면(프론트) 없이 서버와 직접 대화했습니다.
|
||||
브라우저 주소창에 쳐도 GET은 되지만, Postman은
|
||||
상태코드·시간·헤더까지 한눈에 보여주는 게 다릅니다.`;
|
||||
|
||||
const CODE_LOGIN_POST = `POST /api/auth/login — 우리 플랫폼에 진짜 로그인해 보기
|
||||
|
||||
1) 메서드: POST 로 변경
|
||||
2) 주소: https://edu.awesomedevapp.com/api/auth/login
|
||||
3) Body 탭 → raw 선택 → 오른쪽 드롭다운을 JSON 으로!
|
||||
4) 입력:
|
||||
|
||||
{
|
||||
"email": "내 계정 이메일",
|
||||
"password": "내 비밀번호"
|
||||
}
|
||||
|
||||
5) Send!
|
||||
|
||||
성공 응답 (200):
|
||||
{
|
||||
"name": "홍길동",
|
||||
"role": "INTERN"
|
||||
}
|
||||
|
||||
일부러 비밀번호를 틀려 보세요 → 401 Unauthorized 가 옵니다.
|
||||
"틀리면 어떤 답이 오는지" 확인하는 것도 테스트의 절반이에요.`;
|
||||
|
||||
const CODE_COOKIE = `로그인 후 쿠키는 어디에? — Postman이 알아서 챙깁니다
|
||||
|
||||
응답 영역의 Cookies 탭을 눌러 보세요:
|
||||
┌──────────────────────────────────────┐
|
||||
│ SESSION abc123... HttpOnly Secure │ ← 서버가 발급한 출입증
|
||||
└──────────────────────────────────────┘
|
||||
|
||||
브라우저처럼 Postman도 쿠키 통(Cookie Jar)이 있어서,
|
||||
같은 도메인으로 보내는 다음 요청에 자동으로 붙여 줍니다.
|
||||
|
||||
→ 로그인 요청 한 번 성공시킨 뒤,
|
||||
GET /api/checklist 를 보내면 '로그인한 사람'으로 인식돼요.
|
||||
로그아웃 상태를 테스트하고 싶으면 Cookies에서 지우면 끝.`;
|
||||
|
||||
const CODE_COLLECTION = `컬렉션 = 요청들을 담아 두는 폴더 (즐겨찾기 + 실행 순서)
|
||||
|
||||
📁 AWESOMEDEV 수습 플랫폼 ← 컬렉션 (New → Collection)
|
||||
├─ ① POST /api/auth/login 로그인 (출입증 받기)
|
||||
├─ ② GET /api/checklist 내 체크리스트 조회
|
||||
├─ ③ POST /api/checklist/1/submit 항목 제출
|
||||
└─ ④ POST /api/auth/logout 로그아웃
|
||||
|
||||
주소창에 매번 다시 치지 말고, 만든 요청을 Save 해서
|
||||
컬렉션에 쌓아 두세요. 위→아래로 하나씩 실행하면
|
||||
"로그인 → 조회 → 제출" 이라는 실제 사용 시나리오가 됩니다.
|
||||
|
||||
수업 시간의 실험 노트와 같아요 — 한 번 정리해 두면
|
||||
내일도, 팀 동료도 똑같이 재현할 수 있습니다.`;
|
||||
|
||||
const CODE_ENV = `환경(Environment) — 로컬 ↔ 운영을 스위치 하나로 전환
|
||||
|
||||
변수 이름: baseUrl
|
||||
|
||||
[local 환경] [prod 환경]
|
||||
baseUrl = baseUrl =
|
||||
http://localhost:8080 https://edu.awesomedevapp.com
|
||||
|
||||
요청 주소는 이렇게 씁니다 (중괄호 2개!):
|
||||
{{baseUrl}}/api/auth/login
|
||||
{{baseUrl}}/api/checklist
|
||||
|
||||
오른쪽 위 드롭다운에서 local ↔ prod 만 바꾸면
|
||||
컬렉션 전체가 통째로 다른 서버를 향합니다.
|
||||
내 PC의 Spring Boot(8080)를 테스트하다가,
|
||||
같은 요청을 운영 서버로 쏘는 걸 클릭 한 번으로!`;
|
||||
|
||||
const CODE_STATUS = `상태코드 — 서버의 답장에 찍힌 도장
|
||||
|
||||
2xx 성공 200 OK / 201 Created(만들어짐)
|
||||
4xx 요청한 쪽 잘못 400 Bad Request(형식 오류)
|
||||
401 Unauthorized(로그인 안 함)
|
||||
403 Forbidden(권한 없음 — 로그인은 했는데!)
|
||||
404 Not Found(그런 주소 없음)
|
||||
5xx 서버 잘못 500 Internal Server Error(백엔드 코드가 터짐)
|
||||
|
||||
테스트할 때 확인하는 3종 세트:
|
||||
① Status — 도장이 기대한 숫자인가? (성공만 말고 실패 케이스도!)
|
||||
② Body — JSON 내용이 설계서(명세)와 같은가?
|
||||
③ Time — 유난히 느린 API는 없나? (수백 ms가 넘으면 의심)`;
|
||||
|
||||
const CODE_WORKFLOW = `프론트 없이 백엔드부터 검증하는 개발 흐름
|
||||
|
||||
전통적(느린) 방법:
|
||||
백엔드 작성 → 프론트 화면까지 다 만듦 → 브라우저에서 클릭
|
||||
→ 안 됨 → 어디가 문제인지 몰라서 양쪽 다 뒤짐 😵
|
||||
|
||||
Postman 흐름(우리 방식):
|
||||
1) Spring Boot로 API 작성
|
||||
2) Postman으로 바로 두드림 — 200? Body 맞음? 실패 케이스 OK?
|
||||
3) 백엔드가 '확실히 맞다'는 확신을 얻은 뒤
|
||||
4) React에서 fetch 연결 → 화면이 이상하면 이제 프론트만 보면 됨!
|
||||
|
||||
범인 후보를 미리 한 명씩 지워 두는 수사와 같습니다.
|
||||
백엔드 개발자는 화면이 없어도 자기 코드를 증명할 수 있어야 해요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: 'API를 직접 두드리기' },
|
||||
{ n: 2, label: '설치와 화면 구성' },
|
||||
{ n: 3, label: 'GET 첫걸음' },
|
||||
{ n: 4, label: 'POST와 로그인' },
|
||||
{ n: 5, label: '컬렉션으로 정리' },
|
||||
{ n: 6, label: '환경변수' },
|
||||
{ n: 7, label: '상태코드와 검증' },
|
||||
{ n: 8, label: '백엔드 우선 개발 흐름' },
|
||||
];
|
||||
|
||||
export default function ToolPostmanPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 부록 · 도구</div>
|
||||
<h1>Postman으로 API 테스트</h1>
|
||||
<p>
|
||||
브라우저 화면 없이 백엔드 API의 문을 직접 두드려 보는 도구, Postman을 배웁니다.
|
||||
우리 플랫폼의 로그인 API를 진짜로 호출해 보면서 <strong>"버그가 프론트 탓인지
|
||||
백엔드 탓인지"</strong>를 스스로 가려내는 힘을 길러요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습: 우리 API 실제 호출</span>
|
||||
<span className="chip">준비물: 내 PC + 내 계정</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="API를 화면 없이 직접 두드리는 도구" sub="프론트라는 중간 단계를 건너뛰면 보이는 것들">
|
||||
<p>
|
||||
지금까지 우리는 API를 <strong>화면을 통해서만</strong> 만났어요. 버튼을 누르면
|
||||
React가 <span className="icode">fetch</span>로 요청을 보내고, 응답으로 화면이 바뀌죠.
|
||||
그런데 화면이 이상하면? 프론트가 요청을 잘못 보낸 건지, 백엔드가 답을 잘못 준 건지
|
||||
화면만 봐서는 알 수 없어요.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Postman</strong>은 백엔드의 문(엔드포인트)을 <strong>직접</strong> 두드려 보는
|
||||
도구예요. 식당에 비유하면 — 평소엔 홀 직원(프론트)을 통해 주문하지만, Postman은
|
||||
주방(백엔드)에 직접 주문서를 넣어 보는 거죠. 주방에서 제대로 된 음식이 나오면
|
||||
문제는 홀에 있고, 주방부터 이상하면 백엔드를 고쳐야 합니다.
|
||||
</p>
|
||||
<Code>{CODE_WHY_POSTMAN}</Code>
|
||||
<div className="tip">
|
||||
<b>왜 배우나요?</b> 어썸데브의 개발 흐름에서 백엔드(Spring Boot) API는
|
||||
프론트가 완성되기 <strong>전에</strong> Postman으로 먼저 검증해요. 여러분이 만들
|
||||
과제 API도 마찬가지 — 이 코스가 끝나면 화면 없이도 내 API를 증명할 수 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="설치와 화면 구성" sub="처음 열었을 때 봐야 할 곳은 딱 세 군데">
|
||||
<p>
|
||||
Postman 공식 사이트에서 무료 버전을 내려받아 설치하세요(Windows용).
|
||||
계정 가입을 권하는 화면이 나오지만, 우리 실습은 <strong>가입 없이 가볍게
|
||||
쓰는 모드로도 충분</strong>해요. 처음 화면이 복잡해 보여도 겁먹지 마세요 —
|
||||
실제로 쓰는 곳은 세 군데뿐입니다.
|
||||
</p>
|
||||
<Code>{CODE_UI_MAP}</Code>
|
||||
<p>
|
||||
핵심은 가운데 위쪽이에요. <strong>메서드(GET/POST...)를 고르고, 주소를 치고,
|
||||
Send를 누른다</strong> — 이게 Postman 사용법의 전부입니다. 나머지 탭(Params,
|
||||
Headers, Body)은 요청에 딸려 보낼 짐이고, 아래 응답 영역은 서버의 답장이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>메서드 복습</b> GET은 "보여 줘"(조회), POST는 "새로 만들어 줘/처리해 줘",
|
||||
PUT/PATCH는 "고쳐 줘", DELETE는 "지워 줘". 백엔드 코스에서 배운
|
||||
그 HTTP 메서드를 이제 손으로 직접 골라서 보냅니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="GET 요청 첫걸음" sub="서버야, 살아 있니? — 3클릭으로 첫 대화">
|
||||
<p>
|
||||
첫 요청은 가장 안전한 것부터 — 서버가 살아 있는지 묻는 <strong>헬스체크</strong>예요.
|
||||
GET은 데이터를 <strong>읽기만</strong> 하는 요청이라 몇 번을 보내도 서버에
|
||||
아무 변화가 없어요. 마음 놓고 눌러 보세요.
|
||||
</p>
|
||||
<Code>{CODE_FIRST_GET}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 위 순서대로 우리 플랫폼에 GET 요청을 보내 보세요.
|
||||
성공했다면 응답 영역에서 <strong>Status·Time·Size</strong> 세 가지를 찾아 읽어 보고,
|
||||
이번엔 주소 끝을 일부러 <span className="icode">/api/healthhh</span>처럼 틀리게 쳐서
|
||||
<strong> 404</strong>가 오는 것도 확인! "틀렸을 때 뭐가 오는지"까지 봐야 테스트예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="POST와 JSON 바디 — 우리 로그인 API 호출" sub="진짜 로그인을 화면 없이 해 보기">
|
||||
<p>
|
||||
GET과 달리 <strong>POST</strong>는 서버에 데이터를 <strong>실어 보내는</strong> 요청이에요.
|
||||
그 짐칸이 <strong>Body</strong>이고, 우리 백엔드는 짐을 <strong>JSON</strong> 형식으로
|
||||
받습니다. 편지(요청)에 소포(JSON)를 동봉하는 셈이죠. 이번엔 우리 플랫폼에
|
||||
진짜로 로그인해 봅시다 — 여러분이 매일 로그인 화면에서 누르던 그 버튼이
|
||||
뒤에서 보내는 요청과 <strong>완전히 같은</strong> 요청이에요.
|
||||
</p>
|
||||
<Code>{CODE_LOGIN_POST}</Code>
|
||||
<p>
|
||||
로그인에 성공하면 서버는 <strong>쿠키</strong>라는 출입증을 발급해요. 좋은 소식:
|
||||
Postman도 브라우저처럼 쿠키를 <strong>자동으로 보관하고, 다음 요청에 자동으로
|
||||
붙여</strong> 줍니다. 여러분이 헤더를 손으로 만질 필요가 없어요.
|
||||
</p>
|
||||
<Code>{CODE_COOKIE}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 내 계정으로 로그인 POST를 성공시킨 다음, 바로
|
||||
<span className="icode">GET {'{{'}baseUrl{'}}'}/api/checklist</span>… 는 아직이고(환경변수는 섹션 6!),
|
||||
<span className="icode"> https://edu.awesomedevapp.com/api/checklist</span>를 GET으로 보내 보세요.
|
||||
내 체크리스트 JSON이 오면 쿠키가 자동으로 따라간 거예요. 이어서 Cookies에서
|
||||
쿠키를 지우고 같은 요청을 다시 — 이번엔 <strong>401</strong>이 옵니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="컬렉션 — 우리 API를 시나리오로 정리" sub="로그인 → 조회 → 제출, 실험 노트 만들기">
|
||||
<p>
|
||||
요청을 매번 주소창에 새로 치는 건 금방 지겨워져요. Postman의
|
||||
<strong> 컬렉션(Collection)</strong>은 요청들을 담아 두는 폴더입니다.
|
||||
단순한 즐겨찾기가 아니라, 위에서 아래로 실행하면 <strong>실제 사용 시나리오</strong>가
|
||||
되도록 순서대로 정리하는 게 요령이에요.
|
||||
</p>
|
||||
<Code>{CODE_COLLECTION}</Code>
|
||||
<p>
|
||||
요청을 만들고 <span className="kbd">Ctrl</span>+<span className="kbd">S</span>로
|
||||
저장하면 컬렉션을 고를 수 있어요. 각 요청에 <strong>"① 로그인"</strong>처럼
|
||||
번호와 설명을 붙여 두면, 다음에 열었을 때 (그리고 팀 동료가 봤을 때)
|
||||
무슨 순서로 실행해야 하는지 바로 보입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ③</b> "AWESOMEDEV 수습 플랫폼" 컬렉션을 만들고,
|
||||
섹션 3~4에서 보낸 요청들을 저장해 넣으세요. 그리고 Postman을 완전히 껐다 켠 뒤
|
||||
컬렉션에서 로그인부터 순서대로 다시 실행 — 어제의 실험을 오늘 그대로
|
||||
재현할 수 있다는 것, 이게 컬렉션의 힘이에요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="환경변수 — 로컬과 운영을 스위치로 전환" sub="주소를 고치지 말고, 환경을 바꿔라">
|
||||
<p>
|
||||
여러분이 과제로 만든 Spring Boot는 <span className="icode">localhost:8080</span>에서
|
||||
돌고, 우리 운영 서버는 <span className="icode">edu.awesomedevapp.com</span>에 있죠.
|
||||
같은 API를 두 서버에서 테스트하려고 주소를 매번 고치다 보면 언젠가 실수합니다.
|
||||
그래서 <strong>환경(Environment)</strong> — 주소의 앞부분을 변수로 빼 두고,
|
||||
스위치만 바꾸는 방식을 씁니다.
|
||||
</p>
|
||||
<Code>{CODE_ENV}</Code>
|
||||
<p>
|
||||
<span className="icode">{'{{'}baseUrl{'}}'}</span>처럼 <strong>중괄호 두 겹</strong>이
|
||||
Postman의 변수 문법이에요. 오른쪽 위 드롭다운에서 환경을 고르면 컬렉션의 모든 요청이
|
||||
한꺼번에 그 서버를 향합니다. 옷은 그대로 두고 무대 배경만 바꾸는 연극 같은 거죠.
|
||||
나중에 배울 <span className="icode">.env</span> 파일(백엔드 설정 주입)과 똑같은
|
||||
발상이라는 것도 기억해 두세요 — <strong>바뀌는 값은 코드가 아니라 설정으로.</strong>
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="상태코드와 응답 검증" sub="서버의 답장에 찍힌 도장 읽는 법">
|
||||
<p>
|
||||
Send를 누른 뒤 가장 먼저 볼 곳은 Body가 아니라 <strong>Status</strong>예요.
|
||||
상태코드는 서버가 답장에 찍어 주는 도장 — 세 자리 숫자의 <strong>첫 자리</strong>만
|
||||
읽어도 큰 그림이 보입니다. 2로 시작하면 성공, 4로 시작하면 <strong>요청한 내
|
||||
쪽</strong> 잘못, 5로 시작하면 <strong>서버 쪽</strong> 잘못이에요.
|
||||
</p>
|
||||
<Code>{CODE_STATUS}</Code>
|
||||
<p>
|
||||
401과 403의 차이는 시험에 잘 나와요(진짜로 실무에서 헷갈립니다).
|
||||
<strong> 401</strong>은 "너 누군지 몰라"(로그인 안 됨), <strong>403</strong>은
|
||||
"너 누군지 아는데 이건 안 돼"(권한 부족) — 수습 계정으로 관리자 API를 부르면
|
||||
403이 오는 게 정상이에요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>운영 서버에 쓰기 요청 함부로 보내지 않기</b> — GET은 읽기라 안전하지만,
|
||||
POST·PUT·DELETE는 운영 DB의 <strong>진짜 데이터를 바꿉니다.</strong> 실수로 제출
|
||||
API를 두 번 쏘면 진짜로 두 번 제출돼요. 쓰기 연습은 반드시 <strong>내 로컬
|
||||
서버(localhost)</strong>나 멘토가 지정한 테스트 계정으로! 운영에 쓰기 요청을
|
||||
보내야 할 땐 멘토에게 먼저 확인받는 게 어썸데브의 규칙입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="프론트 없이 백엔드만 테스트하는 개발 흐름" sub="화면이 없어도 내 API를 증명하는 법">
|
||||
<p>
|
||||
마지막으로, Postman이 실무에서 <strong>언제</strong> 쓰이는지 봅시다.
|
||||
백엔드와 프론트를 한꺼번에 만들고 브라우저에서 확인하면, 버그가 났을 때
|
||||
용의자가 둘이라 수사가 오래 걸려요. 그래서 우리는 <strong>백엔드부터
|
||||
Postman으로 검증을 끝내고</strong> 프론트를 연결합니다.
|
||||
</p>
|
||||
<Code>{CODE_WORKFLOW}</Code>
|
||||
<p>
|
||||
과제에서 Spring Boot API를 만들 때 이 흐름을 그대로 쓰세요 — 컨트롤러 하나를
|
||||
만들 때마다 Postman 컬렉션에 요청을 추가하고, <strong>성공 케이스와 실패
|
||||
케이스</strong>(빈 값, 틀린 비밀번호, 없는 ID)를 모두 눌러 봅니다. 코드 리뷰 때
|
||||
"Postman으로 이렇게 확인했어요"라고 컬렉션을 보여 주면, 그게 바로
|
||||
여러분 API의 <strong>증거 자료</strong>가 됩니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ④</b> 백엔드 과제 프로젝트를
|
||||
<span className="icode"> localhost:8080</span>으로 띄우고, 섹션 6에서 만든
|
||||
<strong> local 환경</strong>으로 스위치를 바꾼 뒤 내가 만든 API를 컬렉션에
|
||||
추가해 보세요. 성공 1개 + 실패 2개(잘못된 입력, 없는 데이터) 케이스까지
|
||||
저장하면 오늘 코스 완주입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>📮 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 화면 없이도 API와 대화할 수 있어요 — GET/POST 요청 보내기, JSON 바디와
|
||||
쿠키, 컬렉션·환경변수로 정리하기, 상태코드로 답장 읽기까지. 이 도구는 앞으로
|
||||
모든 백엔드 과제의 기본기가 됩니다. 요청과 응답이 오가는 길 자체가 궁금하다면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서 HTTP의
|
||||
밑바닥을, 두드릴 API를 직접 만들고 싶다면 <strong>백엔드 과제</strong>에서
|
||||
Spring Boot 컨트롤러 작성을 이어서 배워 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
364
frontend/src/pages/courses/ToolVscodePage.jsx
Normal file
364
frontend/src/pages/courses/ToolVscodePage.jsx
Normal file
@ -0,0 +1,364 @@
|
||||
// 이 파일이 하는 일: "VS Code 200% 활용" 코스 — 매일 쓰는 에디터를 진짜 '도구'로
|
||||
// 만드는 법을 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (필수 단축키 15 → 확장 추천 → 통합 터미널 → Git 패널 → 설정 동기화 → 검색의 기술 → Emmet → 종합 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 단축키 표·예시 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 표 · 예시 상수들 ──
|
||||
|
||||
const CODE_SHORTCUTS = `필수 단축키 15 — 이것만 손에 붙으면 마우스가 심심해집니다
|
||||
|
||||
분류 단축키 하는 일
|
||||
──────────────────────────────────────────────────────────────
|
||||
만능 Ctrl+Shift+P 명령 팔레트 (VS Code의 모든 기능 검색)
|
||||
이동 Ctrl+P 파일 이름으로 바로 열기
|
||||
Ctrl+Tab 최근 연 파일 사이 전환
|
||||
편집 Alt+↑ / Alt+↓ 현재 줄을 위/아래로 통째로 이동
|
||||
Shift+Alt+↑ / ↓ 현재 줄 복제
|
||||
Ctrl+Shift+K 현재 줄 삭제 (드래그 필요 없음!)
|
||||
Ctrl+/ 주석 토글 (여러 줄 선택해도 OK)
|
||||
멀티커서 Ctrl+D 같은 단어를 하나씩 추가 선택
|
||||
Ctrl+Shift+L 같은 단어 전부 한 번에 선택
|
||||
Alt+클릭 원하는 곳마다 커서 추가
|
||||
선택 Ctrl+L 현재 줄 전체 선택
|
||||
검색 Ctrl+F / Ctrl+Shift+F 현재 파일 검색 / 프로젝트 전체 검색
|
||||
화면 Ctrl+B 사이드바 접기/펴기
|
||||
Ctrl+\` 통합 터미널 열기/닫기 (\`는 1 왼쪽 키)
|
||||
정리 Shift+Alt+F 문서 자동 정렬(포맷)
|
||||
|
||||
→ 한 번에 다 외우려 하지 마세요. 오늘은 위에서 5개만!`;
|
||||
|
||||
const CODE_EXTENSIONS = `추천 확장 4종 — 왼쪽 사이드바의 '확장(Extensions)' 아이콘에서 검색
|
||||
|
||||
이름 하는 일
|
||||
──────────────────────────────────────────────────────────
|
||||
Korean Language Pack 메뉴 전체를 한국어로 (초반 진입장벽 제거)
|
||||
Prettier 저장할 때 코드를 자동으로 예쁘게 정렬
|
||||
ES7+ React Snippets "rafce" 치고 Tab → 컴포넌트 뼈대 자동 완성
|
||||
GitLens 코드 한 줄 한 줄 "누가 언제 왜 고쳤는지" 표시
|
||||
|
||||
설치 팁: 확장은 많다고 좋은 게 아니에요.
|
||||
안 쓰는 확장은 에디터를 느리게 만드는 짐 — 필요할 때 하나씩!`;
|
||||
|
||||
const CODE_PRETTIER_SETTING = `Prettier를 '저장하면 자동 정렬'로 만들기:
|
||||
|
||||
1) Ctrl+Shift+P → "settings" 입력 → "기본 설정: 설정 열기(UI)" 선택
|
||||
2) 검색창에 format on save 입력 → 체크박스 켜기
|
||||
3) 검색창에 default formatter 입력 → Prettier 선택
|
||||
|
||||
이제부터 Ctrl+S를 누를 때마다 들쭉날쭉한 코드가 착착 정렬됩니다.
|
||||
"코드 정렬로 리뷰에서 지적받을 일"이 영원히 사라져요.`;
|
||||
|
||||
const CODE_SNIPPET = `ES7 스니펫 맛보기 — 새 파일에서 "rafce" 네 글자 치고 Tab:
|
||||
|
||||
const 파일이름 = () => {
|
||||
return (
|
||||
<div>파일이름</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default 파일이름;
|
||||
|
||||
→ 우리 프로젝트의 컴포넌트 뼈대가 4글자 만에 완성!
|
||||
(rafce = React Arrow Function Component with Export)`;
|
||||
|
||||
const CODE_TERMINAL = `통합 터미널 — 에디터 안에 사는 터미널
|
||||
|
||||
열기: Ctrl+\` (백틱, 숫자 1 왼쪽 키)
|
||||
|
||||
왜 좋은가?
|
||||
- 지금 열려 있는 폴더에서 바로 시작됨 (cd 할 필요 없음)
|
||||
- 코드 보면서 아래쪽에서 npm run dev 실행 → 에러 나면 바로 위에서 수정
|
||||
- + 버튼으로 터미널 여러 개: 하나는 서버 돌리고, 하나는 git 명령용
|
||||
|
||||
우리 frontend 폴더에서의 하루:
|
||||
Ctrl+\` 터미널 열고
|
||||
npm run dev 개발 서버 켜고
|
||||
+ 버튼 새 터미널 하나 더 열어서
|
||||
git status 작업 상태 확인 — 전부 에디터 안에서!`;
|
||||
|
||||
const CODE_GIT_PANEL = `Git 패널로 커밋/푸시 — 명령어를 '눈'으로 확인하며
|
||||
|
||||
왼쪽 사이드바의 소스 제어 아이콘(가지 모양) 클릭 = Ctrl+Shift+G
|
||||
|
||||
① 변경(Changes) 목록에서 파일 클릭
|
||||
→ 뭐가 바뀌었는지 좌우 비교 화면(diff)이 뜸. 커밋 전 셀프 리뷰!
|
||||
② 파일 옆 + 버튼 = 스테이징 (git add와 같음)
|
||||
③ 위 입력창에 커밋 메시지 쓰고 체크(✓) 버튼 = git commit
|
||||
④ 아래 "변경 내용 동기화" 버튼 = git push + pull
|
||||
|
||||
터미널 명령이 익숙해질 때까지는 패널이 좋은 안전장치예요.
|
||||
diff 화면에서 "어? 이건 왜 바뀌었지?" 하고 실수를 잡는 게 핵심.`;
|
||||
|
||||
const CODE_SETTINGS_SYNC = `설정 동기화(Settings Sync) — 내 에디터를 통째로 백업
|
||||
|
||||
왼쪽 아래 사람/톱니 아이콘 → "설정 동기화 켜기(Backup and Sync)"
|
||||
→ GitHub 계정으로 로그인하면 끝.
|
||||
|
||||
동기화되는 것: 설정, 단축키, 확장 목록, 스니펫, UI 상태
|
||||
|
||||
언제 빛을 발하나?
|
||||
- 집 PC ↔ 학교 PC ↔ 회사 PC, 어디서 로그인해도 같은 환경
|
||||
- PC를 새로 밀어도 로그인 한 번이면 확장까지 전부 복원
|
||||
→ "에디터 세팅에 반나절" 하는 일이 인생에서 사라집니다.`;
|
||||
|
||||
const CODE_SEARCH = `검색의 기술 — Ctrl+P와 Ctrl+Shift+F는 용도가 달라요
|
||||
|
||||
Ctrl+P = 파일 '이름'으로 찾기 (도서관에서 책 제목으로 찾기)
|
||||
netpa 만 쳐도 → NetworkPage.jsx 후보로 등장 (부분 일치 OK)
|
||||
|
||||
Ctrl+Shift+F = 파일 '내용'으로 프로젝트 전체 검색 (책 속 문장으로 찾기)
|
||||
step-card 검색 → 이 클래스를 쓰는 모든 파일과 줄이 주르륵
|
||||
|
||||
실전 시나리오:
|
||||
"이 버튼 문구 어느 파일에 있지?" → 화면에 보이는 문구 그대로
|
||||
Ctrl+Shift+F에 붙여넣기. 모르는 코드베이스를 탐험하는 1번 도구!
|
||||
|
||||
보너스: 검색창의 .* 아이콘을 켜면 정규식 검색,
|
||||
파일 필터에 *.jsx 를 넣으면 특정 확장자만 검색.`;
|
||||
|
||||
const CODE_EMMET = `Emmet 맛보기 — HTML을 수식처럼 쓰기 (VS Code 내장, 설치 불필요)
|
||||
|
||||
치고 Tab 결과
|
||||
─────────────────────────────────────────────
|
||||
div.card <div className="card"></div>
|
||||
ul>li*3 <ul>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
div.row>div.col*2 클래스 붙은 중첩 구조가 한 번에!
|
||||
|
||||
문법 요약: . = 클래스, > = 자식, * = 반복
|
||||
JSX 파일에서도 동작해요(className으로 알아서 변환).
|
||||
리스트 UI 뼈대 잡을 때 타자 수가 1/10로 줄어듭니다.`;
|
||||
|
||||
const CODE_PRACTICE = `종합 실습 — 우리 frontend 폴더에서 단축키 10개 체크리스트
|
||||
|
||||
준비: VS Code에서 [파일 → 폴더 열기]로 mirim-app/frontend 열기
|
||||
|
||||
□ 1. Ctrl+P → "network" 입력 → NetworkPage.jsx 열기
|
||||
□ 2. Ctrl+F → "직접 확인" 검색 → 몇 개 나오나요?
|
||||
□ 3. Ctrl+Shift+F → "step-card" 전체 검색 → 몇 개 파일에서 쓰이나요?
|
||||
□ 4. 아무 줄에서 Ctrl+L → 줄 전체 선택
|
||||
□ 5. 그 줄을 Alt+↓ 로 아래로 옮겼다가 Alt+↑ 로 원위치
|
||||
□ 6. Shift+Alt+↓ 로 줄 복제 → Ctrl+Shift+K 로 복제한 줄 삭제
|
||||
□ 7. 변수 이름 하나에 커서 두고 Ctrl+D 를 3번 → 하나씩 추가 선택
|
||||
□ 8. Ctrl+/ 로 주석 만들었다가 다시 Ctrl+/ 로 해제
|
||||
□ 9. Ctrl+\` 로 터미널 열고 git status 실행
|
||||
□ 10. Ctrl+Shift+P → "reload" 입력 → 창 다시 로드 실행
|
||||
|
||||
⚠ 5~8번으로 파일이 바뀌었다면 Ctrl+Z 로 전부 되돌리고,
|
||||
git status 로 "변경 사항 없음"을 확인하며 마무리!`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '필수 단축키 15' },
|
||||
{ n: 2, label: '확장 추천 4종' },
|
||||
{ n: 3, label: '통합 터미널' },
|
||||
{ n: 4, label: 'Git 패널' },
|
||||
{ n: 5, label: '설정 동기화' },
|
||||
{ n: 6, label: '검색의 기술' },
|
||||
{ n: 7, label: 'Emmet 맛보기' },
|
||||
{ n: 8, label: '종합 실습' },
|
||||
];
|
||||
|
||||
export default function ToolVscodePage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 무엇을 바꿔 주는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 부록 · 도구</div>
|
||||
<h1>VS Code 200% 활용</h1>
|
||||
<p>
|
||||
여러분이 어썸데브에서 매일 가장 오래 붙어 있을 프로그램이 바로 VS Code예요.
|
||||
요리사가 칼을 갈듯이, 이 코스에서 에디터를 <strong>손에 딱 붙는 도구</strong>로
|
||||
만들어 봅니다 — 배운 건 그 자리에서 우리 <span className="icode">frontend</span> 폴더에
|
||||
바로 써 볼 거예요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">단축키 15개 + 실습 체크리스트 10개</span>
|
||||
<span className="chip">준비물: VS Code가 깔린 내 PC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="필수 단축키 15" sub="마우스에서 손을 떼는 첫걸음">
|
||||
<p>
|
||||
단축키는 <strong>속도</strong> 문제이기 전에 <strong>흐름</strong> 문제예요.
|
||||
코드를 고치다 마우스로 손이 갈 때마다 생각이 한 번씩 끊기거든요.
|
||||
자전거 보조 바퀴를 떼는 것처럼, 처음엔 어색해도 일주일이면 몸이 기억합니다.
|
||||
</p>
|
||||
<Code>{CODE_SHORTCUTS}</Code>
|
||||
<p>
|
||||
이 중에 딱 하나만 고르라면 <span className="kbd">Ctrl+Shift+P</span>,
|
||||
<strong> 명령 팔레트</strong>예요. VS Code의 모든 기능이 이 검색창 하나에 들어 있어서,
|
||||
"그 기능 메뉴 어디 있더라?" 할 때 그냥 이름을 쳐서 실행하면 됩니다.
|
||||
단축키를 까먹어도 팔레트만 기억하면 살아남아요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 VS Code에서 <span className="kbd">Ctrl+Shift+P</span>를
|
||||
누르고 <span className="icode">theme</span>이라고 쳐 보세요. "색 테마" 항목을 고르면
|
||||
방향키만으로 에디터 색이 실시간으로 바뀌어요 — 팔레트가 어떤 느낌인지 3초 만에 체험!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="확장 추천 4종" sub="에디터에 앱 설치하듯 기능 더하기">
|
||||
<p>
|
||||
확장(Extension)은 스마트폰의 앱 같은 거예요. 기본 VS Code도 훌륭하지만,
|
||||
확장을 붙이면 우리 팀 스타일에 딱 맞게 커스텀할 수 있죠.
|
||||
어썸데브 수습에게 필요한 건 이 네 개면 충분합니다.
|
||||
</p>
|
||||
<Code>{CODE_EXTENSIONS}</Code>
|
||||
<p>
|
||||
이 중 <strong>Prettier</strong>는 설치 후 설정 한 번만 해 주면 진가를 발휘해요.
|
||||
들여쓰기·따옴표·줄바꿈을 사람이 아니라 도구가 통일해 주니까,
|
||||
코드 리뷰에서 "스타일 얘기"가 사라지고 "로직 얘기"만 남습니다.
|
||||
</p>
|
||||
<Code>{CODE_PRETTIER_SETTING}</Code>
|
||||
<p>
|
||||
<strong>ES7+ React Snippets</strong>는 자주 쓰는 코드 뼈대를 몇 글자로 불러오는
|
||||
치트키예요. 우리가 React로 페이지를 만들 때마다 쓰게 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_SNIPPET}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="통합 터미널" sub="에디터 밖으로 나갈 필요가 없어요">
|
||||
<p>
|
||||
코드는 에디터에서, 명령어는 따로 띄운 터미널 창에서 — 이렇게 두 창을 오가면
|
||||
<span className="kbd">Alt+Tab</span> 곡예를 하게 돼요. VS Code 안에 터미널을 넣으면
|
||||
<strong> 코드와 실행 결과를 한 화면에서</strong> 봅니다. 요리하면서 냄비를
|
||||
옆방에 두지 않는 것과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_TERMINAL}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> <span className="kbd">Ctrl+`</span>로 터미널을 열고
|
||||
<span className="icode">npm run dev</span>를 실행한 뒤, 위쪽 에디터에서 아무 페이지의
|
||||
문구 하나를 살짝 바꿔 저장해 보세요. 브라우저가 즉시 반영되고, 터미널엔 로그가 흐르는 걸
|
||||
한 화면에서 지켜볼 수 있어요. (확인 후 <span className="kbd">Ctrl+Z</span>로 원상복구!)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="Git 패널로 커밋/푸시" sub="명령어가 하는 일을 눈으로 보며 배우기">
|
||||
<p>
|
||||
터미널에서 <span className="icode">git add · commit · push</span>를 치는 것도 좋지만,
|
||||
VS Code의 <strong>소스 제어 패널</strong>을 쓰면 각 단계가 <strong>눈에 보여요</strong>.
|
||||
특히 커밋 전에 뜨는 <strong>diff(비교) 화면</strong>은 "내가 뭘 바꿨는지"를
|
||||
제출 전에 다시 읽는 셀프 검토 시간이 됩니다 — 시험지 제출 전 검산과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_GIT_PANEL}</Code>
|
||||
<div className="warn">
|
||||
<b>커밋 전 diff 확인은 습관으로!</b> 실수로 딸려 들어간 <span className="icode">console.log</span>,
|
||||
테스트하려고 바꿔 둔 값 같은 것들이 diff 화면에서 다 걸립니다.
|
||||
우리 Gitea(edu.awesomedevapp.com)에 올라간 커밋은 팀 전체가 보는 기록이에요 —
|
||||
"무엇을 올리는지 모르고 올리는" 일만은 피해 주세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="설정 동기화" sub="공들인 세팅, 계정에 통째로 백업">
|
||||
<p>
|
||||
단축키 익히고, 확장 깔고, 설정 맞추고… 여기까지 만든 <strong>나만의 작업 환경</strong>이
|
||||
PC를 바꾸는 순간 증발하면 억울하겠죠? 설정 동기화를 켜 두면 이 모든 게
|
||||
계정에 저장돼서, 어떤 PC에서든 로그인 한 번으로 <strong>내 에디터</strong>가 됩니다.
|
||||
게임의 클라우드 세이브와 똑같아요.
|
||||
</p>
|
||||
<Code>{CODE_SETTINGS_SYNC}</Code>
|
||||
<div className="tip">
|
||||
<b>지금 켜 두세요</b> 수습 기간엔 학교 PC·집 PC·회사 PC를 오가게 될 확률이 높아요.
|
||||
오늘 5분 투자하면 "새 PC에서 확장이 없어서…" 같은 변명거리가 사라집니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="검색의 기술" sub="Ctrl+P vs Ctrl+Shift+F — 도서관 사서처럼 찾기">
|
||||
<p>
|
||||
모르는 코드베이스에서 길을 찾는 능력은 코드를 쓰는 능력만큼 중요해요.
|
||||
수습 첫 주에 여러분이 가장 많이 할 일도 "이거 어디서 처리하지?" 하고
|
||||
<strong> 찾아 들어가는 일</strong>이거든요. 핵심은 두 검색의 용도 구분입니다.
|
||||
</p>
|
||||
<Code>{CODE_SEARCH}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 플랫폼 화면에서 아무 버튼의 문구를 하나 골라
|
||||
<span className="kbd">Ctrl+Shift+F</span>로 검색해 보세요. 그 문구가 적힌 파일이
|
||||
바로 나옵니다 — "화면에서 본 것 → 코드 위치"를 잇는 이 기술이
|
||||
앞으로 과제할 때 여러분의 내비게이션이 돼요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="Emmet 맛보기" sub="HTML 태그를 수식 한 줄로">
|
||||
<p>
|
||||
<span className="icode"><div></span> 열고 닫고, 클래스 붙이고…
|
||||
반복 구조를 손으로 치다 보면 오타가 나기 마련이에요. <strong>Emmet</strong>은
|
||||
"만들고 싶은 구조"를 축약식으로 쓰면 태그로 펼쳐 주는 내장 기능입니다.
|
||||
문장을 다 쓰는 대신 줄임말을 쓰는 것과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_EMMET}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 연습용 새 파일(<span className="icode">test.html</span>)을 만들고
|
||||
<span className="icode">ul>li*5</span>를 친 다음 <span className="kbd">Tab</span>을
|
||||
눌러 보세요. 리스트 5줄이 촤르륵 펼쳐집니다. 다 놀았으면 파일은 저장하지 말고 닫기!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="종합 실습 — 단축키 10개 체크리스트" sub="우리 frontend 폴더에서 실전으로">
|
||||
<p>
|
||||
이제 배운 걸 전부 꺼내 봅니다. 아래 체크리스트를 <strong>순서대로, 마우스 최소한으로</strong>
|
||||
해내는 게 목표예요. 1~3번은 검색(섹션 6), 4~8번은 편집 단축키(섹션 1),
|
||||
9번은 터미널(섹션 3), 10번은 팔레트(섹션 1) — 이 코스 전체의 총정리입니다.
|
||||
</p>
|
||||
<Code>{CODE_PRACTICE}</Code>
|
||||
<div className="tip">
|
||||
<b>공부 팁</b> 10개를 한 번에 통과하려 하지 말고, 내일부터 실제 과제를 할 때
|
||||
"마우스로 하려던 걸 단축키로 해 보기"를 하루 3번만 의식적으로 시도해 보세요.
|
||||
2주면 이 페이지를 다시 볼 필요가 없어집니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🛠 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 에디터가 "코드 치는 메모장"에서 <strong>탐색·편집·실행·커밋을 한 번에 하는
|
||||
작업대</strong>가 됐어요. 섹션 4에서 맛본 Git을 제대로 배우고 싶다면{' '}
|
||||
<Link to="/learn/git"><strong>Git과 협업</strong></Link> 코스로,
|
||||
갈아 둔 칼로 바로 요리를 시작하고 싶다면{' '}
|
||||
<Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스의 과제를
|
||||
오늘 배운 단축키만으로 해결해 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
384
frontend/src/pages/courses/TroubleshootingPage.jsx
Normal file
384
frontend/src/pages/courses/TroubleshootingPage.jsx
Normal file
@ -0,0 +1,384 @@
|
||||
// 이 파일이 하는 일: "컴퓨터 문제 해결" 코스 — 컴퓨터가 느려질 때 점검 순서부터
|
||||
// 블루스크린 읽기, 백업 3-2-1 원칙, 재설치, 그리고 "왜 재부팅이 만능인가"까지
|
||||
// 7개 섹션으로 안내하는 정적 학습 페이지. 마지막엔 개발자의 디버깅 사고와 연결한다.
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_SLOW_CHECKLIST = `"컴퓨터가 느려요" — 병원 문진표처럼 순서대로 점검하기
|
||||
|
||||
순서 용의자 증상 확인 방법
|
||||
──────────────────────────────────────────────────────────────
|
||||
1 시작 프로그램 부팅이 유독 오래 걸림 작업 관리자 → 시작 앱
|
||||
2 램(RAM) 부족 창 여러 개 켜면 버벅임 작업 관리자 → 성능 → 메모리
|
||||
3 디스크 뭘 해도 전반적으로 굼뜸 성능 → 디스크 100% 지속?
|
||||
4 발열 한참 쓰다 보면 느려짐 팬 소리·바닥 온도, 먼지 확인
|
||||
|
||||
포인트: 위에서부터 하나씩 지워 나가요.
|
||||
의사가 "어디가 아프세요?"부터 묻듯, 증상 → 용의자 순서로 좁힙니다.`;
|
||||
|
||||
const CODE_TASKMGR = `작업 관리자 여는 법 3가지 (하나만 외워도 OK)
|
||||
|
||||
1) Ctrl + Shift + Esc ← 가장 빠른 지름길!
|
||||
2) Ctrl + Alt + Delete → 작업 관리자
|
||||
3) 작업 표시줄 우클릭 → 작업 관리자
|
||||
|
||||
범인 찾기 루틴:
|
||||
[프로세스 탭] 열 머리글 "CPU" 클릭 → 사용량 높은 순 정렬
|
||||
"메모리" 클릭 → 램 먹는 순 정렬
|
||||
→ 맨 위에 있는 프로그램이 지금 컴퓨터를 무겁게 만드는 1순위 용의자.
|
||||
|
||||
[성능 탭] CPU·메모리·디스크 그래프가 계속 90~100%에 붙어 있다면
|
||||
그 부품이 병목(가장 좁은 길목)이라는 뜻이에요.`;
|
||||
|
||||
const CODE_BSOD = `블루스크린(BSOD)이 떴을 때 — 당황하지 말고 '단서 두 개'만 챙기기
|
||||
|
||||
┌────────────────────────────────────────────┐
|
||||
│ :( Your PC ran into a problem ... │
|
||||
│ │
|
||||
│ 중지 코드: MEMORY_MANAGEMENT ← 단서 ① │
|
||||
│ 실패한 항목: xxxxxx.sys ← 단서 ② │
|
||||
└────────────────────────────────────────────┘
|
||||
|
||||
단서 ① 중지 코드 = 사망 원인명. 대략적인 방향을 알려줘요.
|
||||
MEMORY_MANAGEMENT → 램 쪽 문제일 가능성
|
||||
CRITICAL_PROCESS_DIED → 윈도우 핵심 프로세스가 죽음
|
||||
DRIVER_IRQL_NOT_LESS_... → 드라이버(장치 제어 프로그램) 충돌
|
||||
|
||||
단서 ② .sys 파일명 = 용의자 실명. 어떤 드라이버가 넘어졌는지 알려줘요.
|
||||
|
||||
→ 이 둘을 메모(폰 카메라로 찰칵!)해서 그대로 검색하는 게
|
||||
섹션 7에서 배울 "문제 검색 요령"의 출발점입니다.`;
|
||||
|
||||
const CODE_321 = `백업의 3-2-1 원칙 — 소중한 데이터의 안전벨트
|
||||
|
||||
3 — 데이터 복사본을 총 3개 유지 (원본 1 + 백업 2)
|
||||
2 — 서로 다른 2가지 저장 매체에 (예: 내 PC + 외장하드)
|
||||
1 — 그중 1개는 다른 장소에 (예: 클라우드, 회사 밖)
|
||||
|
||||
예) 수습 과제 코드라면:
|
||||
① 내 노트북 작업 폴더 (원본)
|
||||
② Gitea(edu.awesomedevapp.com) (다른 매체 + 다른 장소, 1석 2조!)
|
||||
③ 외장하드 or 학교 PC (하나 더)
|
||||
|
||||
왜 '다른 장소'까지? 노트북과 외장하드를 같은 가방에 넣고 다니면
|
||||
가방을 잃어버리는 순간 둘 다 사라져요. 달걀을 한 바구니에 담지 말 것!`;
|
||||
|
||||
const CODE_REINSTALL = `포맷·윈도우 재설치 = 컴퓨터의 '이사'
|
||||
|
||||
이사 절차와 똑같이 생각하면 실수가 없어요:
|
||||
|
||||
① 짐 싸기(백업) 문서·사진·바탕화면·브라우저 북마크·
|
||||
개발 환경 설정, 그리고 Git에 안 올린 코드!
|
||||
② 집 비우기(포맷) 디스크의 기존 내용을 싹 지움 — 되돌릴 수 없음
|
||||
③ 새 집 입주(설치) 윈도우를 새로 깔면 '공장 초기 상태'
|
||||
④ 짐 풀기(복원) 백업해 둔 파일 복사 + 프로그램 재설치
|
||||
|
||||
순서 중 가장 중요한 건 ①. 포맷 버튼은 ①이 100% 끝난 뒤에만!
|
||||
"재설치했더니 빨라졌어요"의 정체: 몇 년간 쌓인 프로그램 찌꺼기·
|
||||
시작 프로그램·임시 파일이 전부 사라졌기 때문이에요.`;
|
||||
|
||||
const CODE_REBOOT = `"껐다 켜 보셨어요?"가 만능인 진짜 이유 — 상태(state) 초기화
|
||||
|
||||
컴퓨터가 오래 켜져 있으면 이런 '보이지 않는 상태'가 쌓여요:
|
||||
|
||||
램에 쌓인 것들: 종료 안 된 프로그램 찌꺼기, 반납 안 된 메모리(누수),
|
||||
꼬여 버린 프로그램끼리의 대기 상태(교착)
|
||||
─────────────────────────────────────────────
|
||||
재부팅하면? 램은 전원이 꺼지면 내용이 전부 사라지는 메모리!
|
||||
→ 쌓인 찌꺼기·꼬임이 통째로 리셋
|
||||
→ 모든 프로그램이 '방금 설치한 것 같은' 깨끗한 상태로 재시작
|
||||
|
||||
개발자 버전으로 번역하면:
|
||||
프로그램이 이상하다 → 프로세스 재시작
|
||||
서버가 이상하다 → docker compose restart
|
||||
전부 다 이상하다 → EC2 인스턴스 재부팅
|
||||
전부 같은 원리 — "꼬인 상태를 버리고 깨끗한 초기 상태에서 다시".
|
||||
단, 재부팅은 증상을 지울 뿐 원인은 못 고쳐요. 매일 꼬인다면 원인 추적!`;
|
||||
|
||||
const CODE_SEARCH = `문제 검색의 기술 — 에러 메시지는 '번역하지 말고 그대로'
|
||||
|
||||
나쁜 검색: 컴퓨터 파란 화면 뜨면서 꺼짐 ㅠㅠ
|
||||
좋은 검색: "CRITICAL_PROCESS_DIED" 윈도우11
|
||||
|
||||
나쁜 검색: 리액트 하다가 에러남
|
||||
좋은 검색: "Objects are not valid as a React child"
|
||||
|
||||
요령 3가지:
|
||||
1) 에러 메시지를 복사해서 그대로 붙여넣기 (요약·의역 금지)
|
||||
2) 핵심 문구는 "따옴표"로 감싸기 → 그 문장이 통째로 들어간 글만 검색됨
|
||||
3) 내 상황 키워드 1~2개 추가 (윈도우11, react, spring boot, docker ...)
|
||||
|
||||
단, 내 파일 경로나 프로젝트 이름처럼 '나만의 부분'은 빼고 검색하세요.
|
||||
C:\\Users\\kim\\... 이 들어간 에러를 그대로 검색하면 아무도 못 겪은
|
||||
에러가 되니까요 — 공통 부분만 남기는 게 포인트!`;
|
||||
|
||||
const CODE_DEBUG_MINDSET = `오늘 배운 것 = 사실 개발자의 디버깅 사고방식
|
||||
|
||||
컴퓨터 문제 해결 개발자의 디버깅
|
||||
────────────────────────────────────────────────────
|
||||
증상부터 관찰 (느림? 언제부터?) 버그 재현 조건 확인
|
||||
용의자를 순서대로 지워 나감 원인 후보를 하나씩 배제
|
||||
작업 관리자로 데이터 확인 로그·모니터링 확인
|
||||
블루스크린 코드 = 단서 수집 에러 메시지·스택트레이스 읽기
|
||||
에러 그대로 + 따옴표 검색 에러 그대로 + 따옴표 검색 (똑같음!)
|
||||
재부팅 = 상태 초기화 프로세스/컨테이너 재시작
|
||||
백업 먼저, 포맷은 마지막 되돌릴 수 없는 조치는 마지막
|
||||
|
||||
→ 여러분은 이미 디버깅의 절반을 배웠습니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '느릴 때 점검 순서' },
|
||||
{ n: 2, label: '작업 관리자' },
|
||||
{ n: 3, label: '블루스크린 읽기' },
|
||||
{ n: 4, label: '백업 3-2-1' },
|
||||
{ n: 5, label: '포맷과 재설치' },
|
||||
{ n: 6, label: '재부팅의 원리' },
|
||||
{ n: 7, label: '검색의 기술' },
|
||||
];
|
||||
|
||||
export default function TroubleshootingPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 컴퓨터 기초</div>
|
||||
<h1>컴퓨터 문제 해결<br />— 당황하지 않고 범인을 찾는 법</h1>
|
||||
<p>
|
||||
"컴퓨터가 느려요", "파란 화면이 떴어요" — 누구나 겪지만 아무나 침착하게 대응하진
|
||||
못하는 상황들을 순서대로 정복해 봅니다. 그리고 마지막에 깨닫게 될 거예요 —
|
||||
이 과정이 사실 <strong>개발자가 버그를 잡는 사고방식 그 자체</strong>라는 걸요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 3개</span>
|
||||
<span className="chip">준비물: 내 PC 하나</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="컴퓨터가 느려질 때 점검 순서" sub="증상 → 용의자, 의사처럼 문진하기">
|
||||
<p>
|
||||
컴퓨터가 느려지는 원인은 대부분 <strong>네 가지 용의자</strong> 중 하나예요.
|
||||
시작 프로그램, 램 부족, 디스크, 발열. 아무거나 찔러 보는 게 아니라,
|
||||
의사가 문진표를 채우듯 <strong>증상에 맞춰 순서대로</strong> 지워 나갑니다.
|
||||
</p>
|
||||
<Code>{CODE_SLOW_CHECKLIST}</Code>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>시작 프로그램</strong> — 부팅할 때 자동으로 켜지는 프로그램들이에요.
|
||||
등교 시간에 친구 열 명이 문 앞에서 한꺼번에 말을 거는 상황과 같죠.
|
||||
설치한 프로그램들이 슬쩍 등록해 둔 게 몇 년 쌓이면 부팅이 몇 분씩 걸립니다.
|
||||
</li>
|
||||
<li>
|
||||
<strong>램(RAM) 부족</strong> — 램은 <strong>책상 넓이</strong>예요.
|
||||
책상이 좁은데 교과서를 열 권 펼치면(창을 많이 열면), 책을 바닥(디스크)에
|
||||
내려놨다 다시 올렸다 하느라 느려집니다.
|
||||
</li>
|
||||
<li>
|
||||
<strong>디스크</strong> — 오래된 HDD(회전 원판 방식)라면 SSD로 바꾸는 것만으로
|
||||
딴 컴퓨터가 돼요. 도서관 사서가 서가를 걸어다니며 책을 찾는 것(HDD)과
|
||||
책 위치를 전부 외우고 있는 것(SSD)의 차이거든요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>발열</strong> — CPU는 너무 뜨거워지면 고장 나지 않으려고
|
||||
<strong> 스스로 속도를 줄여요</strong>(스로틀링). 한여름 체육 시간에 전력 질주를
|
||||
계속 못 하는 것과 같아요. 팬 소리가 유독 크거나 노트북 바닥이 뜨겁다면,
|
||||
내부 먼지 청소가 답일 수 있습니다.
|
||||
</li>
|
||||
</ol>
|
||||
<div className="tip">
|
||||
<b>순서가 곧 실력</b> "느리다"는 막연한 증상을 "부팅만 느린가? 켜 두면 점점
|
||||
느려지나? 항상 느린가?"로 쪼개는 순간, 용의자가 절반으로 줄어요.
|
||||
질문을 잘게 쪼개는 것 — 이게 문제 해결의 첫 기술입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="작업 관리자로 범인 찾기" sub="추측 말고 데이터로 — 내 PC의 실시간 CCTV">
|
||||
<p>
|
||||
섹션 1의 용의자들을 <strong>감으로 찍지 않고 데이터로 확인</strong>하는 도구가
|
||||
바로 <strong>작업 관리자</strong>예요. 지금 어떤 프로그램이 CPU와 램을 얼마나
|
||||
쓰는지 실시간으로 보여 주는, 내 PC의 CCTV 관제실이죠.
|
||||
</p>
|
||||
<Code>{CODE_TASKMGR}</Code>
|
||||
<p>
|
||||
<strong>시작 앱 탭</strong>도 꼭 보세요. 부팅 때 자동 실행되는 프로그램 목록과
|
||||
"시작 시 영향"이 나오는데, 여기서 안 쓰는 프로그램을 <strong>사용 안 함</strong>으로
|
||||
바꾸는 것만으로 부팅이 눈에 띄게 빨라집니다. (프로그램 삭제가 아니라
|
||||
자동 실행만 끄는 거라 안전해요.)
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>프로세스 끝내기는 신중하게</b> 이름을 모르는 프로세스를 함부로 "작업 끝내기"
|
||||
하지 마세요. 윈도우 핵심 프로세스를 끊으면 시스템이 불안정해질 수 있어요.
|
||||
모르는 이름은 먼저 검색(섹션 7의 요령으로!)해서 정체를 확인한 다음 판단합니다.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 바로 <span className="kbd">Ctrl</span> +{' '}
|
||||
<span className="kbd">Shift</span> + <span className="kbd">Esc</span>를 눌러 보세요.
|
||||
프로세스 탭에서 <strong>메모리</strong> 열을 클릭해 정렬하면, 지금 이 학습 플랫폼을
|
||||
띄우고 있는 브라우저가 램을 얼마나 쓰는지 보여요. 탭을 몇 개 더 열고 닫으면서
|
||||
숫자가 변하는 걸 관찰해 보세요 — "탭 = 램"이 몸으로 느껴집니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="블루스크린 읽는 법 한 입" sub="파란 화면은 사고가 아니라 '사고 보고서'">
|
||||
<p>
|
||||
블루스크린(BSOD)은 윈도우가 <strong>"이대로 계속 돌면 데이터가 망가질 수 있어서
|
||||
내가 먼저 멈췄어"</strong>라고 알려 주는 화면이에요. 사고 현장이 아니라
|
||||
<strong> 사고 보고서</strong>인 셈이죠. 그러니 읽을 줄만 알면 무섭지 않습니다.
|
||||
</p>
|
||||
<Code>{CODE_BSOD}</Code>
|
||||
<p>
|
||||
한 번 뜨고 마는 블루스크린은 우연일 수 있어요. 하지만 <strong>같은 중지 코드로
|
||||
반복해서</strong> 뜬다면 패턴이 있는 거예요 — 특정 프로그램을 켤 때만? 게임 중에만?
|
||||
이 "언제"가 원인을 찾는 가장 큰 단서가 됩니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>화면이 금방 꺼져서 못 읽었다면</b> 당황해서 코드를 못 적었어도 괜찮아요.
|
||||
블루스크린 기록은 윈도우 안에 남아 있어서 나중에 찾아볼 수 있고, 다음에 또 뜨면
|
||||
<strong> 폰 카메라로 찍는 것</strong>이 가장 확실합니다. 중지 코드와{' '}
|
||||
<span className="icode">.sys</span> 파일명, 딱 두 줄이면 충분해요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="백업의 3-2-1 원칙" sub="문제 해결의 최종 보험 — 고치기 전에 지켜라">
|
||||
<p>
|
||||
지금까지는 "고치는 법"이었다면, 이번엔 <strong>"잃지 않는 법"</strong>이에요.
|
||||
아무리 잘 고쳐도 디스크가 갑자기 죽으면 몇 달치 과제가 통째로 사라질 수 있거든요.
|
||||
그래서 전 세계 엔지니어들이 약속처럼 쓰는 규칙이 <strong>3-2-1</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_321}</Code>
|
||||
<p>
|
||||
눈치챘나요? 우리가 매일 쓰는 <strong>Gitea에 코드를 push하는 것</strong>이
|
||||
그 자체로 3-2-1의 큰 조각이에요. 내 노트북(원본)과 다른 매체·다른 장소(회사 서버)에
|
||||
복사본이 생기니까요. "커밋을 자주, push를 자주"가 괜히 개발자의 습관이 아닙니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 진행 중인 수습 과제 폴더에서{' '}
|
||||
<span className="icode">git status</span>를 쳐 보세요. 커밋 안 된 변경이나
|
||||
push 안 된 커밋이 있다면, 그 코드는 아직 <strong>복사본 1개(내 노트북)</strong>짜리
|
||||
상태예요. 정리해서 push하는 순간 3-2-1에 한 걸음 다가갑니다 —
|
||||
오늘부터 퇴근 전 push를 습관으로!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="포맷과 윈도우 재설치 개요" sub="마지막 수단 = 컴퓨터의 이사">
|
||||
<p>
|
||||
점검해도 원인을 못 찾겠고 시스템이 전반적으로 엉망이라면, 마지막 카드가
|
||||
<strong> 포맷 후 재설치</strong>예요. 집이 너무 어질러져서 청소로는 답이 없을 때
|
||||
아예 이사를 가는 것과 같죠. 단, <strong>이사는 짐부터 싸는 게 순서</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_REINSTALL}</Code>
|
||||
<div className="warn">
|
||||
<b>포맷은 되돌릴 수 없어요</b> 포맷 버튼을 누르는 순간 그 디스크의 파일은
|
||||
사라집니다. 그래서 순서가 생명이에요 — <strong>백업 확인(섹션 4) → 목록으로
|
||||
이중 체크 → 그다음에 포맷</strong>. "바탕화면에 뒀던 파일"과 "다운로드 폴더",
|
||||
그리고 <strong>Git에 push 안 한 코드</strong>가 단골 실종자이니 꼭 챙기세요.
|
||||
</div>
|
||||
<p>
|
||||
개발자에게도 익숙한 그림이에요. 우리가 쓰는 <strong>Docker</strong>가 바로
|
||||
"언제든 컨테이너를 지우고 이미지에서 새로 깔끔하게 시작"하는 도구거든요.
|
||||
재설치의 고통을 아는 사람들이 "처음부터 다시 만들기 쉬운 환경"을 발명한 겁니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="재부팅이 만능인 이유" sub="'껐다 켜기'의 과학 — 상태 초기화">
|
||||
<p>
|
||||
전 세계 IT 상담원의 첫마디, "껐다 켜 보셨어요?"에는 진짜 과학이 있어요.
|
||||
핵심 단어는 <strong>상태(state)</strong>입니다. 컴퓨터가 오래 켜져 있으면 램 위에
|
||||
프로그램들의 찌꺼기와 꼬임이 조금씩 쌓이는데, 재부팅은 이걸 한 방에 비워요.
|
||||
</p>
|
||||
<Code>{CODE_REBOOT}</Code>
|
||||
<p>
|
||||
칠판에 필기가 몇 겹씩 덧쓰여 알아볼 수 없게 됐을 때, 지우개로 부분 부분 지우는
|
||||
것보다 <strong>물걸레로 싹 밀고 새로 쓰는 게</strong> 빠른 것과 같아요.
|
||||
램은 전원이 꺼지면 저절로 비워지는 칠판이라, 재부팅이 곧 물걸레질입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>단, 재부팅은 진통제</b> 아픈 원인을 없애는 게 아니라 증상을 지우는 거예요.
|
||||
한 달에 한 번이면 그러려니 해도, <strong>매일 재부팅해야 버틴다면</strong> 원인이
|
||||
따로 있다는 신호 — 섹션 1~2로 돌아가 범인을 추적할 때입니다. 서버도 똑같아요.
|
||||
매일 재시작해야 하는 서비스는 어딘가 메모리가 새고 있다는 뜻이거든요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="문제를 검색하는 요령" sub="에러 메시지 그대로 + 따옴표 — 개발자의 검색법">
|
||||
<p>
|
||||
여기까지 왔는데도 모르겠다면? 괜찮아요. <strong>세상 어딘가에 같은 문제를 먼저
|
||||
겪은 사람이 거의 반드시 있습니다.</strong> 문제는 그 사람의 글을 찾아내는 검색
|
||||
실력이고, 요령은 딱 하나예요 — <strong>에러 메시지를 내 말로 번역하지 말 것</strong>.
|
||||
</p>
|
||||
<Code>{CODE_SEARCH}</Code>
|
||||
<p>
|
||||
왜 따옴표일까요? 따옴표 없이 검색하면 단어들이 뿔뿔이 흩어진 글도 다 걸리지만,
|
||||
<strong> "따옴표로 감싼 문장"은 그 문장이 통째로 들어간 글만</strong> 찾아 줍니다.
|
||||
같은 에러를 겪고 같은 메시지를 붙여넣은 사람의 글 — 즉 정답 후보만 남는 거죠.
|
||||
</p>
|
||||
<p>
|
||||
그리고 이건 그대로 <strong>개발자의 디버깅 습관</strong>이에요. React 콘솔의
|
||||
빨간 에러도, Spring Boot의 스택트레이스도, PostgreSQL의 에러 코드도
|
||||
전부 "그대로 복사 + 따옴표 + 상황 키워드"로 검색합니다. 신입과 고수의 차이는
|
||||
에러를 안 만나는 게 아니라, <strong>에러 메시지를 끝까지 읽고 그대로 검색하는
|
||||
습관</strong>에 있어요.
|
||||
</p>
|
||||
<Code>{CODE_DEBUG_MINDSET}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 이 페이지에서 <span className="kbd">F12</span>를 눌러
|
||||
개발자 도구의 <span className="kbd">Console</span> 탭을 열어 두고, 우리 플랫폼을
|
||||
이리저리 돌아다녀 보세요. 노란 경고나 빨간 에러가 보이면 한 줄을 복사해서
|
||||
따옴표로 감싸 검색해 보는 거예요. 남의 컴퓨터가 아닌 <strong>우리가 매일 쓰는
|
||||
플랫폼의 메시지</strong>로 하는 첫 디버깅 검색 — 이보다 좋은 연습이 없습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🔧 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 느려진 컴퓨터 앞에서 <strong>증상을 쪼개고 → 데이터(작업 관리자)로
|
||||
확인하고 → 단서(에러 코드)를 모아 → 그대로 검색</strong>하는 사람이 됐어요.
|
||||
그리고 이 순서는 개발자가 버그를 잡는 순서와 정확히 같습니다. 다음은{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스에서
|
||||
"인터넷이 안 될 때"를 진단하는 눈을 키우고, 배운 검색 요령은 이번 주{' '}
|
||||
<Link to="/assignments"><strong>수습 과제</strong></Link>의 에러를 만났을 때
|
||||
바로 써먹어 보세요 — 진통제(재부팅)보다 원인 추적이 먼저라는 것, 잊지 말고요!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
394
frontend/src/pages/courses/TypographyPage.jsx
Normal file
394
frontend/src/pages/courses/TypographyPage.jsx
Normal file
@ -0,0 +1,394 @@
|
||||
// 이 파일이 하는 일: "타이포그래피" 코스 — 세리프/산세리프 구분에서 시작해
|
||||
// 한글 폰트, 크기 위계, 행간·자간, 굵기, 폰트 조합, 라이선스, 실습까지
|
||||
// 8개 섹션으로 안내하는 정적 학습 페이지. (API 호출 없음)
|
||||
// 학습 포인트: 스타일은 global.css의 가이드 전용 클래스(step-card, code-block, tip 등)를
|
||||
// 재사용한다 — 색이나 폰트를 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램·표는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_SERIF_VS_SANS = `세리프(Serif) vs 산세리프(Sans-serif) — 획 끝의 '삐침' 유무
|
||||
|
||||
세리프 산세리프
|
||||
────────────────────── ──────────────────────
|
||||
획 끝에 장식(삐침)이 있음 획 끝이 깔끔하게 잘림
|
||||
붓글씨·활판 인쇄의 후예 금속 간판·표지판의 후예
|
||||
예) 명조 계열, Times 계열 예) 고딕 계열, Pretendard
|
||||
|
||||
언제 뭘 쓰나?
|
||||
세리프 : 긴 글(소설·논문·기사 본문), 격식·전통·신뢰의 분위기
|
||||
산세리프 : 화면(UI), 버튼·메뉴·대시보드, 작은 글씨, 모던한 분위기
|
||||
|
||||
→ 웹 서비스 UI는 거의 다 산세리프예요. 화면 픽셀에서는
|
||||
가는 삐침이 뭉개져서 오히려 지저분해 보이기 때문!`;
|
||||
|
||||
const CODE_HANGUL = `한글이 폰트 만들기 어려운 이유 — 글자 수부터 다릅니다.
|
||||
|
||||
알파벳: A~Z 대소문자 + 숫자 + 기호 ≈ 100자 남짓
|
||||
한글 : 초성×중성×종성 조합 = 완성형 11,172자!
|
||||
|
||||
그래서 생기는 일:
|
||||
- 잘 만든 무료 한글 폰트가 귀함 (만드는 비용이 어마어마)
|
||||
- 영문 폰트만 지정하면 한글은 브라우저 기본 폰트로 '대체'되어
|
||||
한글·영문의 굵기와 크기가 따로 노는 '짬뽕 화면'이 됨
|
||||
|
||||
Pretendard를 쓰는 이유:
|
||||
1) 11,172자 완성형을 모두 갖춘 고품질 무료 폰트 (SIL OFL 라이선스)
|
||||
2) Thin(100)~Black(900) 9단계 굵기 — 위계 표현이 자유로움
|
||||
3) 영문·숫자도 한글과 어울리게 조율되어 있어 섞어 써도 자연스러움
|
||||
→ 우리 학습 플랫폼의 본문 폰트도 이 계열이에요.`;
|
||||
|
||||
const CODE_SCALE = `크기 위계(Type Scale) — 크기 차이가 곧 '읽는 순서'
|
||||
|
||||
역할 크기 예시 쓰임
|
||||
─────────────────────────────────────────────
|
||||
페이지 제목 28~36px 화면에 딱 하나. "여긴 어디?"의 답
|
||||
섹션 제목 20~24px 내용의 큰 덩어리 구분
|
||||
본문 14~16px 실제로 읽는 글. 기준점!
|
||||
캡션·보조 12~13px 날짜, 힌트, 라벨 — 작지만 12px 밑으로는 금지
|
||||
|
||||
포인트: 크기를 '감'으로 정하지 말고 단계(스케일)를 정해 두고
|
||||
그 안에서만 고르세요. 21px, 17px, 15px... 이렇게 제멋대로면
|
||||
화면 전체가 미묘하게 어수선해집니다. 계단은 몇 개만, 간격은 일정하게.`;
|
||||
|
||||
const CODE_LINE_HEIGHT = `행간(line-height)·자간(letter-spacing) — 가독성의 8할
|
||||
|
||||
행간 = 줄과 줄 사이 간격 (책의 '줄 간격')
|
||||
─────────────────────────────────────────────
|
||||
너무 좁으면(1.2 이하) : 줄이 붙어서 답답, 다음 줄 찾다가 길을 잃음
|
||||
적당하면(1.5~1.7) : 본문 기준 가장 편안한 범위
|
||||
너무 넓으면(2.0 이상) : 문단이 흩어져서 한 덩어리로 안 읽힘
|
||||
|
||||
CSS에서는 단위 없는 배수를 추천:
|
||||
line-height: 1.6; /* 글자 크기의 1.6배 — 크기가 바뀌어도 비율 유지 */
|
||||
|
||||
자간 = 글자와 글자 사이 간격
|
||||
─────────────────────────────────────────────
|
||||
한글 본문 : 기본값 그대로 또는 살짝 좁게 (-0.01em ~ -0.02em)
|
||||
제목(큰 글씨) : 클수록 자간이 넓어 '보이므로' 살짝 좁히면 단단해 보임
|
||||
영문 대문자 라벨 : 반대로 살짝 벌리면(+0.05em) 읽기 좋아짐`;
|
||||
|
||||
const CODE_WEIGHT = `굵기(font-weight)로 강조하기 — 색보다 먼저 굵기
|
||||
|
||||
숫자 이름 쓰임
|
||||
─────────────────────────────────────────────
|
||||
400 Regular 본문 기본값
|
||||
500 Medium 살짝 강조 (메뉴, 라벨)
|
||||
600 SemiBold 소제목, 버튼 텍스트
|
||||
700 Bold 제목, 진짜 중요한 단어
|
||||
|
||||
강조의 우선순위:
|
||||
1순위 굵기 ↑ : 조용하지만 확실함. 어디서나 안전
|
||||
2순위 크기 ↑ : 위계 자체를 바꿀 때만
|
||||
3순위 색 변경 : 남용하면 '뭐가 중요한지 모르는 화면'이 됨
|
||||
|
||||
→ 형광펜을 교과서 전체에 칠하면 아무것도 강조 안 한 것과 같죠.
|
||||
Bold도 마찬가지 — 한 문단에 한두 곳이면 충분합니다.`;
|
||||
|
||||
const CODE_TWO_FONTS = `한 화면 폰트는 2종까지 — 조합의 기본 공식
|
||||
|
||||
안전한 조합 패턴:
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 본문용 1종 (예: Pretendard) │ ← 화면의 90%
|
||||
│ + 포인트용 1종 (예: 코드용 고정폭 폰트) │ ← 코드·숫자·로고
|
||||
└─────────────────────────────────────────┘
|
||||
|
||||
3종 이상이 되면:
|
||||
- 화면이 '전단지'처럼 산만해짐
|
||||
- 웹폰트 파일이 늘어나 로딩도 느려짐 (한글 폰트는 특히 무거움!)
|
||||
|
||||
우리 플랫폼도 딱 2종이에요:
|
||||
1) 본문·제목 전부 = 산세리프 1종 (굵기만 바꿔 위계 표현)
|
||||
2) code-block·icode = 고정폭(monospace) 1종 (코드는 글자 폭이
|
||||
같아야 들여쓰기가 안 깨지니까)`;
|
||||
|
||||
const CODE_LICENSE = `무료 폰트 ≠ 마음대로 — 라이선스 4가지 체크포인트
|
||||
|
||||
체크 질문
|
||||
─────────────────────────────────────────────
|
||||
상업적 이용 회사 서비스(돈 버는 곳)에 써도 되나?
|
||||
웹폰트 임베딩 파일을 서버에 올려 배포해도 되나?
|
||||
수정 글자를 고쳐서 재배포해도 되나?
|
||||
표기 의무 "이 폰트를 썼습니다" 고지가 필요한가?
|
||||
|
||||
안심 라이선스: SIL OFL — 상업 이용·웹 임베딩·수정 모두 허용.
|
||||
Pretendard가 이 라이선스예요.
|
||||
|
||||
함정 사례:
|
||||
- "개인 사용 무료" 폰트를 회사 서비스에 씀 → 라이선스 위반!
|
||||
- 유료 폰트가 깔린 PC에서 만든 이미지를 웹에 올림 → 계약 범위 확인 필요
|
||||
→ 회사 프로젝트에 폰트를 추가하기 전엔 반드시 라이선스 원문을 읽고,
|
||||
애매하면 멘토에게 물어보세요. "무료 다운로드 가능"과
|
||||
"무료 사용 가능"은 다른 말입니다.`;
|
||||
|
||||
const CODE_PRACTICE_HIERARCHY = `실습 1 — 우리 플랫폼의 타이포 위계 수사하기
|
||||
|
||||
지금 이 페이지에서 F12 → 요소 선택 도구(Ctrl+Shift+C)로
|
||||
아래 네 곳을 하나씩 클릭하고, Computed 탭에서 값을 받아 적으세요:
|
||||
|
||||
조사 대상 font-size font-weight line-height
|
||||
──────────────────────────────────────────────────────────
|
||||
① 페이지 큰 제목(h1) ___px ___ ___
|
||||
② 섹션 제목(h3) ___px ___ ___
|
||||
③ 이 본문 문단(p) ___px ___ ___
|
||||
④ chip(칩) 글씨 ___px ___ ___
|
||||
|
||||
다 적었으면 스스로 질문:
|
||||
- 크기 계단이 몇 단계인가? 간격은 일정한가?
|
||||
- 굵기는 어디서 바뀌는가? (제목만? 강조 단어도?)
|
||||
→ 답을 멘토에게 보여주고 "왜 이렇게 정했을까" 이야기해 보세요.`;
|
||||
|
||||
const CODE_PRACTICE_LINEHEIGHT = `실습 2 — 개발자도구로 행간 실험 (새로고침하면 원상복구!)
|
||||
|
||||
1) 이 문단 아무 곳이나 우클릭 → 검사
|
||||
2) Styles 패널에서 해당 요소에 직접 입력:
|
||||
line-height: 1.0; ← 줄이 붙어 답답해지는 걸 확인
|
||||
line-height: 2.5; ← 문단이 흩어지는 걸 확인
|
||||
line-height: 1.6; ← 다시 편안해지는 지점 찾기
|
||||
3) 보너스: letter-spacing: 0.3em; 을 줘 보세요.
|
||||
자간이 너무 벌어지면 단어 경계가 사라져 오히려 안 읽히죠?
|
||||
|
||||
핵심: '예쁘다'가 아니라 '오래 읽어도 눈이 안 아프다'가 기준.
|
||||
숫자를 바꿔 가며 그 경계를 직접 눈으로 찾아보는 게 이 실습의 목적입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '세리프 vs 산세리프' },
|
||||
{ n: 2, label: '한글 폰트와 Pretendard' },
|
||||
{ n: 3, label: '크기 위계' },
|
||||
{ n: 4, label: '행간과 자간' },
|
||||
{ n: 5, label: '굵기로 강조하기' },
|
||||
{ n: 6, label: '폰트는 2종까지' },
|
||||
{ n: 7, label: '무료 폰트와 라이선스' },
|
||||
{ n: 8, label: '실습 2종' },
|
||||
];
|
||||
|
||||
export default function TypographyPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 무엇을 다루는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>타이포그래피<br />— 글자가 곧 디자인의 90%</h1>
|
||||
<p>
|
||||
화면에서 우리가 보는 것의 대부분은 결국 <strong>글자</strong>예요. 폰트 고르기,
|
||||
크기 계단 만들기, 행간 조절 — 이 작은 결정들이 "왠지 읽기 편한 화면"과
|
||||
"왠지 어수선한 화면"을 가릅니다. 규칙은 몇 개 안 되니, 직접 만져 보며 배워요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 포함</span>
|
||||
<span className="chip">준비물: 브라우저 + F12</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="세리프 vs 산세리프" sub="획 끝의 삐침 하나가 분위기를 바꾼다">
|
||||
<p>
|
||||
폰트의 가장 큰 분류는 딱 두 갈래예요. 획 끝에 <strong>삐침(세리프)</strong>이
|
||||
있으면 세리프, 없으면 산세리프(sans = 프랑스어로 "없는")입니다.
|
||||
한글로 치면 <strong>명조 계열</strong>이 세리프, <strong>고딕 계열</strong>이
|
||||
산세리프라고 생각하면 거의 맞아요.
|
||||
</p>
|
||||
<p>
|
||||
비유하자면 세리프는 <strong>정장</strong>, 산세리프는 <strong>깔끔한 캐주얼</strong>이에요.
|
||||
종이에 인쇄된 긴 글(책·신문)에서는 세리프의 삐침이 시선을 다음 글자로 자연스럽게
|
||||
이어 줘서 오래 읽기 편하고, 픽셀로 그리는 화면(UI)에서는 삐침이 뭉개지기 쉬워서
|
||||
산세리프가 훨씬 또렷합니다.
|
||||
</p>
|
||||
<Code>{CODE_SERIF_VS_SANS}</Code>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 자주 쓰는 앱 3개(우리 학습 플랫폼 포함)를 열고
|
||||
본문 폰트가 세리프인지 산세리프인지 판정해 보세요. UI는 거의 전부
|
||||
산세리프라는 걸 확인하고 — 반대로 뉴스 기사나 전자책 본문에서
|
||||
세리프를 찾아보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="한글 폰트의 특수성" sub="11,172자의 무게 — 그리고 Pretendard">
|
||||
<p>
|
||||
영문 폰트는 100자 남짓 그리면 끝이지만, 한글은 초성·중성·종성 조합으로
|
||||
<strong> 완성형 11,172자</strong>를 전부 그려야 해요. 알파벳 폰트가 단편 소설이라면
|
||||
한글 폰트는 대하소설인 셈이죠. 그래서 품질 좋은 한글 폰트는 귀하고,
|
||||
아무 영문 폰트나 지정하면 한글 부분만 브라우저 기본 폰트로 대체되어
|
||||
<strong> 한글·영문이 따로 노는 화면</strong>이 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_HANGUL}</Code>
|
||||
<p>
|
||||
그래서 실무에서 <strong>Pretendard</strong> 같은 폰트가 사랑받아요. 전체 완성형을
|
||||
갖췄고, 굵기가 9단계라 위계 표현이 자유롭고, 영문·숫자까지 한글과 어울리게
|
||||
조율돼 있고, 라이선스(SIL OFL)도 안전하거든요. "한 폰트로 화면 전체를
|
||||
해결할 수 있다"는 게 핵심입니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>흔한 실수</b> CSS에 <span className="icode">font-family: 'Cool Font';</span>처럼
|
||||
영문 전용 폰트 하나만 달랑 지정하기. 한글이 전부 대체 폰트로 빠져서 굵기·높이가
|
||||
어긋납니다. 항상 한글을 지원하는 폰트를 앞에, 뒤에는{' '}
|
||||
<span className="icode">sans-serif</span> 같은 최후의 보루를 함께 적어 주세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="크기 위계" sub="제목–본문–캡션, 계단을 먼저 정한다">
|
||||
<p>
|
||||
잘 만든 화면은 소리 내지 않고도 <strong>"이 순서로 읽으세요"</strong>라고 말해요.
|
||||
그 역할을 하는 게 크기 위계입니다. 신문을 떠올려 보세요 — 헤드라인, 소제목,
|
||||
본문, 사진 캡션이 크기만 봐도 구분되죠. 우리 화면도 똑같은 계단이 필요합니다.
|
||||
</p>
|
||||
<Code>{CODE_SCALE}</Code>
|
||||
<p>
|
||||
핵심은 <strong>본문 크기를 기준점</strong>으로 잡고 위아래 계단을 몇 개만 정해 두는
|
||||
것. 매번 "이건 17px? 18px?" 고민하는 대신, 정해 둔 계단에서 고르기만 하면
|
||||
화면 전체가 저절로 통일됩니다. 이게 바로 디자인 시스템의 출발점이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>기억할 숫자 하나</b> 본문은 <strong>14~16px</strong>, 그리고 어떤 글씨도{' '}
|
||||
<strong>12px 아래로 내려가지 않기</strong>. 여러분 눈은 괜찮아도, 모든 사용자의
|
||||
눈이 괜찮은 건 아니거든요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="행간과 자간" sub="가독성의 8할은 글자 '사이'의 공간">
|
||||
<p>
|
||||
같은 폰트, 같은 크기라도 <strong>행간</strong>(줄 간격) 하나로 "술술 읽히는 글"과
|
||||
"숨 막히는 글"이 갈려요. 글자가 배우라면 행간·자간은 무대 위의 간격 —
|
||||
배우들이 다닥다닥 붙어 있으면 누가 누군지 안 보이는 것과 같습니다.
|
||||
</p>
|
||||
<Code>{CODE_LINE_HEIGHT}</Code>
|
||||
<p>
|
||||
특히 한글은 글자 하나하나가 네모 안에 꽉 차는 구조라, 영문보다 행간을
|
||||
<strong> 조금 더 넉넉하게</strong> 주는 게 보통이에요. 본문 기준{' '}
|
||||
<span className="icode">line-height: 1.6</span> 근처에서 시작해 눈으로
|
||||
미세 조정하면 됩니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 아래 섹션 8의 실습 2가 바로 이 내용이에요. 지금 이 문단의
|
||||
행간을 개발자도구로 직접 바꿔 보며 "답답함 ↔ 편안함 ↔ 산만함"의 경계를 눈으로
|
||||
찾아보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="굵기(weight)로 강조하기" sub="색보다 먼저, 크기보다 조용하게">
|
||||
<p>
|
||||
문장에서 뭔가를 강조하고 싶을 때 초보의 손은 색깔로 갑니다. 빨갛게, 파랗게,
|
||||
형광으로... 그런데 프로의 첫 번째 도구는 <strong>굵기</strong>예요. 굵기는
|
||||
화면의 색 조화를 깨지 않으면서도 확실하게 눈에 띄고, 흑백으로 인쇄해도,
|
||||
색약인 사용자에게도 그대로 전달되거든요.
|
||||
</p>
|
||||
<Code>{CODE_WEIGHT}</Code>
|
||||
<p>
|
||||
지금 이 페이지도 그래요 — 본문은 Regular, 강조 단어만{' '}
|
||||
<strong>Bold</strong>, 제목은 크기+굵기를 함께. 색으로 강조한 곳은 링크와
|
||||
경고 박스 정도뿐이죠. <strong>굵기 → 크기 → 색</strong> 순서만 기억해도
|
||||
화면이 한 단계 차분해집니다.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="한 화면 폰트는 2종까지" sub="많을수록 산만하고, 무겁다">
|
||||
<p>
|
||||
폰트를 고르다 보면 욕심이 나요. 제목엔 이거, 본문엔 저거, 버튼엔 또 다른 거...
|
||||
그 결과물은 대개 <strong>동네 전단지</strong>가 됩니다. 옷도 상의·하의·신발
|
||||
브랜드가 전부 다르고 무늬까지 요란하면 어수선하듯, 폰트도 마찬가지예요.
|
||||
</p>
|
||||
<Code>{CODE_TWO_FONTS}</Code>
|
||||
<p>
|
||||
"2종인데 화면이 심심하지 않냐"고요? 섹션 3~5에서 배운 <strong>크기·행간·굵기</strong>가
|
||||
그 다양함을 다 만들어 줍니다. 한 폰트의 굵기 9단계만으로도 표현할 수 있는 위계는
|
||||
충분히 많아요. 폰트 종류가 아니라 <strong>규칙</strong>이 화면을 풍부하게 합니다.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>성능도 걸려 있어요</b> 한글 웹폰트 한 벌은 영문 폰트의 수십 배 용량입니다.
|
||||
폰트를 하나 추가할 때마다 사용자의 첫 화면이 그만큼 늦게 떠요. 예쁨과 속도를
|
||||
맞바꾸는 결정이라는 걸 기억하세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="무료 폰트와 라이선스" sub="'무료 다운로드'와 '무료 사용'은 다르다">
|
||||
<p>
|
||||
폰트는 <strong>저작물</strong>이에요. 누군가 수천 시간을 들여 11,172자를 그린
|
||||
결과물이죠. "인터넷에서 무료로 받았으니 우리 서비스에 써도 되겠지"는
|
||||
위험한 착각입니다 — <strong>개인 사용만 무료</strong>인 폰트를 회사 서비스에
|
||||
쓰면 라이선스 위반이고, 실제로 기업에 청구서가 날아오는 일이 드물지 않아요.
|
||||
</p>
|
||||
<Code>{CODE_LICENSE}</Code>
|
||||
<p>
|
||||
다행히 <strong>SIL OFL</strong> 같은 오픈 라이선스 폰트(Pretendard 포함)는
|
||||
상업 서비스·웹 임베딩까지 자유롭습니다. 회사 프로젝트 기준은 간단해요 —{' '}
|
||||
<strong>라이선스 원문을 확인하지 않은 폰트는 쓰지 않는다.</strong> AWESOMEDEV
|
||||
프로젝트에 폰트를 추가하고 싶으면 라이선스 근거를 함께 가져와서 멘토와
|
||||
상의하세요.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="실습 — 눈으로 배운 걸 손으로" sub="우리 플랫폼이 곧 교재">
|
||||
<p>
|
||||
이제 배운 걸 전부 동원해 <strong>지금 보고 있는 이 플랫폼</strong>을 뜯어볼
|
||||
차례예요. 준비물은 <span className="kbd">F12</span> 하나. 개발자도구에서 바꾼
|
||||
값은 새로고침하면 사라지니 마음껏 실험해도 됩니다.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>
|
||||
<strong>실습 1 · 타이포 위계 수사</strong> — 이 페이지의 제목·본문·칩 글씨를
|
||||
요소 선택 도구로 찍어서 실제 값(크기·굵기·행간)을 표로 채워 보세요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>실습 2 · 행간 실험</strong> — 본문 문단의{' '}
|
||||
<span className="icode">line-height</span>를 직접 바꿔 가며 편안한 범위를
|
||||
눈으로 찾아보세요.
|
||||
</li>
|
||||
</ol>
|
||||
<Code>{CODE_PRACTICE_HIERARCHY}</Code>
|
||||
<Code>{CODE_PRACTICE_LINEHEIGHT}</Code>
|
||||
<div className="tip">
|
||||
<b>제출 팁</b> 실습 1에서 채운 표를 사진이나 메모로 남겨 두세요. 나중에 여러분이
|
||||
직접 화면을 만들 때, 이 표가 그대로 <strong>우리 팀의 타이포 규칙 치트시트</strong>가
|
||||
됩니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>✍️ 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 여러분은 화면을 볼 때 "예쁘다/안 예쁘다" 대신{' '}
|
||||
<strong>"위계가 있다, 행간이 좁다, 폰트가 3종이다"</strong>라고 말할 수 있어요.
|
||||
글자를 다뤘으니 다음은 글자를 담는 <strong>공간</strong> — 여백과 정렬, 색을
|
||||
다루는 디자인 코스로 이어 가거나, <Link to="/learn/coding"><strong>코딩 기초</strong></Link>{' '}
|
||||
코스에서 오늘 본 CSS 속성들을 직접 코드로 만져 보세요. 그리고 다음 과제 화면을
|
||||
만들 때, 실습 1에서 만든 치트시트를 꼭 옆에 두고 시작하기!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
392
frontend/src/pages/courses/UserResearchPage.jsx
Normal file
392
frontend/src/pages/courses/UserResearchPage.jsx
Normal file
@ -0,0 +1,392 @@
|
||||
// 이 파일이 하는 일: "사용자 리서치 기초" 코스 — "내 감"이 아니라 진짜 사용자에게
|
||||
// 묻고·지켜보는 방법을 7개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (감 vs 사용자 → 인터뷰 기본기 → 관찰의 힘 → 페르소나 → 사용성 테스트 → 설문의 함정 → 실전 실습)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 인터뷰 대본·체크리스트 같은 텍스트 자료는 JSX 중괄호/백틱 충돌을 피하려고
|
||||
// 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 자료 상수들 ──
|
||||
|
||||
const CODE_GUT_VS_USER = `"내 감"과 "사용자의 현실"이 어긋난 (아주 흔한) 예
|
||||
|
||||
만든 사람의 생각 실제 사용자
|
||||
────────────────────────── ──────────────────────────
|
||||
"버튼이 딱 보이는데?" 스크롤을 안 내려서 버튼을 못 봄
|
||||
"이 기능 다들 좋아할걸?" 존재 자체를 모름 (메뉴 못 찾음)
|
||||
"용어가 직관적이잖아" '커밋'이 뭔지 몰라서 멈춤
|
||||
"3단계면 금방 하지" 2단계에서 뒤로가기 누르고 이탈
|
||||
|
||||
→ 만든 사람은 이미 답을 알고 화면을 봐요.
|
||||
처음 보는 사람의 눈은 "직접 물어보고 지켜봐야만" 얻을 수 있습니다.`;
|
||||
|
||||
const CODE_INTERVIEW_DO_DONT = `유도 질문 vs 열린 질문 — 한 끗 차이가 답을 바꿔요
|
||||
|
||||
나쁜 질문 (유도 질문) 좋은 질문 (열린 질문)
|
||||
────────────────────────── ──────────────────────────
|
||||
"이 기능 편리하죠?" "이 기능을 언제 마지막으로 썼어요?"
|
||||
"검색이 빨라서 좋지 않아요?" "원하는 걸 찾을 때 어떻게 하세요?"
|
||||
"이 디자인 예쁘죠?" "이 화면에서 처음 눈이 간 곳은요?"
|
||||
"이런 기능 있으면 쓸 거죠?" "지금은 그 문제를 어떻게 해결해요?"
|
||||
|
||||
규칙 1: "네/아니오"로 끝나는 질문을 피하기 → 이야기가 안 나와요
|
||||
규칙 2: 미래("쓸 건가요?")보다 과거("써 봤나요?")를 묻기
|
||||
→ 사람은 미래의 자신을 과대평가합니다
|
||||
규칙 3: 내가 듣고 싶은 답을 질문에 심지 않기 ("편리하죠?" 금지)`;
|
||||
|
||||
const CODE_FIVE_WHYS = `"왜?"를 다섯 번 — 표면의 불만에서 진짜 원인까지 파고들기
|
||||
|
||||
수습생: "과제 제출 페이지가 불편해요."
|
||||
왜? → "제출했는지 안 했는지 헷갈려서요."
|
||||
왜? → "제출 버튼을 눌러도 화면이 그대로거든요."
|
||||
왜 헷갈리죠? → "성공 메시지가 안 보여요."
|
||||
왜 안 보일까요? → "메시지가 화면 맨 위에 뜨는데, 전 맨 아래를 보고 있어서요."
|
||||
왜 맨 아래를 보고 있었어요? → "제출 버튼이 맨 아래에 있으니까요!"
|
||||
|
||||
표면: "페이지가 불편해요" (여기서 멈추면 뭘 고쳐야 할지 모름)
|
||||
진짜 원인: 성공 메시지가 사용자의 시선과 다른 곳에 뜸
|
||||
해결: 버튼 바로 옆에 성공 표시 → 명확한 수리 포인트!
|
||||
|
||||
주의: 취조하듯 "왜? 왜? 왜?"라고 다그치면 안 돼요.
|
||||
"조금 더 자세히 말해 줄래요?" "그때 어떤 기분이었어요?"처럼
|
||||
표현을 바꿔 가며 부드럽게 파고드는 게 기술입니다.`;
|
||||
|
||||
const CODE_SAY_VS_DO = `말과 행동은 다르다 — 같은 사람, 다른 데이터
|
||||
|
||||
인터뷰에서 한 말 옆에서 지켜본 행동
|
||||
────────────────────────── ──────────────────────────
|
||||
"검색 기능 잘 써요" 검색창 대신 메뉴만 5번 클릭
|
||||
"매뉴얼 읽고 시작해요" 매뉴얼 안 열고 바로 버튼부터 누름
|
||||
"에러 메시지 꼼꼼히 읽어요" 에러 창이 뜨자마자 0.5초 만에 닫음
|
||||
"단축키 애용해요" 마우스로만 조작 (Ctrl+S도 안 씀)
|
||||
|
||||
거짓말이 아니에요 — 사람은 자기가 '그랬으면 하는 모습'으로
|
||||
자신을 기억해요. 그래서 리서치는 두 발로 서야 합니다:
|
||||
한 발 = 묻기(인터뷰), 다른 발 = 지켜보기(관찰).`;
|
||||
|
||||
const CODE_PERSONA = `페르소나 예시 — 우리 학습 플랫폼의 대표 사용자
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 김미림 (18) — AWESOMEDEV 신입 수습생 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 배경 미림마이스터고 3학년. HTML/CSS는 해봤지만 │
|
||||
│ Git·서버·배포는 이번이 처음. │
|
||||
│ 목표 과제를 기한 안에 제출하고, 코드 리뷰에서 │
|
||||
│ "잘했다"는 말을 듣고 싶다. │
|
||||
│ 불안 "질문하면 바보 같아 보일까 봐" 검색으로 │
|
||||
│ 30분을 헤맨 뒤에야 멘토에게 물어본다. │
|
||||
│ 환경 학교 PC(Windows) + 집 노트북, 폰으로도 │
|
||||
│ 학습 페이지를 자주 열어 본다. │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ → 이 카드가 있으면 회의에서 이렇게 말할 수 있어요: │
|
||||
│ "그 용어, 미림이가 알아들을까?" │
|
||||
│ "미림이는 폰으로도 보는데 이 표가 잘리진 않을까?"│
|
||||
└─────────────────────────────────────────────┘
|
||||
|
||||
페르소나 = 리서치에서 만난 여러 실제 사용자의 공통점을
|
||||
한 명의 '대표 인물'로 뭉친 카드. 상상으로 지어내면 소설이 되고,
|
||||
리서치 데이터로 만들면 나침반이 됩니다.`;
|
||||
|
||||
const CODE_USABILITY_MINI = `5분 미니 사용성 테스트 — 진행 순서
|
||||
|
||||
준비 (1분)
|
||||
· 과제 1개를 정한다. 예) "학습 센터에서 네트워크 코스를 찾아
|
||||
섹션 5(DNS)로 이동해 보세요."
|
||||
· 관찰 기록용 메모장을 연다.
|
||||
|
||||
진행 (3분)
|
||||
1. 과제를 '목표'로만 말한다 — 방법을 알려주면 반칙!
|
||||
○ "네트워크 코스의 DNS 섹션을 찾아가 보세요"
|
||||
✕ "위 메뉴에서 학습 센터 누르고, 목록에서..."
|
||||
2. "생각을 소리 내어 말해 주세요"라고 부탁한다.
|
||||
("음... 메뉴가 어디지... 이건가?" ← 이게 전부 데이터!)
|
||||
3. 입을 꾹 다물고 지켜본다. 헤매도 3분간은 돕지 않는다.
|
||||
(도와주고 싶어 손이 근질거리는 그 순간이 바로 발견의 순간)
|
||||
|
||||
정리 (1분)
|
||||
· 어디서 멈칫했나? 어디서 잘못 눌렀나? 뭐라고 중얼거렸나?
|
||||
· "감사합니다!" — 테스트당한 건 사람이 아니라 화면이라는 걸
|
||||
꼭 말해 주세요. 참가자는 언제나 옳습니다.`;
|
||||
|
||||
const CODE_SURVEY_TRAPS = `설문의 함정 3종 세트
|
||||
|
||||
함정 1 — 유도하는 문항
|
||||
✕ "새로워진 편리한 검색 기능에 만족하시나요?"
|
||||
('편리한'이라는 답을 문제에 심어 놨어요)
|
||||
○ "검색 기능 사용 경험은 어땠나요? (매우 불만족~매우 만족)"
|
||||
|
||||
함정 2 — 응답자 쏠림 (표본 편향)
|
||||
플랫폼을 좋아하는 사람일수록 설문에 잘 응해요.
|
||||
"만족도 90%!"가 사실은 "설문에 답해 준 팬들의 만족도 90%"일 수 있죠.
|
||||
→ 응답 안 한 사람들은 어떤 사람일지 항상 의심하기.
|
||||
|
||||
함정 3 — 숫자만 있고 이유가 없음
|
||||
"만족도 2점"이 나와도 설문만으론 '왜'를 몰라요.
|
||||
→ 마지막에 주관식 한 칸: "가장 불편했던 순간을 적어 주세요."
|
||||
점수(무엇)는 설문이, 이유(왜)는 인터뷰가 잘합니다.`;
|
||||
|
||||
const CODE_FIELD_NOTE = `실습 기록 템플릿 — 그대로 베껴서 채우면 됩니다
|
||||
|
||||
■ 5분 사용성 테스트 기록
|
||||
날짜/참가자: 7/16, 동기 수습생 ○○
|
||||
과제: "학습 센터에서 ___ 코스를 찾아 ___ 해 보세요"
|
||||
|
||||
발견 1
|
||||
본 것: 메인에서 학습 센터 메뉴를 찾는 데 40초 걸림 (푸터까지 스크롤)
|
||||
들은 것: "메뉴 이름이 '학습 센터'인지 '코스'인지 헷갈려요"
|
||||
내 해석: 메뉴 라벨이 기대와 다를 수 있음 → 라벨 검토 제안
|
||||
|
||||
발견 2
|
||||
본 것: 코스 페이지에서 pill-nav를 안 누르고 계속 스크롤만 함
|
||||
들은 것: (아무 말 없이 지나침)
|
||||
내 해석: 바로가기 내비게이션의 존재를 인지 못함
|
||||
|
||||
발견 3
|
||||
본 것 / 들은 것 / 내 해석: ...
|
||||
|
||||
규칙: '본 것·들은 것'(사실)과 '내 해석'(의견)을 반드시 분리!
|
||||
사실은 나중에 봐도 사실이지만, 해석은 틀릴 수 있으니까요.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '감이 아니라 사용자' },
|
||||
{ n: 2, label: '인터뷰 기본기' },
|
||||
{ n: 3, label: '관찰의 힘' },
|
||||
{ n: 4, label: '페르소나 맛보기' },
|
||||
{ n: 5, label: '사용성 테스트' },
|
||||
{ n: 6, label: '설문의 함정' },
|
||||
{ n: 7, label: '실전 실습' },
|
||||
];
|
||||
|
||||
export default function UserResearchPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 무엇을 바꿔 주는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 디자인</div>
|
||||
<h1>사용자 리서치 기초<br />— "내 감"을 내려놓고 사용자에게 묻기</h1>
|
||||
<p>
|
||||
좋은 제품은 만든 사람의 감이 아니라 <strong>사용자의 입과 손</strong>에서
|
||||
나옵니다. 이 코스에서는 인터뷰로 묻고, 관찰로 확인하고, 5분짜리 사용성
|
||||
테스트까지 직접 해 봐요 — 실습 상대는 바로 옆자리 동료 수습생입니다.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 50분</span>
|
||||
<span className="chip">실습 2개 + 미니 테스트</span>
|
||||
<span className="chip">준비물: 동료 수습생 1명</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title={'"내 감"이 아니라 사용자에게 묻기'} sub="만든 사람의 눈은 이미 오염(?)되어 있다">
|
||||
<p>
|
||||
내가 만든 화면은 나에게 <strong>절대 낯설 수 없어요</strong>. 버튼이 어디 있는지,
|
||||
다음에 뭘 눌러야 하는지 전부 알고 보니까요. 자기 방 불을 끄고도 침대까지
|
||||
걸어갈 수 있는 것과 같죠 — 하지만 처음 온 손님은 문턱에 발가락을 찧습니다.
|
||||
사용자는 언제나 <strong>불 꺼진 방에 처음 들어온 손님</strong>이에요.
|
||||
</p>
|
||||
<Code>{CODE_GUT_VS_USER}</Code>
|
||||
<p>
|
||||
그래서 리서치의 첫 마음가짐은 이거예요: <strong>"나는 사용자가 아니다."</strong>
|
||||
우리 회사에서도 마찬가지 — 이 학습 플랫폼을 만드는 사람(멘토·개발자)과
|
||||
쓰는 사람(수습생)은 아는 것도, 익숙한 용어도 달라요. 그 틈을 메우는 유일한
|
||||
방법이 <strong>직접 묻고 지켜보는 것</strong>, 즉 사용자 리서치입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>기억할 한 문장</b> 회의에서 "사용자들이 이걸 좋아할 거예요"라는 말이 나오면
|
||||
속으로 물어보세요 — <strong>"그건 물어본 결과인가요, 내 감인가요?"</strong>
|
||||
이 질문 하나가 리서처의 시작입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="인터뷰 기본기" sub="유도 질문 금지, 그리고 '왜?'를 다섯 번">
|
||||
<p>
|
||||
인터뷰는 그냥 수다가 아니라 <strong>기술</strong>이에요. 제일 흔한 실수는
|
||||
내가 듣고 싶은 답을 질문에 심어 버리는 <strong>유도 질문</strong>입니다.
|
||||
"이거 편리하죠?"라고 물으면 대부분 "네..."라고 해요 — 면전에서 "별로예요"라고
|
||||
말하기는 어려우니까요. 이건 데이터가 아니라 <strong>예의</strong>를 수집한 겁니다.
|
||||
</p>
|
||||
<Code>{CODE_INTERVIEW_DO_DONT}</Code>
|
||||
<p>
|
||||
두 번째 기술은 <strong>"왜?"를 다섯 번</strong> 묻기예요. 사용자의 첫 대답은
|
||||
보통 표면의 불만("불편해요")이고, 진짜 원인은 몇 겹 아래에 숨어 있거든요.
|
||||
양파 껍질을 벗기듯 한 겹씩 파고들어야 <strong>고칠 수 있는 지점</strong>이 나옵니다.
|
||||
</p>
|
||||
<Code>{CODE_FIVE_WHYS}</Code>
|
||||
<div className="warn">
|
||||
<b>주의 — 인터뷰는 취조가 아니에요</b> "왜요? 왜요? 왜요?"를 기계처럼 반복하면
|
||||
상대는 심문받는 기분이 듭니다. "조금 더 얘기해 줄래요?", "그때 어땠어요?"처럼
|
||||
표현을 바꾸고, 상대의 침묵도 3초쯤 기다려 주세요 — 좋은 답은 침묵 뒤에 나와요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ①</b> 옆자리 동료 수습생에게 5분만 빌려서 미니 인터뷰를
|
||||
해 보세요. 주제: <strong>"우리 학습 플랫폼에서 과제를 제출할 때의 경험"</strong>.
|
||||
규칙은 두 가지 — (1) "네/아니오"로 답할 수 있는 질문 금지,
|
||||
(2) 첫 대답에서 멈추지 말고 "왜?"를 최소 3번 파고들기.
|
||||
끝나면 <strong>몰랐던 사실 1개</strong>를 메모해 두세요. 반드시 하나는 나옵니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="관찰의 힘" sub="말과 행동은 다르다 — 그래서 지켜봐야 한다">
|
||||
<p>
|
||||
인터뷰만으로 충분하지 않은 이유가 있어요. 사람은 거짓말을 하려는 게 아니라,
|
||||
<strong> 자기 행동을 정확히 기억하지 못해요</strong>. "운동 자주 하세요?"라고
|
||||
물으면 "일주일에 세 번쯤요"라고 답하지만, 실제 기록을 보면 한 번인 경우가
|
||||
많은 것처럼요.
|
||||
</p>
|
||||
<Code>{CODE_SAY_VS_DO}</Code>
|
||||
<p>
|
||||
그래서 리서치의 황금 조합은 <strong>인터뷰(말) + 관찰(행동)</strong>입니다.
|
||||
말은 사용자의 <strong>생각과 기대</strong>를, 행동은 <strong>현실</strong>을
|
||||
보여줘요. 둘이 어긋나는 지점이 발견되면 — 축하해요, 거기가 바로
|
||||
제품이 고쳐져야 할 가장 값진 자리입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>관찰 연습</b> 오늘 동료가 플랫폼이나 Gitea(edu.awesomedevapp.com)를 쓰는
|
||||
모습을 어깨너머로 1분만 지켜보세요(허락은 받고요!). 마우스가 헤매는 곳,
|
||||
한숨이 나오는 순간, 같은 버튼을 두 번 누르는 장면 — 본인은 불편하다고
|
||||
말한 적 없는 것들이 보이기 시작할 거예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="페르소나 맛보기" sub="우리 플랫폼의 페르소나 = 바로 여러분, 수습생!">
|
||||
<p>
|
||||
리서치를 하다 보면 사용자 이야기가 수십 개 쌓여요. 이걸 회의 때마다
|
||||
"그... 어떤 분이 그러셨는데..."라고 꺼내긴 어렵죠. 그래서 여러 실제 사용자의
|
||||
공통점을 <strong>한 명의 대표 인물 카드</strong>로 뭉칩니다 — 그게
|
||||
<strong> 페르소나</strong>예요. 반 전체 급식 취향을 조사해서
|
||||
"우리 반 대표 입맛" 한 장으로 정리하는 것과 같아요.
|
||||
</p>
|
||||
<Code>{CODE_PERSONA}</Code>
|
||||
<p>
|
||||
재미있는 사실: <strong>이 학습 플랫폼의 페르소나가 바로 여러분</strong>이에요.
|
||||
멘토들이 코스를 만들 때 "수습생이 이 용어를 알까?", "폰으로 봐도 표가 안 깨질까?"를
|
||||
기준으로 결정해요. 여러분이 지금 느끼는 편함과 불편함이 곧 이 플랫폼의
|
||||
리서치 데이터인 셈이죠.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>페르소나의 함정</b> 리서치 없이 상상으로 만든 페르소나는 소설 속 인물일
|
||||
뿐이에요. "20대 여성, 커피를 좋아함" 같은 카드는 아무 결정도 도와주지 못합니다.
|
||||
<strong> 목표와 불안</strong>이 담겨야 진짜 페르소나예요 — 위 카드에서
|
||||
"질문하면 바보 같아 보일까 봐" 같은 줄이 결정을 바꾸는 부분입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="사용성 테스트 미니 방법" sub="과제를 주고, 입을 다물고, 지켜보기">
|
||||
<p>
|
||||
사용성 테스트는 거창한 실험실이 필요 없어요. 핵심은 딱 세 가지 —
|
||||
<strong> 과제를 주고</strong>, <strong>입을 다물고</strong>,
|
||||
<strong> 지켜보는 것</strong>. 참가자 한 명, 5분이면 충분히 시작할 수 있습니다.
|
||||
운전면허 기능 시험과 비슷해요: "주차해 보세요"라고 과제만 주고 옆에서
|
||||
지켜보지, "핸들을 왼쪽으로 꺾으세요"라고 알려주지 않죠.
|
||||
</p>
|
||||
<Code>{CODE_USABILITY_MINI}</Code>
|
||||
<p>
|
||||
제일 어려운 건 <strong>침묵</strong>이에요. 참가자가 헤매면 도와주고 싶어서
|
||||
입이 근질근질하거든요. 하지만 도와주는 순간 그 테스트는 끝 — 실제 사용자
|
||||
옆에는 도와줄 사람이 없으니까요. 참가자가 막히는 그 순간이 버그 리포트보다
|
||||
귀한 <strong>발견</strong>입니다.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>마법의 주문</b> 참가자가 "이거 맞아요?"라고 물으면 이렇게 답하세요 —
|
||||
<strong> "평소라면 어떻게 하실 것 같아요?"</strong> 답을 알려주지 않으면서도
|
||||
상대를 존중하는, 사용성 테스트 진행자의 필살기입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="설문의 함정" sub="숫자는 쉽게 모이지만, 쉽게 거짓말한다">
|
||||
<p>
|
||||
설문은 매력적이에요 — 한 번에 수십 명의 답을 숫자로 받을 수 있으니까요.
|
||||
하지만 <strong>잘못 만든 설문은 잘못된 확신</strong>을 줍니다. 틀린 지도를
|
||||
들고 자신 있게 걷는 것보다는 지도가 없는 편이 나아요.
|
||||
</p>
|
||||
<Code>{CODE_SURVEY_TRAPS}</Code>
|
||||
<p>
|
||||
정리하면 — <strong>설문은 "무엇(what)"과 "얼마나(how many)"</strong>를 재는
|
||||
도구이고, <strong>"왜(why)"는 인터뷰와 관찰</strong>이 잘합니다. 설문 결과에서
|
||||
이상한 숫자를 발견하면, 그게 인터뷰 주제가 되는 거예요. 도구마다 잘하는 일이
|
||||
다릅니다 — 드라이버로 못을 박으려 하지 마세요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기 ②</b> 설문 문항 고치기 연습. 아래 문항의 문제를 찾아
|
||||
열린 문항으로 고쳐 보세요 — <span className="icode">"새로 열린 알찬 학습 센터
|
||||
코스들이 도움이 되었나요? (네/아니오)"</span>. 힌트: 유도하는 형용사가 2개,
|
||||
네/아니오 강요가 1개 숨어 있어요. 고친 문항을 멘토에게 보여주고 피드백을 받아 보세요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="실전 실습 — 5분 사용성 테스트" sub="동료 수습생 1명 + 발견 3개 기록하기">
|
||||
<p>
|
||||
이제 배운 걸 전부 합칠 시간이에요. 오늘 안에 <strong>동료 수습생 한 명</strong>을
|
||||
섭외해서, 우리 학습 플랫폼으로 <strong>5분 사용성 테스트</strong>를 진행하고
|
||||
<strong> 발견 3개</strong>를 기록하세요. 섹션 5의 진행 순서를 그대로 따라 하면 됩니다.
|
||||
</p>
|
||||
<ol className="olist">
|
||||
<li>과제 1개 정하기 — 상대가 아직 안 해본 일이면 더 좋아요. 예) "학습 센터에서
|
||||
네트워크 코스를 찾아 DNS 섹션으로 이동하기"</li>
|
||||
<li>과제를 <strong>목표로만</strong> 전달하고, "생각을 소리 내어 말해 달라"고 부탁하기</li>
|
||||
<li>3분간 입 다물고 관찰 — 멈칫한 곳, 잘못 누른 곳, 중얼거림을 메모</li>
|
||||
<li>끝나면 "왜?"를 2~3번 물어 진짜 원인 파기 (섹션 2의 기술!)</li>
|
||||
<li>아래 템플릿으로 발견 3개를 정리해 멘토에게 공유하기</li>
|
||||
</ol>
|
||||
<Code>{CODE_FIELD_NOTE}</Code>
|
||||
<div className="warn">
|
||||
<b>꼭 지키기</b> 기록에서 <strong>사실(본 것·들은 것)</strong>과
|
||||
<strong> 해석(내 생각)</strong>을 반드시 분리하세요. "메뉴를 40초간 못 찾음"은
|
||||
사실이고, "메뉴 이름이 나쁘다"는 해석이에요. 사실은 시간이 지나도 사실이지만,
|
||||
해석은 틀릴 수 있습니다 — 섞어 쓰면 나중에 뭐가 진짜였는지 알 수 없어요.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>보너스</b> 여러분의 발견 3개는 진짜로 이 플랫폼을 좋게 만드는 데 쓰일 수
|
||||
있어요. 정리한 기록을 멘토에게 전달해 보세요 — 수습생의 리서치가 실제 개선으로
|
||||
이어지는 경험, 그게 이 실습의 진짜 목표입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🔍 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
이제 "내 감"이 아니라 <strong>물어보고(인터뷰), 지켜보고(관찰),
|
||||
시켜보는(사용성 테스트)</strong> 방법을 알게 됐어요. 사용자를 이해했으니
|
||||
다음은 그 이해를 화면으로 옮길 차례 — 리서치에서 나온 발견을 실제 코드로
|
||||
고쳐 보고 싶다면 <Link to="/learn/coding"><strong>코딩 기초</strong></Link> 코스로,
|
||||
발견을 팀에 전달하는 연습은 섹션 7의 실습 기록을 멘토와 리뷰하는 것부터
|
||||
시작해 보세요. 발견 3개 기록, 잊지 않았죠?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
450
frontend/src/pages/courses/WifiSecurityPage.jsx
Normal file
450
frontend/src/pages/courses/WifiSecurityPage.jsx
Normal file
@ -0,0 +1,450 @@
|
||||
// 이 파일이 하는 일: "무선 네트워크와 보안" 코스 — 눈에 안 보이는 전파로 데이터가
|
||||
// 어떻게 날아가는지에서 출발해, 공용 와이파이의 위험·피싱 구별법·비밀번호와 2단계 인증까지
|
||||
// '나를 지키는 습관'을 8개 섹션으로 안내하는 정적 학습 페이지.
|
||||
// (전파의 원리 → WPA2/WPA3 → 공용 와이파이의 위험 → 피싱·스미싱 → 비밀번호 관리
|
||||
// → 2단계 인증 → 회사 보안 수칙(수습 가이드 A7) → 정리 퀴즈)
|
||||
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 global.css의
|
||||
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용한다 — 색을 여기서 하드코딩하지 않는다.
|
||||
// 텍스트 다이어그램은 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// ── 텍스트 다이어그램 · 표 상수들 ──
|
||||
|
||||
const CODE_RADIO = `와이파이 = 눈에 안 보이는 '빛'으로 보내는 모스 부호
|
||||
|
||||
[노트북] ~~~전파(2.4GHz / 5GHz)~~~ [공유기(AP)] ──랜선──> 인터넷
|
||||
|
||||
- 전파도 빛과 같은 전자기파예요. 다만 눈에 보이는 색보다 훨씬 '낮은 음'일 뿐.
|
||||
- 0과 1을 전파의 모양(진폭·주파수·위상)에 실어서 깜빡깜빡 보냅니다.
|
||||
- 2.4GHz: 멀리 가고 벽을 잘 뚫지만 붐빔 (전자레인지·블루투스와 같은 동네!)
|
||||
- 5GHz : 빠르지만 벽에 약함 — 방문 하나에도 신호가 뚝 떨어져요.
|
||||
|
||||
핵심: 전파는 '벽이 없는 복도'에 대고 말하는 것과 같아서,
|
||||
범위 안의 누구나 물리적으로는 '들을 수' 있습니다. → 그래서 암호화가 필수!`;
|
||||
|
||||
const CODE_NETSH = `# Windows 터미널(cmd/PowerShell)에서 — 지금 연결된 와이파이의 속살 보기:
|
||||
netsh wlan show interfaces
|
||||
|
||||
SSID : AWESOMEDEV_5G ← 네트워크 이름
|
||||
무선 종류 : 802.11ax ← Wi-Fi 6이라는 뜻!
|
||||
인증 : WPA3-개인 ← 오늘의 주인공 (섹션 2)
|
||||
채널 : 44 ← 5GHz 대역
|
||||
신호 : 88% ← 벽에서 멀어지면 떨어져요
|
||||
|
||||
# 자리를 옮겨 가며 '신호' %가 변하는 걸 관찰해 보세요.
|
||||
# '인증' 칸이 WPA2인지 WPA3인지도 꼭 확인!`;
|
||||
|
||||
const CODE_WPA = `무선 암호화의 세대교체 — 자물쇠의 역사
|
||||
|
||||
세대 상태 한 줄 평
|
||||
──────────────────────────────────────────────
|
||||
WEP 절대 금지 몇 분 만에 뚫림. 박물관행.
|
||||
WPA 은퇴 WEP 응급처치 버전. 역시 옛날 것.
|
||||
WPA2 아직 주력 20년 가까이 버틴 튼튼한 자물쇠.
|
||||
WPA3 현재 표준 최신. 아래 두 가지가 크게 좋아짐.
|
||||
|
||||
WPA3가 좋아진 점 (비유와 함께):
|
||||
1) 비밀번호 추측 공격 방어(SAE)
|
||||
WPA2: 악수 과정을 녹음해 가서 집에서 무한 대입 가능
|
||||
WPA3: 한 번 틀릴 때마다 공유기와 '다시 대면'해야 함 → 무한 대입 불가
|
||||
2) 순방향 비밀성(Forward Secrecy)
|
||||
나중에 비밀번호가 털려도, '과거에 오간' 데이터는 못 풀어요.
|
||||
매일 다른 열쇠로 잠근 일기장이라, 마스터키를 뺏겨도 어제 일기는 안전.`;
|
||||
|
||||
const CODE_MITM = `중간자 공격(MITM, Man-in-the-Middle) 개념도 — 가짜 우체부 이야기
|
||||
|
||||
정상 상황:
|
||||
[내 폰] ────────────────────> [카페 공유기] ──> [인터넷]
|
||||
|
||||
공격 상황: 공격자가 '진짜보다 신호가 센 가짜 공유기'를 켭니다.
|
||||
[내 폰] ──> [가짜 AP "Cafe_Free_WiFi"] ──> [진짜 인터넷]
|
||||
│
|
||||
공격자가 여기 앉아서
|
||||
오가는 내용을 전부 읽고 · 바꿀 수 있음
|
||||
|
||||
- 내 폰 입장에선 인터넷이 '잘 되니까' 눈치채기 어려워요.
|
||||
- 편지를 우체부가 몰래 뜯어 읽고, 다시 붙여서 배달하는 것과 같습니다.
|
||||
- 이름(SSID)은 누구나 마음대로 지을 수 있다는 게 함정 —
|
||||
"OO카페 공식 와이파이"라는 이름 자체는 아무 증명도 아니에요.`;
|
||||
|
||||
const CODE_DEFENSE = `공용 와이파이에서 나를 지키는 3중 방어
|
||||
|
||||
1) HTTPS 확인 — 편지 내용을 '암호문'으로 쓰기
|
||||
주소창의 자물쇠(🔒) = 브라우저와 서버가 종단간 암호화 중.
|
||||
가짜 우체부가 편지를 뜯어도 암호문만 보여요.
|
||||
단, 자물쇠는 "대화가 암호화됐다"는 뜻이지 "상대가 착한 사이트"라는
|
||||
보증이 아님! (피싱 사이트도 자물쇠는 달 수 있어요 → 섹션 4)
|
||||
|
||||
2) VPN — 아예 '전용 비밀 터널'로 다니기
|
||||
내 기기 ↔ VPN 서버 사이에 암호화 터널을 뚫고, 모든 트래픽이
|
||||
그 터널 안으로만 다녀요. 카페 공유기(와 가짜 우체부)에게는
|
||||
터널의 겉모습만 보이고 내용물·목적지가 안 보입니다.
|
||||
|
||||
3) 습관 — 공용 와이파이에서는
|
||||
✗ 인터넷뱅킹·결제·회사 시스템 로그인은 미루기
|
||||
✗ "자동 연결" 켜 두지 않기 (폰이 알아서 가짜 AP에 붙을 수 있어요)
|
||||
✓ 급하면 차라리 내 폰의 LTE/5G 테더링 쓰기`;
|
||||
|
||||
const CODE_PHISHING = `피싱·스미싱 감별 체크리스트 — 4가지만 기억해요
|
||||
|
||||
□ 1. 긴급함을 조성하나요?
|
||||
"오늘까지 안 하면 계정 정지" "택배 반송 임박" —
|
||||
사람을 급하게 만들어 생각할 틈을 뺏는 게 1번 수법.
|
||||
|
||||
□ 2. 링크의 '진짜 도메인'이 맞나요?
|
||||
edu.awesomedevapp.com ← 진짜 (awesomedevapp.com 소속)
|
||||
edu.awesomedevapp.com.xn.ru ← 가짜! (진짜 주인은 맨 뒤 xn.ru)
|
||||
awesornedevapp.com ← 가짜! (m이 아니라 r+n, 폰트 착시)
|
||||
도메인은 '뒤에서부터' 읽으세요. 마지막 두 덩어리가 진짜 주인.
|
||||
|
||||
□ 3. 보낸 사람과 링크가 따로 노나요?
|
||||
문자·메일의 파란 글씨(표시 텍스트)와 실제 이동 주소는
|
||||
다를 수 있어요. PC에선 마우스를 '올려만' 놓고 좌하단 미리보기 확인.
|
||||
|
||||
□ 4. 비밀번호·인증번호를 '먼저' 요구하나요?
|
||||
정상 서비스는 문자·전화로 비밀번호나 인증번호를 묻지 않아요.
|
||||
인증번호를 알려달라는 전화 = 100% 사기.
|
||||
|
||||
스미싱(SMS+피싱) 단골 소재: 택배 주소 오류, 건강검진 결과,
|
||||
경조사 부고, 과태료·범칙금. → 링크 누르지 말고 공식 앱/사이트로 직접 확인!`;
|
||||
|
||||
const CODE_PASSWORD = `비밀번호 도미노 — 재사용이 위험한 진짜 이유
|
||||
|
||||
[허술한 사이트 A가 해킹당함]
|
||||
│ A에서 쓴 이메일+비밀번호 목록이 유출
|
||||
▼
|
||||
[공격자가 그 조합을 유명 서비스들에 자동 대입] ← 크리덴셜 스터핑
|
||||
▼
|
||||
[같은 비밀번호를 쓴 내 메일·SNS·회사 계정까지 연쇄 함락]
|
||||
|
||||
즉, 내 비밀번호의 안전은 '내가 가입한 사이트 중
|
||||
가장 허술한 곳'의 보안 수준으로 떨어집니다.
|
||||
|
||||
해법: 사이트마다 다른, 길고 무작위한 비밀번호
|
||||
사람 머리로는 불가능 → 비밀번호 관리자(password manager)에게 위임
|
||||
- 마스터 비밀번호 '하나'만 외우면, 나머지는 도구가
|
||||
생성(무작위 20자+)·저장·자동입력까지 해 줘요.
|
||||
- 브라우저 내장 관리자도 훌륭한 출발점입니다.
|
||||
- 마스터 비밀번호는 '길이'가 왕: 특수문자 범벅 8자보다
|
||||
"무작위 한글 단어 4~5개 조합" 같은 긴 문구가 더 강해요.`;
|
||||
|
||||
const CODE_2FA = `2단계 인증(2FA) = 아는 것 + 가진 것
|
||||
|
||||
1단계: 비밀번호 → 내가 '아는 것' (유출될 수 있음)
|
||||
2단계: 내 폰의 코드 → 내가 '가진 것' (공격자에겐 없음)
|
||||
|
||||
비밀번호가 통째로 털려도, 공격자 화면엔 이게 뜹니다:
|
||||
┌─────────────────────────────┐
|
||||
│ 인증 앱의 6자리 코드를 입력하세요 │ ← 여기서 막힘!
|
||||
└─────────────────────────────┘
|
||||
|
||||
방식별 안전도 (아래로 갈수록 강함):
|
||||
SMS 문자 △ 없는 것보단 훨씬 낫지만 심스와핑 위험
|
||||
인증 앱(TOTP) ○ 30초마다 바뀌는 코드, 오프라인 생성
|
||||
보안키/패스키 ◎ 피싱 사이트에선 아예 작동 안 함
|
||||
|
||||
한 줄 결론: 메일·Gitea·클라우드처럼 '다른 계정의 열쇠'가 되는
|
||||
계정부터 2FA를 켜세요. 특히 이메일 = 모든 비밀번호 재설정의 관문!`;
|
||||
|
||||
const CODE_COMPANY = `수습 가이드 A7 '보안 수칙'과 오늘 배운 것의 연결
|
||||
|
||||
A7 수칙 오늘 코스의 근거
|
||||
──────────────────────────────────────────────────
|
||||
회사 계정 2FA 필수 섹션 6 — 비밀번호 유출 시 2차 방어선
|
||||
사내 계정 비밀번호 재사용 금지 섹션 5 — 크리덴셜 스터핑 도미노
|
||||
외부/공용 와이파이에서
|
||||
사내 시스템 접속 시 VPN 섹션 3 — 중간자 공격 차단
|
||||
출처 불명 링크·첨부 열지 않기 섹션 4 — 피싱의 관문 봉쇄
|
||||
자리 비울 때 화면 잠금(Win+L) '물리적 중간자'도 있어요
|
||||
|
||||
왜 회사에서 더 엄격할까?
|
||||
개인 계정이 털리면 '내' 피해로 끝나지만, 회사 계정 하나가 털리면
|
||||
동료 전체·고객 데이터·서비스(EC2의 운영 서버, Gitea의 소스 코드,
|
||||
PostgreSQL의 실데이터)까지 도미노로 무너질 수 있기 때문이에요.
|
||||
신입/수습의 계정이 오히려 공격의 첫 표적이 되는 경우가 많습니다 —
|
||||
"아직 규칙에 익숙하지 않을 것"이라고 노리는 거죠.`;
|
||||
|
||||
const CODE_QUIZ = `셀프 체크 5문항 — 답을 소리 내어 설명할 수 있으면 통과!
|
||||
|
||||
Q1. 와이파이 전파는 왜 '벽 없는 복도에서 말하기'에 비유될까요?
|
||||
힌트: 범위 안에서는 누구나 물리적으로... (섹션 1)
|
||||
|
||||
Q2. WPA3가 WPA2보다 비밀번호 추측 공격에 강한 이유는?
|
||||
힌트: 녹음해 가서 집에서 대입 vs 매번 다시 대면 (섹션 2)
|
||||
|
||||
Q3. 카페 와이파이 이름이 "OO카페 공식"이면 믿어도 될까요?
|
||||
힌트: SSID는 누가 정하죠? (섹션 3)
|
||||
|
||||
Q4. 주소창의 자물쇠(HTTPS)가 있으면 피싱 걱정은 끝일까요?
|
||||
힌트: 자물쇠가 보증하는 건 '암호화'지 '착한 상대'가 아님 (섹션 3·4)
|
||||
|
||||
Q5. 여러 사이트에 같은 비밀번호를 쓰면, 내 보안 수준은
|
||||
어느 사이트에 의해 결정될까요? (섹션 5)
|
||||
|
||||
→ 내가 쓴 답이 맞는지 멘토와 함께 확인해 보세요.
|
||||
"말로 설명하다 막히는 지점"이 진짜 복습 포인트입니다.`;
|
||||
|
||||
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
|
||||
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
|
||||
function Section({ n, title, sub, children }) {
|
||||
return (
|
||||
<section className="step-card" id={`sec-${n}`}>
|
||||
<div className="step-head">
|
||||
<span className="step-num">{n}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
{sub && <span className="step-sub">{sub}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Code({ children }) {
|
||||
return <pre className="code-block"><code>{children}</code></pre>;
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ n: 1, label: '전파의 원리' },
|
||||
{ n: 2, label: 'WPA2 / WPA3' },
|
||||
{ n: 3, label: '공용 와이파이의 위험' },
|
||||
{ n: 4, label: '피싱·스미싱' },
|
||||
{ n: 5, label: '비밀번호 관리' },
|
||||
{ n: 6, label: '2단계 인증' },
|
||||
{ n: 7, label: '회사 보안 수칙' },
|
||||
{ n: 8, label: '정리 퀴즈' },
|
||||
];
|
||||
|
||||
export default function WifiSecurityPage() {
|
||||
return (
|
||||
<div>
|
||||
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
|
||||
<div className="hero">
|
||||
<div className="eyebrow">Course · 네트워크</div>
|
||||
<h1>무선 네트워크와 보안<br />— 보이지 않는 전파, 보여야 하는 위험</h1>
|
||||
<p>
|
||||
랜선 없이도 인터넷이 되는 건 데이터가 <strong>전파</strong>를 타고 날아다니기
|
||||
때문인데, 전파는 벽 없는 복도라서 <strong>지키는 법</strong>을 함께 배워야 해요.
|
||||
와이파이의 원리에서 출발해 공용 와이파이의 함정, 피싱 감별법, 그리고 회사
|
||||
보안 수칙(수습 가이드 A7)까지 — <strong>"직접 확인해 보기"</strong>는 꼭 내
|
||||
PC와 폰에서 손으로 해보세요.
|
||||
</p>
|
||||
<div className="chip-row">
|
||||
<span className="chip">예상 소요 60분</span>
|
||||
<span className="chip">실습 4개</span>
|
||||
<span className="chip">준비물: 내 PC + 스마트폰</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 섹션 바로가기 */}
|
||||
<nav className="pill-nav">
|
||||
{SECTIONS.map((s) => (
|
||||
<a key={s.n} href={`#sec-${s.n}`}>
|
||||
<b>{s.n}</b>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Section n={1} title="전파로 데이터가 가는 원리" sub="선이 없는데 어떻게 0과 1이 날아갈까?">
|
||||
<p>
|
||||
<Link to="/learn/network">네트워크의 이해</Link> 코스에서 랜선 속 구리선으로
|
||||
전기 신호가 달리는 걸 배웠죠. 와이파이는 그 <strong>구리선을 공기로 바꾼 것</strong>이에요.
|
||||
데이터(0과 1)를 <strong>전파</strong>라는 전자기파에 실어서, 노트북과 공유기가
|
||||
서로에게 손전등으로 모스 부호를 보내듯 깜빡깜빡 주고받습니다.
|
||||
</p>
|
||||
<Code>{CODE_RADIO}</Code>
|
||||
<p>
|
||||
여기서 보안 관점의 결정적 차이가 나와요. 랜선은 <strong>꽂은 사람만</strong> 신호를
|
||||
받지만, 전파는 <strong>범위 안의 모두에게 퍼집니다</strong>. 교실에서 쪽지를 손으로
|
||||
전달하는 것(유선)과, 칠판에 크게 써 붙이는 것(무선)의 차이예요. 그래서 무선은
|
||||
쪽지 내용을 <strong>암호문으로 바꿔 쓰는 것</strong>, 즉 암호화가 태생적으로 필수입니다.
|
||||
그 암호화 방식이 바로 다음 섹션의 WPA예요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 터미널에 아래 명령을 쳐서 지금 연결된 와이파이의
|
||||
주파수 대역·신호 세기·<strong>인증 방식(WPA2? WPA3?)</strong>을 확인해 보세요.
|
||||
노트북이라면 공유기에서 먼 방으로 이동하며 <span className="icode">신호</span> %가
|
||||
떨어지는 것도 관찰!
|
||||
</div>
|
||||
<Code>{CODE_NETSH}</Code>
|
||||
</Section>
|
||||
|
||||
<Section n={2} title="WPA2와 WPA3" sub="와이파이 자물쇠의 세대교체">
|
||||
<p>
|
||||
와이파이 비밀번호를 입력하는 순간, 내 기기와 공유기는 그 비밀번호를 바탕으로
|
||||
<strong> 암호 열쇠</strong>를 만들어 이후의 모든 전파를 잠급니다. 이 잠그는
|
||||
규칙의 이름이 <strong>WPA</strong>(Wi-Fi Protected Access)이고, 세대를 거치며
|
||||
자물쇠가 점점 단단해졌어요.
|
||||
</p>
|
||||
<Code>{CODE_WPA}</Code>
|
||||
<p>
|
||||
집이나 회사 공유기 설정에서 암호화 방식을 고를 수 있다면
|
||||
<strong> WPA3</strong>(구형 기기가 있으면 WPA2/WPA3 혼합)를 선택하는 게 정답이에요.
|
||||
그리고 아무리 WPA3라도 <strong>와이파이 비밀번호 자체가 짧고 뻔하면</strong>
|
||||
(<span className="icode">12345678</span> 같은) 자물쇠가 좋아도 열쇠를 문 앞에
|
||||
둔 셈이라는 것, 잊지 마세요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>WEP나 '열림(Open)'으로 표시되는 와이파이</b>는 전파가 암호화 없이 그대로
|
||||
날아다닌다는 뜻이에요. 칠판에 쪽지를 <strong>원문 그대로</strong> 써 붙이는 것과
|
||||
같으니, 연결하더라도 로그인 같은 민감한 일은 하지 않는 게 원칙입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={3} title="공용 와이파이의 위험과 자기 방어" sub="가짜 우체부가 편지를 뜯어 보는 세계 — 중간자 공격">
|
||||
<p>
|
||||
카페·지하철의 공짜 와이파이는 편하지만, <strong>누가 세운 공유기인지 알 수 없다</strong>는
|
||||
근본 문제가 있어요. 최악의 시나리오가 <strong>중간자 공격</strong>(MITM)입니다 —
|
||||
공격자가 나와 인터넷 <strong>사이에 끼어들어</strong> 오가는 내용을 몰래 읽거나
|
||||
바꾸는 공격이에요.
|
||||
</p>
|
||||
<Code>{CODE_MITM}</Code>
|
||||
<p>
|
||||
무섭죠? 하지만 방어법은 명확합니다. 편지를 <strong>뜯겨도 못 읽게</strong> 만들거나
|
||||
(HTTPS), 아예 <strong>전용 터널</strong>로 다니는(VPN) 거예요.
|
||||
</p>
|
||||
<Code>{CODE_DEFENSE}</Code>
|
||||
<p>
|
||||
VPN을 비유하면, 복도(공용 와이파이)에 <strong>불투명한 파이프</strong>를 깔고
|
||||
그 안으로만 다니는 겁니다. 복도에 서 있는 사람(가짜 우체부 포함)은 파이프가
|
||||
있다는 것만 알 뿐, 안에서 뭐가 어디로 가는지 못 봐요. 우리 회사도 외부에서
|
||||
사내 시스템에 접속할 땐 VPN을 쓰도록 하는 이유가 이거예요(섹션 7).
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 지금 이 페이지 주소창의 <strong>자물쇠 아이콘</strong>을
|
||||
클릭해 보세요. "연결이 안전함" → 인증서 정보에서 <strong>누구에게 발급된
|
||||
인증서인지</strong>(edu.awesomedevapp.com)까지 열어 보면, 브라우저가 매 접속마다
|
||||
"상대가 진짜인지" 검사하고 있다는 걸 눈으로 확인할 수 있어요.
|
||||
<span className="kbd">F12</span> → <span className="kbd">Security</span> 탭에서도
|
||||
같은 정보를 볼 수 있습니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={4} title="피싱·스미싱 구별법" sub="기술이 아니라 '사람'을 해킹하는 공격">
|
||||
<p>
|
||||
지금까지는 전파·암호화 같은 기술 이야기였다면, <strong>피싱</strong>(phishing)은
|
||||
기술 대신 <strong>사람의 마음</strong>을 노려요. 진짜처럼 꾸민 가짜 사이트·메일·문자로
|
||||
비밀번호를 <strong>스스로 입력하게</strong> 만드는 거죠. 낚시(fishing)처럼 미끼를
|
||||
던진다고 해서 피싱, 문자(SMS)로 오면 <strong>스미싱</strong>입니다.
|
||||
</p>
|
||||
<Code>{CODE_PHISHING}</Code>
|
||||
<p>
|
||||
체크리스트 2번이 특히 중요해요. 도메인은 <strong>뒤에서부터</strong> 읽는 습관을
|
||||
들이세요. <span className="icode">edu.awesomedevapp.com</span>의 주인은
|
||||
<span className="icode">awesomedevapp.com</span>이지만,
|
||||
<span className="icode">awesomedevapp.com.evil.ru</span>의 진짜 주인은 맨 뒤의
|
||||
<span className="icode">evil.ru</span>예요. 앞부분은 얼마든지 그럴듯하게 붙일 수 있거든요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>피싱 사이트도 자물쇠(HTTPS)를 달 수 있어요.</b> 자물쇠는 "대화가 암호화된다"는
|
||||
뜻일 뿐, "상대가 착하다"는 보증이 아닙니다. 가짜 은행 창구에서도 문은 잠글 수
|
||||
있잖아요. <strong>암호화 확인(자물쇠) + 도메인 확인(진짜 주인)</strong>, 둘 다
|
||||
해야 안전합니다.
|
||||
</div>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 최근 받은 광고·안내 문자 중 링크가 든 것을 하나 골라
|
||||
(누르지 말고!) 링크를 <strong>길게 눌러 주소 미리보기</strong>를 띄워 보세요.
|
||||
도메인을 뒤에서부터 읽어 진짜 주인이 누군지 판별 — 위 체크리스트 4개를 하나씩
|
||||
적용해 보는 게 오늘의 실전 훈련입니다.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={5} title="비밀번호 관리" sub="재사용은 도미노, 기억은 도구에게">
|
||||
<p>
|
||||
"비밀번호를 어렵게 만들어라"보다 먼저 알아야 할 게 <strong>재사용 금지</strong>예요.
|
||||
아무리 어려운 비밀번호도 <strong>여러 사이트에 같이 쓰는 순간</strong> 도미노의
|
||||
첫 조각이 됩니다.
|
||||
</p>
|
||||
<Code>{CODE_PASSWORD}</Code>
|
||||
<p>
|
||||
"사이트마다 다른 무작위 20자를 어떻게 다 외워요?"가 당연한 반응이고, 정답은
|
||||
<strong> 외우지 않는 것</strong>이에요. <strong>비밀번호 관리자</strong>는 금고지기
|
||||
같은 도구라서, 우리는 금고 열쇠(마스터 비밀번호) 하나만 단단히 만들고 나머지
|
||||
비밀번호의 생성·보관·입력은 전부 금고지기에게 맡깁니다. 관리자 도구는 저장해 둔
|
||||
사이트의 <strong>진짜 도메인에서만</strong> 자동입력이 뜨기 때문에, 피싱 사이트에서
|
||||
자동입력이 <strong>안 뜨는 것 자체가 경보</strong>가 되는 보너스 효과도 있어요(섹션 4 복습!).
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 크롬 주소창에
|
||||
<span className="icode">chrome://password-manager/checkup</span>을 열어 보세요
|
||||
(엣지는 설정 → 프로필 → 비밀번호). 브라우저가 내 저장된 비밀번호 중
|
||||
<strong> 유출된 것·재사용 중인 것·약한 것</strong>을 검사해 줍니다. 재사용 목록이
|
||||
나온다면 오늘 밤 안에 하나씩 바꾸는 게 이번 코스의 진짜 과제예요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={6} title="2단계 인증, 왜 켜나" sub="비밀번호가 털려도 문이 안 열리는 이유">
|
||||
<p>
|
||||
섹션 5를 완벽히 지켜도 비밀번호는 털릴 수 있어요 — 내 잘못이 아니라
|
||||
<strong> 사이트 쪽이 해킹</strong>당해서요. 그래서 마지막 안전망이
|
||||
<strong> 2단계 인증</strong>(2FA)입니다. 현관에 <strong>비밀번호 자물쇠 + 내 폰에만
|
||||
있는 열쇠</strong>, 이중 잠금을 다는 거예요.
|
||||
</p>
|
||||
<Code>{CODE_2FA}</Code>
|
||||
<p>
|
||||
"매번 코드 치기 귀찮은데요"가 솔직한 마음이죠. 하지만 대부분의 서비스는
|
||||
<strong> 자주 쓰는 기기를 기억</strong>해서 2FA를 매일 묻지 않아요. 실제 체감은
|
||||
"새 기기에서 로그인할 때 한 번 더" 정도인데, 얻는 건 <strong>비밀번호 유출을
|
||||
무력화하는 방패</strong>입니다. 세상에서 가성비가 가장 좋은 보안 습관이에요.
|
||||
</p>
|
||||
<div className="tip">
|
||||
<b>직접 확인해 보기</b> 우리 Gitea(<span className="icode">edu.awesomedevapp.com</span>)에
|
||||
로그인 → 우상단 프로필 → <span className="kbd">설정</span> →
|
||||
<span className="kbd">보안</span>에서 <strong>2단계 인증(TOTP) 등록</strong>을 켜 보세요.
|
||||
폰에 인증 앱을 설치하고 QR을 찍으면 끝 — 이때 함께 발급되는
|
||||
<strong> 복구 코드</strong>는 폰을 잃어버렸을 때의 비상열쇠니 안전한 곳에 적어 두기!
|
||||
개인 이메일 계정에도 오늘 켜는 것을 강력 추천해요.
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={7} title="회사 보안 수칙과 연결" sub="수습 가이드 A7이 잔소리가 아닌 이유">
|
||||
<p>
|
||||
여기까지 배웠다면, 수습 가이드 <strong>A7 보안 수칙</strong>이 왜 그렇게 쓰여
|
||||
있는지 전부 설명할 수 있어요. 규칙을 외우는 것과 <strong>이유를 아는 것</strong>은
|
||||
완전히 다릅니다 — 이유를 알면 규칙에 없는 새로운 상황에서도 스스로 판단할 수 있거든요.
|
||||
</p>
|
||||
<Code>{CODE_COMPANY}</Code>
|
||||
<p>
|
||||
하나만 더 — 보안 사고에서 가장 위험한 건 사고 자체보다 <strong>숨기는 것</strong>이에요.
|
||||
이상한 링크를 눌러 버렸거나, 비밀번호를 입력하고 나서 "어? 뭔가 이상한데" 싶으면
|
||||
<strong> 그 즉시 멘토나 관리자에게 알리는 게 정답</strong>입니다. 빨리 알리면
|
||||
비밀번호 변경·세션 차단으로 몇 분 만에 막을 수 있는 일이, 숨기면 도미노가 돼요.
|
||||
어썸데브에서 "보고했다"고 혼나는 일은 없습니다. 반대로 그게 수습이 보여줄 수 있는
|
||||
가장 프로다운 행동이에요.
|
||||
</p>
|
||||
<div className="warn">
|
||||
<b>회사 계정 = 개인 계정과 분리.</b> 사내 Gitea·AWS·DB 비밀번호를 개인 사이트
|
||||
비밀번호와 같게 쓰는 순간, 섹션 5의 도미노가 <strong>회사 전체</strong>로
|
||||
확장됩니다. 입사 첫 주에 받은 초기 비밀번호를 아직 안 바꿨다면 지금이 바로 그때!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section n={8} title="정리 퀴즈 — 셀프 체크 5문항" sub="설명할 수 있어야 아는 것">
|
||||
<Code>{CODE_QUIZ}</Code>
|
||||
<div className="tip">
|
||||
<b>공부 팁</b> 답을 종이에 쓰지 말고 <strong>옆 사람에게 말로 설명</strong>해 보세요.
|
||||
비유("벽 없는 복도", "가짜 우체부", "도미노")를 들어 설명할 수 있으면 진짜 이해한
|
||||
거예요. 막히는 문항은 해당 섹션으로 돌아가 "직접 확인해 보기"를 한 번 더!
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="step-card">
|
||||
<div className="step-body">
|
||||
<h3 style={{ marginBottom: 6 }}>🔐 여기까지 왔다면</h3>
|
||||
<p className="muted">
|
||||
전파가 데이터를 나르는 원리부터 WPA3 자물쇠, 중간자 공격과 HTTPS·VPN 방패,
|
||||
피싱 감별 4단계, 비밀번호 도미노와 2단계 인증, 그리고 A7 수칙의 '이유'까지 —
|
||||
이제 여러분은 규칙을 <strong>따르는</strong> 사람이 아니라 <strong>이해하고 지키는</strong> 사람이에요.
|
||||
데이터가 달리는 길 자체가 궁금해졌다면{' '}
|
||||
<Link to="/learn/network"><strong>네트워크의 이해</strong></Link> 코스로,
|
||||
오늘 배운 습관은 <strong>Gitea 2FA 켜기 + 비밀번호 checkup 돌리기</strong> 두 가지
|
||||
과제로 바로 실천해 보세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user