index.html)를 더블클릭하면 검색·필터·무한스크롤이 되는 운동 탐색기가 열리고,
또 다른 파일(setup.html)은 이 데이터를 내 백엔드로 옮기는 SQL·API 코드·LLM 프롬프트를 브라우저 안에서 만들어 준다.
"데이터셋을 제품처럼 포장하는 법"의 교과서다.
(저장소: hasaneyldrm/exercises-dataset · HTML/JSON · ★249 · 포크 45 · 교육·비상업 전용)
이 저장소가 대체 무엇인가.
이 저장소는 헬스·홈트 운동 1,324개를 정리한 데이터 모음이다. 각 운동은 이름, 대상 부위(가슴·등·하체 등), 필요한 기구(맨몸·덤벨·바벨 등), 주동근/협응근, 6개 언어 단계별 설명, 정지 썸네일(JPG), 동작 GIF까지 빠짐없이 들어 있다.
기술적으로 특이한 건 전달 방식이다. 보통 이런 데이터는 "JSON 파일 받아서 알아서 쓰세요"로 끝난다. 그런데 여기엔 데이터를 통째로 품은 단일 HTML 앱(index.html)과, 데이터를 내 서비스로 옮기는 작업을 자동화하는 개발자 온보딩 페이지(setup.html)가 함께 들어 있다.
즉 이 저장소는 세 덩어리다. ① 데이터(data/exercises.json) — 1,324개 운동의 모든 정보. ② 탐색기(index.html) — 그 데이터를 검색·필터·상세보기로 둘러보는 앱. ③ 온보딩 도구(setup.html) — 데이터를 DB에 넣고 API로 감싸는 코드를 자동 생성. 데이터셋 하나가 "받아쓰기 자료 + 데모 앱 + 통합 가이드"를 한 저장소에 다 갖춘 보기 드문 구성이다.
{ "이름": "값" } 형태로 적는 텍스트 형식. 사람도 읽고 거의 모든 언어(파이썬·자바스크립트·자바…)가 바로 불러올 수 있어, 프로그램끼리 데이터를 주고받는 공용어처럼 쓰인다.트렌딩 이유 · 경쟁 자료 대비 장점.
① 즉시 완성형 데이터 · ② 무서버·무빌드 데모 · ③ "데이터셋을 제품처럼" 포장한 온보딩, 이 셋이 이 저장소를 TrendShift 상위로 끌어올렸다. 운동 앱·홈트 서비스·AI 코치를 만들려는 사람에게 "가장 귀찮은 첫 단계(데이터 모으기)"를 통째로 건너뛰게 해 주기 때문이다.
| 비교 대상 | 한계 | exercises-dataset의 접근 |
|---|---|---|
| 운동 API (ExerciseDB 등) | 요청 수 제한·API 키·유료 플랜. 인터넷 끊기면 못 씀 | 정적 JSON+미디어라 무제한·오프라인. 한 번 받으면 끝 |
| 직접 스크래핑 | 사이트마다 구조 달라 수집·정제에 며칠 | 1,324개가 동일 스키마로 이미 정제됨 |
| 흩어진 운동 자료 | 설명만 있고 이미지/GIF 없음, 언어 1개 | 썸네일+GIF+6개 언어 설명을 레코드마다 포함 |
| 일반 데이터셋(CSV만) | "데모로 보여줄" 수단이 없어 감이 안 옴 | 더블클릭으로 열리는 탐색기 + DB/API 생성기 동봉 |
데이터 품질도 단단하다. 1,324개 레코드 전부가 13개 필드를 100% 채우고 있다 — 빈칸(null)이 없다. 설명은 영어·이탈리아어·터키어·스페인어·러시아어·중국어 6개 언어가 모두 들어 있고, 썸네일과 GIF도 1,324개씩 빠짐없이 매칭된다. "받아서 바로 화면에 꽂을 수 있는" 상태라는 뜻이다.
운동 앱을 만들려면 운동 목록·부위 분류·시범 이미지가 필요한데, 막상 모으려면 외부 API는 요청 제한과 과금이 걸리고, 직접 스크래핑은 법적·기술적으로 번거롭다. 많은 토이 프로젝트가 바로 이 "데이터 확보" 단계에서 멈춘다.
클론 한 번이면 1,324개 운동 데이터와 미디어가 손에 들어오고, index.html로 바로 둘러보고, setup.html로 DB·API 코드를 뽑아 내 서비스에 붙인다. "데이터 → 데모 → 통합"의 거리가 거의 0이다.
저장소는 "교육 및 비상업적 연구 목적"으로만 제공된다고 명시한다. 이미지·GIF는 각 원저작권자의 것이고 상업적 사용은 금지다(README의 Disclaimer·License). created_at이 전부 한 날짜(2026-03-18)인 점으로 보아 외부 출처에서 일괄 수집·정제된 집합물이다. 학습·프로토타입엔 훌륭하지만, 제품에 그대로 실으려면 미디어 저작권을 따로 해결해야 한다.
반전 — "프레임워크가 0개"라는 게 핵심이다.
화려한 스택 목록을 기대했다면 또 한 번 김이 샌다. 이 저장소의 코드는 의존성 0개다 — React도, 빌드 도구도, 차트 라이브러리도 없다. 1,324개를 검색·필터하는 앱이 순수 HTML·CSS·바닐라 자바스크립트만으로 돌아간다. GitHub가 매긴 언어 비율이 HTML 100.0%인 이유다.
| 레이어 | 기술 / 구성 | 역할 |
|---|---|---|
| 데이터(원본) | data/exercises.json | 9.3MB·1,324 레코드. 13개 필드 100% 채움. 단일 진실원(Single Source of Truth). |
| 탐색기 앱 | index.html (바닐라 JS) | 8.7MB. 데이터를 파일 안에 통째로 inline해 fetch() 없이 동작. 검색·필터·무한스크롤·상세 모달. |
| 온보딩 도구 | setup.html (바닐라 JS) | 60KB. 브라우저 안에서 SQL·다국어 API 코드·LLM 프롬프트를 생성. 외부 라이브러리 0. |
| 미디어 | images/ · videos/ | 썸네일 JPG 1,324개 + 동작 GIF 1,324개. 레코드의 image/gif_url이 상대경로로 가리킴. |
| 스타일 | 인라인 <style> + CSS 변수 | 디자인 토큰(--accent:#ff4f00 등)·CSS Grid 레이아웃. 외부 CSS 프레임워크 없음. |
| 빌드 / 번들 | 없음 | webpack·vite·npm 스크립트 부재. 파일을 그대로 열면 끝. |
| 서버 / DB | 없음 | 백엔드·데이터베이스 0. 필요하면 setup.html로 "내가" 만들어 붙이는 구조. |
fetch) 않고 HTML/JS 코드 안에 직접 박아 넣는 것. index.html은 9MB 데이터를 통째로 품어서, 인터넷·서버 없이 파일 하나만 열어도 완전히 동작한다. 대가는 파일이 무거워진다는 것(8.7MB)."JSON 한 덩어리"가 두 갈래로 쓰이는 길을 따라가기.
이 저장소의 심장은 exercises.json 하나다. 그 데이터가 두 방향으로 흐른다 — 왼쪽은 "보여주기"(탐색기), 오른쪽은 "넘겨주기"(내 백엔드로). 먼저 전체 그림을 한 장으로 보자.
핵심 설계는 "데이터는 한 곳, 쓰임은 여러 곳"이다. JSON 원본이 진실원이고, index.html은 그걸 화면으로, setup.html은 그걸 코드로 바꾼다. 이제 가장 대표적인 흐름 "탐색기에서 운동을 검색하고 상세를 여는" 한 줄기를 입력부터 화면까지 손으로 따라가 보자.
// index.html 의 동작 순서 (개념 발췌)
// ① 앱이 켜질 때: 데이터를 받고, 검색용 색인을 미리 만든다
state.exercises = EXERCISES;
state.exercises.forEach(ex => {
ex._idx = `${ex.name} ${ex.category} ${ex.target} ${ex.equipment} ${ex.muscle_group}`.toLowerCase();
}); // 검색할 때마다 만들지 않고, 한 번만 합쳐 둔다
// ② 사용자가 타이핑 → 250ms 멈추면 한 번만 실행(debounce)
searchEl.addEventListener('input', debounce(() => {
state.search = searchEl.value; applyFilters();
}, 250));
// ③ 필터 적용: 검색어 + 카테고리/기구/타깃(다중선택)을 모두 통과한 것만
state.filtered = state.exercises.filter(ex => {
if (q && !ex._idx.includes(q)) return false; // 미리 만든 색인에서 부분일치
if (category.size && !category.has(ex.category)) return false;
return true;
});
// ④ 화면엔 처음 60개만 그린다(페이지네이션)
const slice = state.filtered.slice(0, state.pageSize); // pageSize = 60
여기서 멈추지 않는다. 사용자가 스크롤을 내리면 화면 맨 아래의 보이지 않는 "감지선"이 뷰포트에 들어오고, 그걸 IntersectionObserver가 알아채 다음 60개를 이어 붙인다(appendNextPage). 카드에 마우스를 올리면 그제서야 무거운 GIF를 불러오고(mouseover → gif.src = data-src), 카드를 클릭하면 상세 모달이 열려 6개 언어 탭으로 단계별 설명을 보여 준다.
검색 색인(_idx)을 미리 만드는 건, 뷔페에서 모든 접시에 "치킨·매움·닭고기" 같은 이름표를 미리 붙여 두는 것과 같다. 손님이 "매운 거"를 찾을 때 접시를 하나씩 들춰 재료를 확인하는(매번 문자열을 합치는) 대신, 붙은 이름표만 슥 보면 된다. 무한 스크롤은 "접시를 한 번에 다 깔지 않고, 손님이 끝에 다다르면 다음 판을 내오는" 것 — 1,324개를 한꺼번에 그리면 브라우저가 버벅이니까.
| 패턴 | 설명 · 어디서 배우나 |
|---|---|
| 데이터 인라이닝 | 9MB JSON을 index.html에 통째로 박아 fetch·서버 없이 자기완결. 대가는 무거운 파일. |
| 사전계산 검색 색인 | 앱 시작 때 운동마다 _idx(합친 소문자 문자열)를 1회 생성 → 타이핑마다 부분일치만. 매번 재가공 안 함. |
| 디바운스(debounce) | 타이핑이 250ms 멈춰야 필터 실행. 글자마다 1,324개를 훑는 낭비를 막음. |
| 페이지네이션 + IntersectionObserver | 한 번에 60개만 렌더, 끝에 닿으면 다음 판 추가. DOM 폭증 방지. |
| 지연 로딩(lazy) + 이벤트 위임 | 썸네일은 loading="lazy", GIF는 hover 때만 로드. 클릭/hover는 그리드에 한 번만 건다. |
데이터를 inline한 덕에 "서버 없이 더블클릭으로 열림"을 얻었지만, 대신 ① 첫 로딩에 8.7MB를 통째로 받아 파싱하고, ② 데이터가 바뀌면 HTML을 다시 구워야 하며(데이터·표현이 한 파일에 묶임), ③ 검색은 includes() 단순 부분일치라 오타·동의어엔 약하다. 데이터 규모가 이 단순함이 감당할 선(수천 개)이라 성립하는 선택이다.
루트 파일 몇 개 + 미디어 폴더 둘 — 전부 본다.
| 경로 | 역할 요약 |
|---|---|
data/exercises.json | 저장소의 심장. 운동 1,324개가 동일 스키마(13필드)로. 모든 화면·코드가 결국 이 파일을 가리킨다. |
images/ | 각 운동의 정지 썸네일. 파일명은 {id}-{해시}.jpg. 레코드의 image 필드가 상대경로로 참조. |
videos/ | 동작 시범 GIF. 썸네일과 같은 베이스 이름({id}-{해시}.gif)이라 짝을 맞추기 쉽다. 레코드의 gif_url이 참조. |
index.html | 데이터를 품은 탐색기 SPA. 의존성 0. 더블클릭이면 끝. |
setup.html | "이 데이터를 내 서비스에 넣는 법"을 자동화한 페이지. 3개 탭(DB·API·LLM). |
README.md | 개요·통계·스키마·언어별 사용 예제. 단, 일부 수치가 실제 데이터와 다르다(아래 관찰). |
exercises.json은 1,324개다(저장소가 자란 뒤 설명만 안 고친 흔적). 또 README 스키마는 설명이 "영어·터키어 2개"라 하지만 실제 데이터엔 6개 언어(en·it·tr·es·ru·zh)가 들어 있다. 데이터셋을 쓸 땐 문서가 아니라 실제 파일을 직접 까 보는 습관이 중요하다는 좋은 사례.이 저장소에서 진짜로 배울 것 + 어디를 보면 되는지.
React 없이도 검색·다중필터·무한스크롤·모달이 다 된다. 상태 객체 하나(state)에 데이터/필터/페이지를 담고, "필터 → 슬라이스 → 렌더" 파이프라인으로 화면을 다시 그리는 전형적 패턴을 작은 코드로 읽을 수 있다. 프레임워크가 뒤에서 해 주던 일을 "직접" 보게 되는 게 핵심.
볼 파일: index.html의 applyFilters() → renderGrid() 흐름
검색이 느려지는 흔한 이유는 "글자 하나 칠 때마다 전체를 무겁게 재가공"하기 때문. 이 앱은 시작 때 운동마다 검색 문자열(_idx)을 한 번 만들어 두고(사전계산), 타이핑이 멈출 때만(디바운스) 부분일치를 돌린다. 두 기법의 교과서적 조합.
볼 파일: init()의 _idx 생성, debounce() 유틸
스크롤 위치를 직접 계산하지 않고, 화면 끝 "감지선"이 보이면 콜백이 도는 IntersectionObserver로 다음 페이지를 붙인다. 또 무거운 GIF는 카드에 마우스를 올릴 때만 로드해 데이터 낭비를 막는다. 이미지가 많은 갤러리/피드에 그대로 응용된다.
볼 파일: appendNextPage(), 그리드의 mouseover 핸들러
setup.html은 서버 없이 데이터로부터 CREATE TABLE + 1,324개 INSERT를 만들어 Blob으로 다운로드시킨다. DB 방언(MS SQL·PostgreSQL·MySQL·SQLite)마다 컬럼 타입과 따옴표 이스케이프가 다른 걸 코드로 처리하는 법을 배운다.
// setup.html — DB마다 다른 CREATE TABLE 템플릿(PostgreSQL 발췌)
CREATE TABLE exercises (
id VARCHAR(10) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
instructions_en TEXT, instructions_es TEXT, instructions_zh TEXT, // 6개 언어를 컬럼으로 평탄화
secondary_muscles TEXT, // 배열은 JSON 문자열로 저장
image VARCHAR(500), gif_url VARCHAR(500), created_at TIMESTAMP
);
볼 파일: buildInserts(), escStr(), generateInsertSql()
원본 JSON은 instructions.en처럼 언어를 중첩 객체로 담는다. 그런데 setup.html의 SQL 스키마는 이를 instructions_en, instructions_es… 처럼 컬럼으로 펼친다. "별도 번역 테이블로 정규화"하는 정석 대신 "컬럼 평탄화"를 택한 트레이드오프(조회 단순 ↔ 언어 추가 시 스키마 변경)를 비교해 보는 좋은 재료다.
볼 파일: JSON의 instructions 구조 vs setup.html의 CREATE TABLE
가장 시대를 타는 부분. setup.html은 프레임워크(Express·FastAPI·ASP.NET·Spring Boot·Laravel·Gin)와 DB를 고르면, "이 데이터로 완성형 REST API를 만들어 줘"라는 구조화된 프롬프트를 생성한다. 그대로 ChatGPT·Claude에 붙여 백엔드 초안을 받는 흐름이다. "문서 대신 프롬프트를 배포한다"는 새로운 온보딩 감각.
볼 파일: buildLlmPrompt(framework, db)
"보기"는 거의 0, "제품화"는 미디어 용량이 관건.
| 목적 | 필요한 것 |
|---|---|
| 탐색기 그냥 보기 | 최신 웹 브라우저뿐. index.html 더블클릭. 서버·인터넷 불필요(데이터가 inline). |
| 데이터만 활용 | 아무 언어나(파이썬·JS·자바…). json 파서로 exercises.json을 읽으면 끝. 1만 행 미만이라 메모리 부담 적음. |
| DB에 적재 | setup.html로 .sql 생성 → SQLite/PostgreSQL 등에 import. SQLite면 설치도 거의 불필요. |
| 앱/백엔드 구축 | 프레임워크 자유. setup.html의 API 코드·LLM 프롬프트를 출발점으로. |
| 미디어까지 호스팅 | 여기가 진짜 비용. JPG·GIF 합쳐 약 2,648개 파일(수백 MB급). 그대로 git에 두기보다 CDN이나 git LFS 고려. |
운동 데이터(텍스트)는 9MB라 깃털처럼 가볍게 어디든 들고 다닌다. 진짜 무게는 GIF·이미지(짐가방)다 — 수백 MB가 될 수 있어, 웹에 올릴 땐 이 짐을 어디(저장소? CDN? LFS?)에 둘지가 설계의 핵심이 된다. 텍스트와 미디어의 "이사 방법"을 분리해서 생각하는 게 요령.
데이터 만지기 → 화면 붙이기 → 백엔드 세우기 순으로.
저장소를 클론한 뒤 index.html을 더블클릭해 검색·필터를 체험한다. 그다음 exercises.json을 파이썬으로 읽어 "부위별 운동 수", "기구별 분포"를 출력해 본다. README의 "433개"가 아니라 실제로 몇 개인지 직접 세어 보는 게 포인트(스포일러: 1,324개).
# 부위(category)별 운동 개수 세기
import json, collections
data = json.load(open("data/exercises.json", encoding="utf-8"))
print("총 운동:", len(data))
print(collections.Counter(ex["category"] for ex in data))
기구가 body weight인 운동(325개)만 골라 무작위 6개를 뽑아 "오늘의 홈트 루틴"으로 출력하는 스크립트를 짠다. 부위가 겹치지 않게 고르는 조건을 붙이면 난이도가 살짝 올라간다. 데이터 필터링·랜덤 샘플링의 기본기 연습.
setup.html을 열어 SQLite 방언으로 .sql을 생성·다운로드하고, sqlite3 gym.db < exercises.sql로 적재한다. 그다음 "가슴 운동 중 덤벨만", "부위별 개수" 같은 쿼리를 직접 날려 본다. 생성된 스키마가 다국어를 어떻게 컬럼으로 폈는지 확인하는 게 핵심.
바닐라 JS 또는 React로 작은 앱을 만든다 — 인체 부위 버튼을 누르면 해당 category 운동을 카드로 보여주고, 카드를 누르면 GIF와 설명을 띄운다. 원본 index.html의 createCard()·openModal()을 참고하되 내 식으로 다시 써 보는 게 목표.
setup.html의 "Ask Your LLM"에서 FastAPI+SQLite 프롬프트를 뽑아 백엔드 초안을 받고, 거기에 ?category=chest&equipment=dumbbell&page=2 같은 필터·페이지네이션을 직접 구현한다. 이미지/GIF는 정적 파일로 서빙. "LLM 초안 → 내가 검증·보강"이라는 2026년형 개발 흐름을 체험한다.
offset = (page-1) * limit로 잘라 반환. 응답에 total·has_more를 같이 주면 프론트가 무한스크롤을 붙이기 쉽다."신뢰하되 검증하라". image/gif_url이 가리키는 파일이 실제로 images/·videos/에 있는지, ID 중복은 없는지, 6개 언어가 빠짐없이 채워졌는지 검사하는 스크립트를 만든다. 데이터셋을 제품에 넣기 전 반드시 거치는 "수입 검사" 과정을 직접 설계하는 실습.
한 주씩 따라가는 "데이터 → 프론트 → 백엔드 → 운영" 코스.
| 주차 | 주제 | 학습 자료 / 할 일 |
|---|---|---|
| 1주차 | JSON·데이터 모델 읽기 | exercises.json 스키마 분석, 파이썬/JS로 로드·집계(실습 1·2) |
| 2주차 | 바닐라 DOM — 검색·필터 | applyFilters() 따라 만들기, 다중선택 필터(Set) 이해 |
| 3주차 | 성능 — 디바운스·사전계산 | 색인 생성·debounce 직접 구현, 있고/없고 속도 비교 |
| 4주차 | 브라우저 API — 무한스크롤·lazy | IntersectionObserver·loading="lazy"로 이미지 갤러리 만들기 |
| 5주차 | SQL 적재·인덱싱 | setup.html로 .sql 생성, 인덱스 추가 후 쿼리 속도 측정(실습 3) |
| 6주차 | i18n 데이터 설계 | 컬럼 평탄화 vs 번역 테이블 정규화 비교, 언어 추가 시나리오 토론 |
| 7주차 | REST API 구축 | FastAPI/Express로 필터·페이지네이션 API(실습 5) |
| 8주차 | 미디어 파이프라인 + 저작권 | GIF 최적화·CDN/git LFS, 데이터셋 라이선스·저작권 점검 |
본문에 나온 용어 빠른 참조.
| 용어 | 의미 |
|---|---|
| 데이터셋 (Dataset) | 일정 형식으로 모은 데이터 묶음. 여기선 운동 1,324개의 JSON 배열. |
| JSON | { "키": "값" } 형태의 텍스트 데이터 형식. 거의 모든 언어가 바로 읽음. |
| 스키마 (Schema) | 데이터의 "정해진 형식". 어떤 필드가 어떤 타입으로 있는지의 약속(여기선 13개 필드). |
| 레코드 (Record) | 데이터 한 건. 운동 한 개 = 레코드 한 줄. |
| 바닐라 JS | 프레임워크 없이 쓰는 순수 자바스크립트. |
| SPA | Single Page Application. 페이지 새로고침 없이 JS로 화면을 바꾸는 앱. |
| 데이터 인라이닝 | 데이터를 외부에서 불러오지 않고 코드 안에 직접 박는 것. index.html의 9MB. |
| 디바운스 (Debounce) | 연속 이벤트(타이핑)가 잠잠해진 뒤 한 번만 실행하게 묶는 기법. |
| 검색 색인 (Index) | 빨리 찾으려고 미리 만들어 둔 보조 데이터. 여기선 합친 소문자 문자열 _idx. |
| IntersectionObserver | 어떤 요소가 화면에 보이는지 감지하는 브라우저 API. 무한스크롤·lazy 로딩에 씀. |
| 무한 스크롤 | 끝까지 내리면 다음 묶음을 이어 붙여 계속 보여 주는 UI 패턴. |
| 지연 로딩 (Lazy Loading) | 필요할 때(보일 때·hover 때)만 자원을 불러와 낭비를 줄이는 기법. |
| 이벤트 위임 (Delegation) | 자식마다 핸들러를 달지 않고 부모에 하나만 달아 이벤트를 처리. |
| i18n (국제화) | internationalization. 여러 언어를 지원하도록 만드는 것(여기선 6개 언어 설명). |
| 정규화 / 평탄화 | 다국어를 별도 테이블로 쪼개면 정규화, instructions_en처럼 컬럼으로 펴면 평탄화. |
| Blob / Object URL | 브라우저가 메모리의 데이터를 "파일처럼" 만들어 다운로드시키는 수단(setup.html의 .sql). |
| REST API | HTTP로 데이터를 주고받는 표준 방식. GET /exercises?category=chest 같은 형태. |
| 페이지네이션 | 큰 목록을 page·limit으로 나눠 조금씩 주는 것. |
| CDN | 전 세계에 파일을 빠르게 배포하는 네트워크. 무거운 미디어 서빙에 적합. |
| git LFS | Large File Storage. 큰 바이너리(이미지·GIF)를 git에서 따로 관리하는 확장. |