이번 글에서는 Flask로 구현한 코드 소개와 제가 Next.js로 구현했던 기능을 어떤 식으로 구현했는지도 함께 설명해보려고 합니다.
글을 작성하다보면 리액트와 Jinja2를 비교하는 글이 될 것 같기도 합니다 :)
공통 레이아웃 만들기
Next.js에서 app/layout.tsx으로 공통으로 사용할 레이아웃을 만들듯, Jinja2에서도 어떤 페이지에서든 동일하게 사용할 레이아웃 템플릿을 만들 수 있습니다.
예를 들어, 아래 이미지에서 빨간색 박스로 표시한 사이드바를 모든 페이지에서 공통으로 사용하기로 했다면 jinja2로 다음과 같이 코드를 작성할 수 있습니다.

일단 먼저 templates 폴더에 layout.html 파일을 생성해줍니다.

그리고 layout.html 파일에는 어떤 페이지에서든 어디에서든 공통으로 사용할 코드를 추가해주고, 특정 페이지에서만 사용할 코드는 추가해주지 않습니다. 저는 제가 실제로 사용했던 코드의 일부를 예시로 가져와봤습니다.
아래 코드에서 우리가 주목해야할 점은 {% block content %} {% endblock %} 입니다. 이 부분을 통해서 우리는 공통 레이아웃은 유지한채 각기 다른 페이지에서 우리가 보여줄 다양한 컨텐츠 형태를 보여줄 수 있죠.
<!-- layout.html -->
<!doctype html>
<html lang="ko" data-theme="dark">
<head>
<meta charset="UTF-8" />
<title>Health Log with Flask</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/global.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/layout.css') }}" />
</head>
<body>
<div>
<aside class="sidebar">
<div class="sidebar-header">
<p class="sidebar-header-title"><i data-lucide="activity"></i>HealthLog</p>
<p class="sidebar-header-subtitle">with Flask</p>
</div>
<nav class="sidebar-nav">
<ul class="sidebar-nav-list">
<li class="sidebar-nav-item">
<a href="/" class="sidebar-nav-link">
<i data-lucide="house"></i><span>홈</span>
</a>
</li>
</ul>
</nav>
</aside>
<main class="main-content">
{% block content %}
{% endblock %}
</main>
</div>
<!-- Development version -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<!-- Production version -->
<script src="https://unpkg.com/lucide@latest"></script>
<script>
lucide.createIcons();
</script>
</body>
</html>
이렇게 layout.html을 가져와서 사용하는 방법은 다음과 같습니다.
1. 만약에 '홈' 이라는 페이지를 만든다고 하면, 마찬가지로 temlpates 폴더에 index.html을 생성해서 제일 상단에 {% extends "layout.html" %} 을 작성해줍니다. 이 코드의 의미는 layout.html을 상속해서 사용하겠다는 의미입니다.
즉, extends는 현재 템플릿이 다른 템플릿을 사용해서 확장하겠다는 의미로 보면 됩니다.
2. 그리고 {% block content %} 와 {% endblock %} 사이에 컨텐츠로 보여줄 코드를 작성해주면 됩니다. 완성된 코드는 아래와 같습니다.
<!-- index.html -->
{% extends "layout.html" %}
{% block content %}
<div class="page-container">
<h1 class="page-title">대시보드</h1>
<p class="page-description">건강 지표와 변화 추이를 모니터링하세요.</p>
<section class="latest-metrics-section">
<div class="card latest-metrics-card">
<div class="latest-metrics-header">
<h2 class="latest-metrics-header-title">최근 혈당 수치</h2>
<p class="latest-metrics-header-date" data-date="2025.10.31">
측정일: <span class="formatted-date"></span>
</p>
</div>
<div class="latest-metrics-content">
<div class="latest-metrics-description">
<div class="latest-metrics-description-value-container">
<span class="latest-metrics-description-value">100</span>
<span class="latest-metrics-description-unit">mg/dL</span>
<span class="latest-metrics-description-status latest-metrics-description-status-normal"
>정상</span
>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
참고로, app.py에서는 바꿀 코드가 전혀 없습니다. return render_template("index.html") 이라는 코드를 사용했다면 그대로 쓰면 됩니다.
이렇게 완성한 코드의 화면은 처음에 제가 빨간색 박스 표시와 함께 예시를 들었던 이미지를 보실 수 있을 겁니다. 단, 제가 전체 코드 중 설명에 필요한 코드만 추려내서 가져왔습니다.
Next.js 코드와 비교해보기
생각보다 쉽죠? next.js에서도 레이아웃 파일이나 컴포넌트를 생성했는데, jinja2도 크게 다르지 않는 방식으로 사용한다는 것을 배울 수 있었습니다.
Next.js app router 방식 기준으로 가볍게 비교를 해본다면, app 폴더 안에 layout.tsx 파일을 생성하고 jinja2에서 작성했던 것과 마찬가지로 공통으로 사용할 코드를 추가해줍니다.

