Part 3.2 데이터는 어떻게 흐르는가
제가 개발을 처음 배울 때, 프론트와 백엔드 간, 혹은 각 사이드에서 데이터가 어떻게 '흘러가는지'를 전혀 이해를 못했습니다. 이 버튼을 누르면 왜 저쪽에서 뭔가가 툭 튀어나오지? submit을 눌렀을 뿐인데, 어찌 프론트와 백엔드라는 것들이 '소통'을 하지? 쟤는 어떻게 나한테 이 데이터를 주지?...등
이런 고민을 다시 마주하니까, 재밌기도 하고, 공감도 많이 가더라구요.
제가 겪었던 문제나, 교육생분들의 문제는 대게는 데이터가 어디서 어디로 흐르는지 이해하지 못해서 생깁니다.
데이터 흐름을 이해하면:
-
문제가 어느 단계에서 생겼는지 파악할 수 있음
-
AI에게 정확한 위치를 짚어서 질문할 수 있음
-
"왜 안 되지?"에서 "아, 여기가 문제구나"로 바뀜
복잡한 코드를 이해할 필요는 없습니다. 데이터가 이동하는 큰 그림만 알면 됩니다.
데이터의 4단계 여행
모든 앱에서 데이터는 이 4단계를 거칩니다:
[입력] → [처리] → [저장] → [표시]
↑ ↓
└──────────────────────────┘
카페 주문 앱을 예로 들어보겠습니다.
| 단계 | 카페 앱 예시 | 실제로 일어나는 일 |
|---|---|---|
| 입력 | 아메리카노 2잔 선택 | 사용자가 버튼 클릭/폼 작성 |
| 처리 | 가격 계산 (4,500원 × 2) | 코드가 데이터를 가공 |
| 저장 | 주문 내역 DB에 기록 | 데이터베이스에 저장 |
| 표시 | "주문 완료" 화면 | UI에 결과 표시 |
1단계: 입력 (사용자 → 앱)
사용자가 앱에 데이터를 넣는 모든 행위입니다.
흔한 입력 방식:
-
텍스트 입력 (
<input>,<textarea>) -
버튼 클릭 (
<button>) -
체크박스/라디오 선택
-
파일 업로드
-
드래그 앤 드롭
Cursor에서 보이는 코드:
// 입력창
<input
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
/>
// 버튼
<button onClick={handleSubmit}>
제출하기
</button>
여기서 핵심은 onChange와 onClick입니다. 사용자가 뭔가를 하면 → 이 함수들이 실행됩니다.
문제가 생겼을 때 체크할 것:
-
입력창에 타이핑하는데 글자가 안 써짐 →
onChange가 제대로 연결됐는지 -
버튼 눌러도 반응이 없음 →
onClick에 함수가 연결됐는지
2단계: 처리 (로직)
입력받은 데이터를 가공하는 단계입니다.
흔한 처리 작업:
-
계산 (가격, 수량, 합계)
-
검증 (이메일 형식 맞는지, 비밀번호 조건 충족하는지)
-
변환 (날짜 형식 바꾸기, 텍스트 정리)
-
필터링 (조건에 맞는 것만 골라내기)
Cursor에서 보이는 코드:
// 가격 계산 로직
const calculateTotal = (items) => {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
return total;
};
// 이메일 검증 로직
const isValidEmail = (email) => {
return email.includes("@") && email.includes(".");
};
문제가 생겼을 때 체크할 것:
-
계산 결과가 이상함 → 계산 로직(함수) 확인
-
"유효하지 않은 입력"이라고 뜸 → 검증 조건 확인
AI에게 요청하는 팁:
❌ "계산이 이상해요"
✅ "calculateTotal 함수에서 할인율이 적용 안 되는 것 같아요.
10% 할인이 적용되도록 수정해주세요."
3단계: 저장 (DB)
처리된 데이터를 어딘가에 보관하는 단계입니다.
여기서 중요한 개념이 나옵니다: 어디에 저장하느냐에 따라 데이터의 수명이 달라집니다.
| 저장 위치 | 수명 | 예시 |
|---|---|---|
| 변수/State | 새로고침하면 사라짐 | 입력 중인 폼 데이터 |
| LocalStorage | 브라우저 닫아도 유지 (그 기기에서만) | 다크모드 설정 |
| 데이터베이스 | 영구 저장 (어디서든 접근) | 회원 정보, 게시글 |
바이브코딩에서 가장 흔한 실수:
"저장했는데 새로고침하면 사라져요!"
이건 십중팔구 State에만 저장하고 DB에는 저장 안 해서 생기는 문제입니다.
Cursor에서 보이는 코드:
// ❌ State에만 저장 (새로고침하면 사라짐)
const [todos, setTodos] = useState([]);
const addTodo = (newTodo) => {
setTodos([...todos, newTodo]); // 메모리에만 저장
};
// ✅ DB에도 저장 (영구 보존)
const addTodo = async (newTodo) => {
setTodos([...todos, newTodo]); // 화면에 바로 반영
await supabase.from("todos").insert(newTodo); // DB에도 저장
};
문제가 생겼을 때 체크할 것:
-
새로고침하면 데이터 사라짐 → DB 저장 코드가 있는지
-
다른 기기에서 안 보임 → DB에 저장되고 있는지
-
저장은 되는데 불러오기가 안 됨 → 페이지 로드 시 DB에서 가져오는 코드가 있는지
( DB저장은 다음에 배울거에요. 지금 DB를 배우는 것은 너무 시기상조입니다. 일단 Local storage를 써서 최대한 구현을 해보세요! )
4단계: 표시 (UI)
저장된 데이터를 화면에 보여주는 단계입니다.
Cursor에서 보이는 코드:
// 데이터를 화면에 표시
return (
<div>
<h1>할 일 목록</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
todos.map(...)은 "todos 배열의 각 항목을 <li>로 변환해서 보여줘"라는 의미입니다.
문제가 생겼을 때 체크할 것:
-
데이터가 있는데 안 보임 →
map이나 표시 로직 확인 -
"Cannot read property of undefined" 에러 → 데이터가 아직 안 불러와졌는데 표시하려고 해서
흔한 해결책:
// 데이터가 없을 때 대비
{todos && todos.length > 0 ? (
todos.map((todo) => <li key={todo.id}>{todo.title}</li>)
) : (
<p>할 일이 없습니다.</p>
)}
클라이언트 vs 서버: 코드가 실행되는 두 장소
데이터 흐름을 이해하려면 코드가 어디서 실행되는지도 알아야 합니다.
한 줄 요약
| 구분 | 어디서 실행? | 뭘 담당? |
|---|---|---|
| 클라이언트 | 주로 사용자의 브라우저, 앱 | 화면 표시, 사용자 상호작용 |
| 서버 | 주로 원격 컴퓨터 | 데이터 저장, 민감한 처리 |
비유로 이해하기
레스토랑으로 비유하면:
-
클라이언트 = 손님이 앉은 테이블 (메뉴 보기, 주문하기)
-
서버 = 주방 (요리하기, 재료 보관)
손님(클라이언트)은 주방(서버)에서 무슨 일이 일어나는지 몰라도 됩니다. 그냥 주문하고 음식 받으면 됩니다.
왜 구분이 중요한가?
보안 때문입니다. 여러분, LLM을 쓰려고 OpanAI나 Gemini, 혹은 클라우드까지 이미 쓰고 계시다면, Azure, AWS, supabase, firebase 도 쓰고 계실건데요. 이 api key는 절대 노출이 되면 안됩니다. 실제로 자신의 로컬에 저장하는 것을 넘어서 이 자체를 cloud에 저장하는 방법도 있습니다. 여기서는 cloud에 이런 중요한 환경변수를 저장하는 법을 배우지는 않겠지만, 이 정도로 중요한 것이고, 노출이 되면 위험하다는 것을 알려드립니다.
// ❌ 위험: 클라이언트 코드에 API 키 노출
const apiKey = "sk-secret-key-12345"; // 누구나 볼 수 있음!
// ✅ 안전: 서버에서 처리
// 클라이언트는 서버에 요청만 하고,
// 서버가 API 키를 사용해서 처리
클라이언트 코드는 누구나 볼 수 있습니다 (브라우저에서 F12 누르면 보임). 그래서 비밀번호, API 키 같은 민감한 정보는 절대 클라이언트에 두면 안 됩니다.
Cursor 프로젝트에서 구분하기
my-frontend/
├── src/ # 클라이언트 코드
│ ├── components/
│ └── pages/
my-backend/
├── src/ # 서버 코드 (또는 서버리스 함수)
│ └── routes/
│ ...
└── .env # 환경 변수 (서버에서만 사용)
Next.js나 Remix 같은 프레임워크를 쓰면 한 프로젝트에 클라이언트/서버 코드가 같이 있습니다. 파일 위치나 이름으로 구분합니다.
만약 backend를 따로 분리한다면, 당연히 코드는 달라지겠죠?
실제 예시: 로그인 기능의 데이터 흐름
로그인 기능으로 전체 흐름을 따라가 보겠습니다.
[사용자] [클라이언트] [서버] [DB]
| | | |
| 1. 이메일/비번 입력 | | |
|--------------------------->| | |
| | | |
| | 2. 입력값 검증 | |
| | (빈칸인지 등) | |
| | | |
| | 3. 서버에 로그인 요청 | |
| |--------------------------->| |
| | | |
| | | 4. DB에서 사용자 조회 |
| | |----------------------->|
| | | |
| | | 5. 비밀번호 일치 확인 |
| | |<-----------------------|
| | | |
| | 6. 성공/실패 응답 | |
| |<---------------------------| |
| | | |
| 7. 결과 화면 표시 | | |
|<---------------------------| | |
각 단계에서 문제가 생기면:
| 단계 | 증상 | 원인 |
|---|---|---|
| 1-2 | 입력해도 반응 없음 | 이벤트 핸들러 문제 |
| 3 | "네트워크 오류" | API 주소 잘못됨, 서버 안 켜짐 |
| 4-5 | "사용자를 찾을 수 없음" | DB 연결 문제, 쿼리 오류 |
| 6 | 로그인 됐는데 화면 안 바뀜 | 응답 처리 로직 문제 |
| 7 | 페이지 이동하면 로그인 풀림 | 세션/토큰 저장 안 됨 |
실전 팁
데이터 흐름 디버깅하는 방법
문제가 생기면 각 단계마다 데이터를 확인하세요.
const handleSubmit = async () => {
console.log("1. 입력값:", email, password); // 입력 확인
const result = validateInput(email, password);
console.log("2. 검증 결과:", result); // 처리 확인
const response = await api.login(email, password);
console.log("3. 서버 응답:", response); // 서버 응답 확인
setUser(response.user);
console.log("4. 저장된 유저:", response.user); // 저장 확인
};
브라우저에서 F12 → Console 탭을 열면 console.log의 결과를 볼 수 있습니다. 어느 단계에서 데이터가 이상해지는지 찾으세요.
AI에게 데이터 흐름 설명 요청하기
새 프로젝트를 받거나 복잡한 기능을 이해해야 할 때:
"이 로그인 기능의 데이터 흐름을 설명해줘.
사용자가 로그인 버튼을 누르면 어떤 순서로 코드가 실행되고,
데이터가 어디로 이동하는지 단계별로 알려줘."
State vs Props 구분하기
React에서 자주 보는 두 가지 데이터 전달 방식:
-
State: 컴포넌트 내부에서 관리하는 데이터
-
Props: 부모 컴포넌트에서 받아오는 데이터
// State: 이 컴포넌트가 직접 관리
const [count, setCount] = useState(0);
// Props: 부모에서 받아옴
function ChildComponent({ title, onClose }) {
return <h1>{title}</h1>; // title은 부모가 정해줌
}
주의사항: 흔한 실수들
실수 1: State만 바꾸고 DB는 안 바꿈
// ❌ 화면에서만 바뀌고 새로고침하면 사라짐
const deleteItem = (id) => {
setItems(items.filter(item => item.id !== id));
};
// ✅ DB에서도 삭제
const deleteItem = async (id) => {
setItems(items.filter(item => item.id !== id)); // 화면 반영
await supabase.from("items").delete().eq("id", id); // DB 반영
};
실수 2: 데이터 로딩 전에 화면 그리려고 함
// ❌ 데이터 없을 때 에러 발생
return <div>{user.name}</div>; // user가 null이면 에러!
// ✅ 로딩 상태 처리
if (!user) return <div>로딩 중...</div>;
return <div>{user.name}</div>;
실수 3: 비동기 처리 이해 못함 ( 이건 지금 당장 이해 못해도 괜찮습니다. 다음에 배울거에요 )
데이터베이스 작업은 시간이 걸립니다. await를 빼먹으면 데이터가 오기 전에 다음 코드가 실행됩니다.
// ❌ 데이터 오기 전에 사용하려고 함
const data = supabase.from("users").select();
console.log(data); // 아직 데이터 안 옴!
// ✅ 기다렸다가 사용
const { data } = await supabase.from("users").select();
console.log(data); // 데이터 있음
마무리
모든 앱의 데이터는 이 흐름을 따릅니다:
입력 → 처리 → 저장 → 표시
그리고 이 과정은 클라이언트와 서버 사이를 오갑니다.
핵심 체크리스트:
-
입력이 안 됨 → 이벤트 핸들러(
onChange,onClick) 확인 -
처리가 이상함 → 로직 함수 확인
-
저장이 안 됨 → State만? DB까지?
-
표시가 안 됨 → 데이터 있는지, 로딩 처리 했는지
-
새로고침하면 사라짐 → DB에 저장했는지
이 흐름을 이해하면 "왜 안 되지?"라는 막막함에서 벗어나, "이 단계를 확인해봐야겠다"는 구체적인 방향을 잡을 수 있습니다.
Related Articles
Thank you for reading.