Back
ESSAY비개발자를 위한 바이브코딩 안내서
DEC 07, 2025

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>

여기서 핵심은 onChangeonClick입니다. 사용자가 뭔가를 하면 → 이 함수들이 실행됩니다.

문제가 생겼을 때 체크할 것:

  • 입력창에 타이핑하는데 글자가 안 써짐 → 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.jsRemix 같은 프레임워크를 쓰면 한 프로젝트에 클라이언트/서버 코드가 같이 있습니다. 파일 위치나 이름으로 구분합니다.

만약 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에 저장했는지

이 흐름을 이해하면 "왜 안 되지?"라는 막막함에서 벗어나, "이 단계를 확인해봐야겠다"는 구체적인 방향을 잡을 수 있습니다.


Thank you for reading.

Based in Seoul
Since 2024