TL;DR
- UI는 데이터를 직접 가지지 않는다.
- 백엔드는 라우트 -> DB 조회 -> 템플릿 전달 순서로 테이터를 넘긴다.
- HTML 템플릿과 JS는 전달받은 데이터를 "렌더링 + 상호작용"으로 표현한다
프론트엔드 개발은 단순히 화면을 배치하는 것에서 끝나지 않습니다.
각 화면이 백엔드에서 전달받은 데이터를 자연스럽게 표현하고, 사용자가 원하는 기능을 직관적으로 수행할 수있도록 하는것이 프론트엔드의 진정한 역할입니다. 이 글에서는 저희 프로젝트에서 사용한 html구조, css레이아웃, js 상호작용, flask와 데이터 연동 방식을 중심으로, 어떤 구조로 서비스를 만들었는지 정리해보고자 합니다!
왜 UI-백엔드 연동 구조가 중요할까?
중고거래 플랫폼의 화면은 단순히 예쁘게 보이는 것만으로는 부족합니다. 사용자는 상품 검색 -> 상품 상세 페이지 접속 -> 위시리스트 -> 구매 -> 구매내역 확인 의 흐름을 경험합니다. 이 과정이 끊기지 않고 자연스럽게 이어지기 위해서는 백엔드에서 가져온 데이터를 읽기 좋게 정돈하는 UI, 화면 간 이동 흐름, 반복되는 패턴을 하나의 시스템으로 묶는 디자인 구조가 필수적입니다.
Background
이번 프로젝트의 화면 개발은 HTML, CSS, JS, Flask를 중심으로 이루어졌으며, 프론트엔드와 백엔드가 명확하게 역할을 나누어 협력하는 구조였습니다. 본 내용으로 들어가기 전, 이번 프로젝트에서 프론트엔드와 관련된 기술 일부에 대해 요약해보았습니다.
1. HTML
- HTML로 웹 페이지의 골격을 만들었습니다
- 화면들이 공통된 구조를 공유할 수 있도록 jinja 템플릿 구조를 활용하여 반복되는 UI를 모듈화 하였습니다.
- 화면 구조는 그대로 유지하면서 백엔드에서 넘어오는 데이터에 따라 다양한 정보를 렌더링 할 수 있게되었습니다.
2. CSS
- CSS로 디자인 시스템을 구축하였습니다.
- CSS는 사용자가 정보를 빠르게 이해하도록 돕는 역할도 하였습니다.
- section-wrapper, section-title처럼 페이지를 일정한 구역 단위로 나누어 여러 화면이 하나의 디자인 시스템을 공유하도록 했습니다.
- 상품/위시리스트/리뷰와 같은 반복형 UI는 css grid 개념을 사용하여 일정한 카드 형태로 배치했습니다.
3. JS
- JS를 통해 사용자의 활동에 따라 페이지가 즉각적인 반응을 하도록 구성하였습니다.
- 버튼 클릭시 active 변화
- 마이페이지 메뉴 전환
- 특정 부분만 화면 갱신
- 등의 기능을 구현하는데 사용되었습니다
4. Flask
- UI에 전달되는 모든 정보는 Flask 라우트에서 결정됩니다.
마이페이지 기능으로 살펴보는 FE-BE 구조
1) FE 템플릿 구조 (Jinja)
Jinja 템플릿을 사용해 현재 선택된 메뉴에 따라서 다른 메뉴 파일이 동적으로 로드되로록 하였습니다. 저희는 왼쪽 사이드 바와 우측 콘텐츠를 담아주는 구조를 만들어 놓았습니다.
<main class="mypage-container">
<!-- 왼쪽 사이드바 -->
<aside class="sidebar">
<ul>
<li class="{% if active_section == 'profile' %}active{% endif %}">
<a href="{{ url_for('user.mypage', section='profile') }}">회원정보</a>
</li>
<li class="{% if active_section == 'wishlist' %}active{% endif %}">
<a href="{{ url_for('user.mypage', section='wishlist') }}">관심 상품</a>
</li>
<li class="{% if active_section == 'review' %}active{% endif %}">
<a href="{{ url_for('user.mypage', section='review') }}">내 리뷰</a>
</li>
<li class="{% if active_section == 'buy' %}active{% endif %}">
<a href="{{ url_for('user.mypage', section='buy') }}">구매 내역</a>
</li>
<li class="{% if active_section == 'sell' %}active{% endif %}">
<a href="{{ url_for('user.mypage', section='sell') }}">판매 내역</a>
</li>
</ul>
</aside>
<!-- 우측 콘텐츠 -->
<section class="content-area">
{% if active_section == 'profile' %}
{% include 'mypage/_profile.html' %}
{% elif active_section == 'wishlist' %}
{% include 'mypage/_wishlist.html' %}
{% elif active_section == 'review' %}
{% include 'mypage/_myreview.html' %}
{% elif active_section == 'sell' %}
{% include 'mypage/_sellList.html' %}
{% elif active_section == 'buy' %}
{% include 'mypage/_buyList.html' %}
{% endif %}
</section>
</main>
포인트 정리
- active_section 값에 따라 <li>에 active 클래스를 부여 -> 현재 선택된 메뉴를 시각적으로 표시
- 같은 레이아웃에서 {% include %}로 부분 템플릿을 교체 -> 하나의 틀로 여러 화면을 재사용 할 수 있어 확장성과 유지보수성이 올라감
2) BE-Flask 라우트 & 데이터 전달
마이페이지는 /mypage/<section> 형태의 url을 사용하기 때문에, flask는 section의 값에 따라 필요한 데이터만 db에서 조회해 템플릿에 전달합니다.
if section == "wishlist":
wishlist = DB.get_products_by_ids(DB.get_my_heart_ids(user_id))
elif section == "review":
my_reviews = DB.get_reviews_by_purchaser(user_id)
elif section == "sell":
my_items = DB.get_items_by_seller(user_id)
elif section == "buy":
my_buys = DB.get_purchases_by_user(DB.get_uid_by_id(user_id))
else:
section = "profile"
위의 코드 처럼 section에 특정 값이 들어오면 flask가 그 값을 처리하여 템플릿 변수로 전달합니다. 즉, UI는 DB에 직접 접근하지 않고, 백엔드에서 미리 준비한 데이터를 받아 화면에 표시하는 역할만 담당하게 됩니다.
회원정보 수정 기능
회원정보 수정은 사용자가 직접 데이터를 입력하고, 서버에서 이를 검증-저장하는 마이페이지의 유일한 양방향 기능입니다. 단순히 화면을 렌더링 하는 수준을 넘어, FE 입력과 BE 검증이 단계적으로 이루어지는 구조이며, 데이터 검증, 비밀번호 암호화, UI 피드백이 함께 작동하는 대표적인 FE-BE 연동 구조입니다.
1. FE - 회원정보 수정 UI 구성
<div class="profile-card">
<form action="{{ url_for('user.update_profile') }}" method="post">
<div class="info-item">
<label for="email">이메일</label>
<input type="text" name="email" value="{{ user.email }}">
</div>
<div class="info-item">
<label for="password_new">새 비밀번호</label>
<input type="password" name="password_new" placeholder="8자 이상, 영문+숫자 포함">
</div>
<div class="button-group">
<button type="submit">수정하기</button>
</div>
</form>
</div>
.profile-card input:focus {
outline: none;
border-color: #2e6b56;
box-shadow: 0 0 4px rgba(46,107,86,0.3);
}
프로필 화면은 입력창, 라벨, 버튼 간격을 모두 통일하여 일관된 정보 경험을 제공하는데 중점을 두었습니다. 같은 info-item 레이아웃을 사용하여 모든 입력창 정렬/간격을 통일하였고, focus 할 시 시각적으로 강조되도록 디자인을 하여 UX를 강화하였습니다.
2. BE - 사용자 인증, 데이터 검증, DB 업데이트
회원정보 수정 요청은 /mypage/update 라우트로 전송되며, 백엔드는 다음과 같은 순서로 로직을 처리합니다.
- 세션으로 로그인 여부 확인
- 비밀번호 규칙 검증
- _hash_pw()를 사용한 비밀번호 암호화
- DBhander을 통한 데이터 업데이트
- flash 메세지로 결과 알림 후 마이페이지로 리다이렉트
@user_bp.route("/mypage/update", methods=["POST"])
def update_profile():
DB = current_app.config["DB"]
user_id = session.get("id")
if not user_id:
flash("로그인이 필요합니다.")
return redirect(url_for("login"))
new_password = request.form.get("password_new")
new_email = request.form.get("email")
if new_password:
if not PW_PATTERN.match(new_password):
flash("비밀번호는 8자 이상이며 영문+숫자를 포함해야 합니다.")
return redirect(url_for("user.mypage", section="profile"))
hashed = _hash_pw(new_password)
DB.update_user_password(user_id, hashed)
if new_email:
DB.update_user_email(user_id, new_email)
flash("회원정보가 수정되었습니다.")
return redirect(url_for("user.mypage"))
또한 비밀번호 변경시 사용자가 입력한 기존 비밀번호와 동일한지 확인하는 기능도 있습니다.
@user_bp.route("/check_password_match", methods=["POST"])
def check_password_match():
data = data.get("password")
user_id = session.get("id")
hashed_input = _hash_pw(input_password)
current_user = DB.get_user(user_id)
return jsonify({"match": hashed_input == current_user["pw"]})
비밀번호 확인 입력창은 사용자가 글자를 입력할때마다 Ajax로 백엔드에 검증 요청을 보내도록 구현되었습니다. 서버에서 해싱 비교를 수행한 뒤 결과를 JSOM으로 반환하고, FE는 이를 바로 UI에 적용하여 실시간 피드백을 제공합니다.
위의 설계를 통해 만든 프로젝트의 화면들


마무리
이번 블로그 포스팅을 통해 FE의 역할은 단순히 화면을 만드는 것이 아니라, 서로 다른기능들이 하나의 서비스처럼 보이도록 만드는 일의 중요성이었습니다. 초기에는 페이지마다 스타일이 조금씩 달라 UI가 분리되어 보였는데, 공통 규칙과 디자인 시스템을 정리하면서 서비스 전체가 자연스럽게 연결되는 경험을 할 수 있었습니다. 또한 HTML, CSS, JS 처럼 기본적인 기술만 사용했지만, 이 세가지로도 충분히 사용성을 고려한 인터페이스를 만들 수 있다는 점을 느꼈습니다. 특히 grid 레이아웃, 반응형 처리, 입력창 포커스 같은 작은 디테일이 전체 사용자 경험에서 큰 차이를 만드는 것을 체감했습니다. 총체적으로 단순한 화면 구성을 넘어, 서비스의 흐름을 설계하는 시각을 얻게되는 시간이었습니다.