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:
AWESOMEDEV 2026-07-16 10:04:29 +09:00
parent 3adebdb44a
commit 96f92eceba
48 changed files with 18125 additions and 157 deletions

View File

@ -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 />} />

View 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));

View File

@ -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>
))}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 ?"
// : 8070 '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>
);
}

View 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>
);
}

View 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>흑백으로 상상했을 때도 위계(제목 &gt; 본문 &gt; 배경) 보이는지 확인해요.</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>
);
}

View 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>
);
}

View 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>
);
}

View 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() 데이터가 10배면 일은 100!
전체 악수: 30명이 서로 악수하면 450,
300명이면 45,000 사람 10배에 악수는 100
) 반복문 안의 반복문 (모든 비교)
n = 100,000 대략:
O(1) = 1 깜빡할
O(n) = 100,000 그래도 순식간
O() = 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()</strong>(반복문 반복문) 때문입니다.
정확한 계산은 아침수업에서 다듬고, 지금은 <strong> 단계의 체감 차이</strong>
가져가면 충분해요.
</p>
<div className="warn">
<b>흔한 오해</b> "O(n)은 나쁘다" 아니에요. 데이터가 100개뿐이면 O()
멀쩡히 돌아갑니다. 문제는 <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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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">&lt;&lt;&lt;&lt;&lt;&lt;&lt; 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>
);
}

View 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>
);
}

View 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
겹침
겹침
안전: 1611
아파트라면? 윗집·옆집 공유기가 전부 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>
);
}

View 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>
);
}

View 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">&lt;head&gt;</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">&lt;svg&gt;</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>
);
}

View 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 사람이 읽는 표기 (점으로 구분)
11000000101010000000000000000101 컴퓨터가 보는 실제 모습
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>
);
}

View 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>
);
}

View 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>
);
}

View 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">&gt;&gt; 로그파일</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">&gt;</span> <span className="icode">&gt;&gt;</span> 차이는
기억하세요. <span className="icode">&gt;</span> <strong>덮어쓰기</strong>(기존 내용 삭제!),{' '}
<span className="icode">&gt;&gt;</span> <strong>이어 붙이기</strong>예요. 로그를 모을 {' '}
<span className="icode">&gt;</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">&gt; 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>
);
}

View 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> 사람이 8GB32GB로 올려도 체감이 없고,
램이 <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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>해상도 &gt; 패널(IPS) &gt; 주사율</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>
);
}

View 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>
);
}

View 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">&lt;Badge /&gt;</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">&lt;div&gt;...&lt;/div&gt;</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>
);
}

View 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>
);
}

View File

@ -0,0 +1,382 @@
// : "Spring Boot " (DI)
// , ControllerServiceRepository , 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>
);
}

View 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(*) &gt; 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">&gt;</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>
);
}

View 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>
);
}

View 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>
);
}

View 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> 우리 프로젝트 폴더에서 루틴 12번을 그대로 실행해 보고,
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>
);
}

View 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>
);
}

View 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>
);
}

View 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">&lt;div&gt;</span> 열고 닫고, 클래스 붙이고
반복 구조를 손으로 치다 보면 오타가 나기 마련이에요. <strong>Emmet</strong>
"만들고 싶은 구조" 축약식으로 쓰면 태그로 펼쳐 주는 내장 기능입니다.
문장을 쓰는 대신 줄임말을 쓰는 것과 같아요.
</p>
<Code>{CODE_EMMET}</Code>
<div className="tip">
<b>직접 확인해 보기</b> 연습용 파일(<span className="icode">test.html</span>) 만들고
<span className="icode">ul&gt;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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}