이 때, {% block content %} {% endblock %} 역할은 {children}이 하게 됩니다. children을 통해 자식 컴포넌트를 렌더링을 하는 역할을 하는데 "공통 레이아웃 안에 각 페이지의 내용을 넣는다"는 관점에서 {% block content %} {% endblock %} 과 동일한 역할을 한다는 것을 알 수 있습니다.
다만, 렌더링 시점은 둘이 다르다는 차이점이 있습니다.
jinja2는 서버에서 렌더링이 끝난 HTML이 클라이언트에게 전달되어서 block에 표현될 내용이 서버에서 이미 정해진 상태이고, 리액트는 렌더링이 클라이언트 혹은 서버에서 실행될 수 있기 때문에 동적으로 state나 props에 따라 렌더링할 컨텐츠가 변경될 수 있습니다.
// layout.tsx
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
import { Sidebar } from '@/widgets/sidebar';
import { QueryProvider } from '@/providers/QueryProvider';
import { ModalProvider } from '@/shared/ui/Modal/ModalProvider';
import { ToastContainer } from 'react-toastify';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault('Asia/Seoul');
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: 'HealthLog',
description: '우리가족 혈당, 혈압 기록은 HealthLog 하나면 충분해요!',
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko" data-theme="dark">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<QueryProvider>
<ModalProvider>
<div className="flex h-screen">
<Sidebar />
<div className="pl-[255px]">{children}</div>
</div>
<ToastContainer />
</ModalProvider>
</QueryProvider>
</body>
</html>
);
}
활성 링크 표시하기
Next.js에서 useRouter 혹은 usePathname을 사용하여 현재 보고 있는 페이지를 활성화 상태로 표시했다면, Jinja2에서는 다음과 같이 표시할 수 있습니다.
1. 먼저 app.py 에 아래 코드를 추가해줍니다.
아래코드가 의미하는 건 다음과 같습니다.
- @app.context_processor : 모든 jinja 템플릿에서 접근할 수 있는 변수를 사용할 수 있도록 해주는 역할을 합니다. 템플릿 렌더링시 자동으로 실행되며, 반환된 값을 템플릿의 전역 컨텍스트에 추가해주죠.
한마디로, 템플릿을 렌더링 할 때 inject_current_path를 실행하여 currentPath를 템플릿의 전역 컨텍스트에 주입해준다고 생각하시면 됩니다.
@app.context_processor
def inject_current_path():
return {"currentPath": request.path}
위와 같이 코드를 작성하면 매번 render_template에 currentPath 데이터를 전달하지 않고도 모든 템플릿에서 currentPath를 바로 사용할 수 있게 되죠.
2. currentPath 사용 방법은 다음과 같습니다.
<nav class="sidebar-nav">
<ul class="sidebar-nav-list">
<li class="sidebar-nav-item">
<a href="/" class="sidebar-nav-link {% if currentPath == '/' %}active{% endif %}">
<i data-lucide="house"></i><span>홈</span>
</a>
</li>
</ul>
</nav>
{% if currentPath == '/' %}active{% endif %} 이쪽 코드를 중점적으로 살펴보면 되는데요,
전역으로 전달받은 값인 currentPath와 홈을 지칭하는 url인 '/'이 같다면, active 클래스를 추가하겠다는 의미입니다.
간단히 if 연산자를 사용했지만 아래와 같이 삼항 연산자를 사용해서 표현할 수도 있습니다.
<a href="/" class="sidebar-nav-link {{ 'active' if currentPath == '/' else '' }}">
<i data-lucide="house"></i><span>홈</span>
</a>
3. 이와 같은 과정을 거쳐서, 네비게이션 바에 활성 링크를 표시할 수 있습니다.
처음에는 Next.js에서 사용하던 방법이 익숙했다보니까 활성 링크를 구현하려면 자바스크립트 코드를 사용해야하나 하고 고민을 했는데 @app.context_processor를 통해 간단하고 쉽게 구현할 수 있었습니다.
Next.js 코드와 비교해보기
Next.js 코드와 비교를 해볼까요? Next.js는 usePathname 훅을 불러와서 구현할 수 있습니다.
중점적으로 볼 코드는 ${pathname === item.href ? 'text-(--text) bg-(--nav-active) ' : 'text-(--text-subtitle)'} 입니다. jinja2로 구현했을 때와 코드 문법이 다르다는 것 외에 큰 차이는 없죠? :)
활성 링크에서도 마찬가지로 Next.js와 Flask로 구현했을 때의 차이점은 렌더링 시점입니다.
Flask에서는 이미 서버에서 작업을 끝마친 데이터를 클라이언트에서 전달해주는 것과 달리, Next.js에서는 클라이언트 컴포넌트를 렌더링하는 시점에 usePathname() 훅을 통해 현재 경로를 가져옵니다.
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Activity, House } from 'lucide-react';
export function Sidebar() {
const pathname = usePathname();
const menuItems = [
{
label: '홈',
href: '/',
icon: <House />,
}
];
return (
<aside className="fixed top-0 left-0 w-3xs h-screen px-2 py-7 bg-(--background) border-r-1 border-r-(--divider)">
<div className="flex items-center gap-2 mb-10">
<Activity className="text-(--text)" />
<span className="text-2xl font-bold tracking-tight text-(--text)">HealthLog</span>
</div>
<nav className="mt-4">
<ul className="flex flex-col gap-1">
{menuItems.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className={`flex items-center gap-3 w-full text-(--text) text-base font-medium px-4 py-2 hover:bg-(--nav-hover) rounded-lg
${pathname === item.href ? 'text-(--text) bg-(--nav-active) ' : 'text-(--text-subtitle)'}
`}
>
{item.icon}
{item.label}
</Link>
</li>
))}
</ul>
</nav>
</aside>
);
}
마무리
Next.js와 Flask 코드를 비교하며 작성해봤는데 이 둘의 코드를 비교하며 작성하다보니, 어렴풋이 느꼈던 렌더링에 관한 둘 사이의 차이점을 더 명확히 알 수 있었습니다.
사실 Next.js을 사용하면서 생겼던 습관이 Flask에서 그대로 이어져와 코드를 작성할 때 자꾸 Next.js에서 코드를 작성했던 것 처럼 하려는 버릇이 있었습니다. 그러다보니 Next.js에서는 이러한 기능이 있는데, Flask에서는 이런 게 없나? js에서는 이런 문법을 쓰는데 python에서는 이런 문법을 어떻게 쓰지? 혹은 이렇게 써도 괜찮은 걸까? 하는 것들이 계속 있었죠.
변수명이나 함수명만해도 js에는 camel case로 작성하는데 python에서는 snake case를 네이밍 컨벤션으로 따른다고 해서 작성하다가 고치기를 반복했습니다.
새로운 프레임워크나 언어에 단 며칠만에 적응하는 게 쉽지 않았지만, 어떤 언어든 그간 공부했던 것들에서 큰 틀은 크게 벗어나지 않는다는 생각이 들었고 익숙한 환경에서 벗어나니 리프레쉬되는 시간이어서 좋기도 했습니다.
Flask와 관련된 글은 앞으로 몇 개의 글을 더 작성한 뒤 마무리할 예정입니다.
긴 글 읽어주셔서 감사합니다!
궁금한 점은 언제든 편하게 댓글 남겨주세요!ㅎㅎ
출처
jinja2 layout.html 예제 코드 - https://www.geeksforgeeks.org/python/templating-with-jinja2-in-flask/
jinja2 템플릿 상속 - https://flask-docs-kr.readthedocs.io/ko/latest/patterns/templateinheritance.html
'개발 지식 > Flask와 Jinja2' 카테고리의 다른 글
| Flask와 Jinja2로 개발하기 - (3) SCSS 사용하기 (0) | 2025.11.01 |
|---|---|
| Flask와 Jinja2로 개발하기 - (2) Prettier와 Black 설정하기 (0) | 2025.11.01 |
| Flask와 Jinja2로 개발하기 - (1) 설치 및 간단한 예제 실행하기 (0) | 2025.10.31 |