← Start here · Module 2A · Foundations (hello-world)
Flask + React(Vite) hello-world에 나오는 개념들을 비유부터 차근차근. 각 항목은 ① 한 줄 요약 → ② 비유 → ③ 이 앱에서는 → ④ (펼치면) 더 깊이, 그리고 ⑤ 미니 퀴즈로 끝납니다. 퀴즈는 선택지를 누르면 바로 정답/오답을 알려줘요.
ft_inception에서 docker-compose로
nginx + 여러 컨테이너를 띄우고 nginx가 요청을 알맞은 서비스로 넘겨준 그
구조가 여기 두 서버 + 프록시와 똑같습니다.
ft_transcendence의 SPA + 백엔드 + 로그인(JWT) 경험은 여기
React fetch + stateless 이야기와 그대로 이어집니다.
→ 커밋마다 한 일은 변경 이력에서.
프로그램이 두 개 돕니다. 화면 담당(프론트)과 데이터 담당(백). 둘은 서로 다른 문(포트)으로 들어갑니다.
:5173)는
손님을 받는 홀, 백(:5001, Flask)은 주방입니다.
손님(브라우저)이 "인사말 주세요" 주문하면, 홀 직원(Vite)이 주방에 전달하고,
주방이 만든 걸 다시 손님에게 가져다줍니다.
이 앱에서 데이터 하나가 흐르는 길:
핵심: 프론트와 백은 별개의 프로그램이고, 둘 다 따로 켜야 합니다(터미널 2개). ft_inception에서 컨테이너를 여러 개 띄운 것과 같은 느낌이에요.
이 앱은 프로그램이 몇 개 돌고 있나요?
프론트(Vite, 5173)와 백(Flask, 5001)은 별개의 프로그램이라 터미널 2개로 각각 띄웁니다. (DB는 이 hello-world엔 없어요.)
"이 주소로 요청이 오면 이 함수를 실행해"를 적는 파이썬 도구.
@app.route("/api/hello")는
"이 번호로 오면 이 사람 바꿔줘"라고 교환대에 등록하는 것입니다.
이 앱의 백엔드는 사실 이게 거의 전부입니다:
@app.route("/api/hello") # 이 주소로 GET이 오면
def hello(): # 이 함수를 실행하고
return jsonify(message="Hello from Flask") # 이 JSON을 돌려준다
"마이크로 프레임워크"라고 부르는 건 기능이 적어서가 아니라, 요청 → 함수 → 응답이라는 최소 골격만 주고 DB·로그인 같은 건 강제하지 않는다는 뜻입니다. 필요한 부품은 직접 골라 끼웁니다. (Django가 풀옵션 정장이라면 Flask는 티셔츠 한 장에서 시작하는 느낌.)
@app.route("/api/hello")가 하는 일은?
@app.route는 "주소 ↔ 함수"를 연결(라우팅)하는 표시예요.
화면을 그리는 건 프론트(React)의 일이고, Flask는 요청에 함수로 답할 뿐입니다.
"주소(경로) + 방법(메서드)" 한 쌍. 우리에겐 GET /api/hello 하나뿐.
흔히 쓰는 방법(메서드):
GET — 달라고 요청 (읽기). 우리 앱이 쓰는 것.POST — 새로 만들어 달라 (쓰기).PUT/PATCH — 고쳐 달라, DELETE — 지워 달라.GET /api/hello 와 POST /api/hello 의 관계는?
엔드포인트 = "경로 + 메서드". 경로가 같아도 GET과 POST는 다른 항목이라,
같은 /api/hello에 둘 다 따로 정의할 수 있습니다.
"다른 동네에서 온 응답을 읽어도 돼?"를 브라우저가 확인하는 보안 규칙.
CORS(app) 한 줄이 도장을 자동으로 찍어줍니다.
왜 막을까요? 악성 사이트가 당신이 로그인한 은행 페이지의 응답을 몰래 읽어가는 걸 막기 위해서입니다. 그래서 기본은 차단, 허락은 명시적으로.
CORS(app)은 프록시를 안 거치고
브라우저가 백엔드를 직접 부를 때(또는 배포 후)를 위한 안전장치예요.
"출처(origin)"는 스킴+호스트+포트의 조합입니다. 포트만 달라도
(5173 vs 5001) 다른 출처죠. 또 일부 요청은 진짜 요청 전에
OPTIONS라는 예비 질문(preflight)을 먼저
보내 "이렇게 보내도 돼?"를 확인합니다.
중요한 오해 하나: CORS는 서버를 지키는 게 아니라
브라우저가 사용자를 지키는 규칙입니다. 도장이 없어도 요청은
서버에 도달하고 처리됩니다 — 단지 브라우저가 그 응답을 JS에게 안 넘길
뿐. 그래서 curl엔 CORS가 의미가 없습니다.
CORS는 누구를, 무엇으로부터 보호하나요?
CORS는 브라우저가 사용자를 지키는 규칙입니다. 그래서 브라우저를 안 쓰는
curl엔 영향이 없어요(요청 자체는 늘 서버에 도달합니다).
화면을 직접 고치지 말고, "이 상태면 화면이 이렇게 생겨야 해"만 적는 도구.
document.createElement로 DOM을 손수 조작), 당신은 설계도만
그립니다. 그러면 React가 현재 집과 설계도를 비교해 바뀐 부분만
알아서 시공합니다.
그래서 우리 코드는 "데이터가 있으면 이렇게, 없으면 이렇게"만 적습니다:
{error
? <p>Error: {error}</p> // 에러면 빨간 문구
: <p>{message || "Loading…"}</p>} // 아니면 메시지(없으면 Loading…)
ft_transcendence에서 화면을 JS로 직접 갈아끼웠다면, React는 그 "갈아끼우기"를 대신 해주는 도구라고 보면 됩니다.
React는 상태가 바뀌면 화면을 머릿속(메모리)에서 새로 그려보고, 직전 그림과 비교(diff)해 달라진 칸만 실제 화면에 반영합니다. 화면을 통째로 다시 그리는 것보다 훨씬 싸기 때문입니다. 자료구조 두 개를 비교해 차이만 적용하는 문제라고 보면 익숙할 거예요.
React에서 화면을 바꾸는 올바른 방식은?
React는 선언형이에요. 당신은 "상태 → 화면" 규칙만 적고, 상태를 바꾸면 React가 바뀐 부분만 찾아서 실제 화면에 적용합니다.
프론트 개발용 서버. 코드 저장하면 화면이 즉시 갱신되고, 백으로 요청을 대신 넘겨준다.
/api로 시작하는 편지를 보면
"이건 주방 거네" 하고 백엔드(:5001)로 대신 배달합니다. 이게
vite.config.js의 프록시(proxy) 설정이에요.
설정은 딱 이 한 줄의 규칙입니다:
// vite.config.js
proxy: { "/api": "http://localhost:5001" }
// "/api 로 시작하는 요청은 전부 5001(Flask)로 넘겨라"
/api는 백엔드 컨테이너로, 나머지는 프론트로 넘겼던 그 설정 —
Vite 프록시가 개발용으로 그 역할을 합니다.
Vite가 빠른 이유: 옛 도구들은 프로젝트 전체를 미리 하나로 합쳐(번들링)
놓느라 시작이 느렸는데, Vite는 브라우저가 필요하다고 요청하는 파일만
그때그때 변환해 보냅니다. 그래서 프로젝트가 커도 dev 서버가 즉시 뜹니다.
(배포용 vite build일 때만 전체를 합쳐 최적화합니다.)
proxy: { "/api": "http://localhost:5001" } 의 뜻은?
프록시는 "전달 규칙"이에요. 브라우저는 5173에만 요청하고, Vite가
/api 요청을 골라 5001로 넘겨줍니다(ft_inception의 nginx처럼).
JavaScript 안에 HTML처럼 쓰는 문법. 브라우저가 읽기 전에 평범한 JS로 번역된다.
<p>{message}</p>처럼
HTML같이 생긴 걸 JS 파일에 쓰면, 빌드 도구가 이걸 브라우저가 아는 평범한
함수 호출로 번역합니다. 브라우저는 JSX를 모르고, 번역된 결과만 봅니다.
// 우리가 쓰는 것 (JSX)
<p>{message}</p>
// 빌드 후 실제로 실행되는 것 (평범한 JS)
React.createElement("p", null, message)
파일 확장자가 .jsx인 건 "이 파일엔 JSX 문법이 섞여 있으니
번역해 줘"라고 도구에 알리는 표시입니다.
<p>{message}</p> 같은 JSX는 결국 무엇이 되나요?
브라우저는 JSX를 모릅니다. 빌드 도구가 JSX를 JS 함수 호출로 번역하고, 브라우저는 그 번역 결과만 실행해요.
컴포넌트가 화면에 처음 나타나는 순간. "fetch on mount" = 화면 뜨자마자 한 번 데이터 가져오기.
그걸 담당하는 코드:
useEffect(() => {
load(); // 데이터 가져오기
}, []); // ← 빈 [] = "처음 한 번만". 이 줄이 'on mount'를 뜻한다
맨 끝의 빈 배열 []가 "마운트 때 한 번만"의 신호입니다.
만약 거기에 무언가를 넣으면 그게 바뀔 때마다 다시 실행돼요.
네트워크 요청 같은 "부수효과"는 화면을 그리는 함수 본문에 직접 두지
않고 useEffect 안에 둡니다. 화면 그리는 함수는 그릴 때마다
실행되고 깔끔해야(예측 가능해야) 하기 때문에, 외부와 통신하는 일은
따로 떼어 두는 것입니다.
참고: 개발 모드(StrictMode)에선 이 효과가 일부러 두 번 실행돼서 콘솔에 요청이 두 번 찍힐 수 있는데, 버그를 미리 드러내려는 장치이니 정상입니다.
useEffect(() => {...}, []) 의 빈 []가 의미하는 것은?
빈 배열은 "의존하는 값이 없음" → 마운트 때 한 번만. 배열에 값을 넣으면 그 값이 바뀔 때마다 다시 실행됩니다.
서버는 기억상실. 매 요청을 "처음 보는 손님"처럼 대한다. 그래서 갱신하려면 매번 새로 물어야 한다.
예를 들어 서버 시각(/api/time)은 "묻는 그 순간"의 값일 뿐입니다.
1분 뒤 시각을 알고 싶으면? 다시 물어봐야 합니다. 서버가 알아서
밀어주지 않아요.
"기억이 없다"는 건 서버 얘기일 뿐, 화면 쪽(React)은
useState로 잠깐의 상태(message, error)를 들고 있습니다.
서버 시각을 1분마다 갱신해서 보여주려면?
HTTP는 stateless라 서버가 먼저 밀어주지 않습니다. 최신 값을 원하면
클라이언트가 주기적으로 다시 fetch해야 해요(예: setInterval).
위 퀴즈가 개념이었다면, 이건 실제 코드를 보고 답하는 문제입니다. 정답은 접어 뒀어요 — 먼저 생각해 보고 펼치세요.
GET /api/time을 추가해 시각을 보여주려면 어떤 파일을?두 파일만:
Projects/Module_02_chat-app/backend/app.py — 새 라우트 추가, 현재 시각을 JSON으로 반환.Projects/Module_02_chat-app/frontend/src/App.jsx — 그 시각을 받아 화면에 표시.
vite.config.js는 안 바꿔도 됩니다 — 프록시
규칙이 "/api로 시작하는 건 전부"라서 새 주소도 자동 포함이거든요.