2025 / 04 / 07
오늘 수업 시간에는 저번에 만든 간단한 블로그를 CSS 프레임워크인 Tailwind CSS를 사용하여 다시 만들어보았습니다. 이번 벨로그에서는 기술적인 부분 보다는 Tailwind CSS를 사용한 화면 구현 부분에 대해 이해하기 쉽도록 하나씩 정리해보도록 하겠습니다.
- 유틸리티 - 퍼스트 방식의 CSS 프레임워크입니다.
- 직접 클래스를 만들어서 스타일을 입히는 방식이 아니라, 이미 만들어진 아주 작고 재사용 가능한 클래스들을 조합해서 스타일을 입히는 방식입니다.
일반 CSS
<div className="card"> Hello </div> <style> .card { padding: 16px; background-color: #f3f4f6; border-radius: 8px; font-weight: bold; } </style>
- 클래스를 따로 만들지 않고, 이미 제공된 유틸리티 클래스들로 스타일을 지정합니다.
<div className="p-4 bg-gray-100 rounded font-bold"> Hello </div>
- 기능 단위로 쪼개진 CSS 클래스를 조합해서 사용하는 구조입니다.
- ex) bg-blue-500, text-white, rounded-md, p-4
- 스크린 사이즈별로 클래스를 지정할 수 있어서 반응형 디자인도 간단합니다.
<div className="text-sm md:text-base lg:text-lg"> 반응형 글자 크기 </div>
- Tailwind는 다크모드를 아주 간단하게 지원합니다.
// tailwind.config.js module.exports = { darkMode: 'class', // 'media' 도 가능 }<div className="bg-white dark:bg-gray-800 text-black dark:text-white"> 다크 모드 지원! </div>
- tailwind.config.js에서 테마를 쉽게 확장하거나 변경이 가능합니다.
theme: { extend: { colors: { brand: '#1DA1F2', } } }
1. 빠른 개발
- 컴포넌트를 작성하면서 바로 스타일을 추가할 수 있습니다.
2. 반응형 편함
- 클래스 이름으로 직접 반응형을 지정할 수 있습니다.
3. 유지보수 쉬움
- 글로벌 CSS 작성이 거의 없습니다.
4. 일관성 유지
- 전역 디자인 시스템 없이도 일관된 스타일을 사용할 수 있습니다.
5. 커스터마이징 쉬움
- 설정 파일로 확장이 가능합니다.(컬러, 폰트 등)
1. 클래스 길어짐
- 많은 스타일이 필요한 경우
<div>의 className이 너무 길어집니다.
2. 학습 곡선
- 포반엔 클래스 이름을 외우는 게 살짝 어려울 수 있습니다.
3. 추상화 어려움
- CSS의 의미적인 클래스(.card .button 등)를 잘 쓰는 사람에게는 어려울 수 있습니다.
4. 자동 완성 필수
- VSCode Tailwind IntelliSense 같은 플러그인이 없으면 생산성이 떨어집니다.
실전에서 자주 사용되는 Tailwind 유틸리티 클래스들 입니다.
| 스타일 | 클래스 예시 |
|---|---|
| padding | p-4, px-6, py-2 |
| margin | m-2, mt-4, mb-1 |
| color | m-2, mt-4, mb-1 |
| font-size | text-sm, font-bold, tracking-wide |
| layout | flex, grid, justify-center, items-center |
| 반응형 | sm:, md:, lg:, xl: 접두사 |
| 다크모드 | dark:bg-gray-800, dark:text-white |
- Tailwind CSS : 빠른 UI 구현과 유지보수에 강한 유틸리티 퍼스트 CSS 프레임워크입니다.
- React와 궁합이 좋고, 컴포넌트 기발에 최적화되어 있습니다.
- 단점도 있지만, 자동 완성 플러그인과 좋은 구조화 전략으로 극복할 수 있습니다.
- 설정 후에는 생산성이 향상되었다는 느낌을 받을 수 있습니다.
- 간단한 블로그 형식을 Tailwind CSS를 사용하여 구현해보았습니다.
- Tailwind CSS를 활용한 화면 UI가 구성되어있습니다.
전체 애플리케이션을 구성하는 중심 컴포넌트
- 사용자(user)와 포스트(post) 데이터를 API에서 불러옵니다.
- 상태 관리(useState, useRef, useEffect)를 하고있습니다.
- 검색 기능이 구현되어 있습니다.
- 컴포넌트의 렌더링을 제어합니다.(검색창 vs 포스트 리스트)
function App() { const [post, setPosts] = useState([]); const [user, setUser] = useState([]); const [isSearch, setIsSearch] = useState(false); const [search, setSearch] = useState(""); const inputRef = useRef(null); useEffect(() => { const getPosts = async () => { let url = "https://jsonplaceholder.typicode.com/posts"; if (search) { url += `?userId=${search}`; } const response = await fetch(url); if (response.ok) { const data = await response.json(); console.log(data); setPosts(data); } }; getPosts(); }, [search]); useEffect(() => { const getUsers = async () => { let userurl = "https://jsonplaceholder.typicode.com/users"; const response = await fetch(userurl); if (response.ok) { const data = await response.json(); setUser(data); } }; getUsers(); }, []); const userSearch = (e) => { if (e.key === "Enter") { const name = inputRef.current.value; const result = user.find((user) => user.username.toLowerCase().includes(name.toLowerCase()) ); if (result) { console.log(result); setSearch(result.id); setIsSearch(false); } else { setSearch(null); } } }; return ( <div className="flex flex-col py-6 px-30"> <Header setIsSearch={setIsSearch} /> {isSearch && ( <Input ref={inputRef} userSearch={userSearch} search={search} /> )} {!isSearch && ( <main className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"> {post.map((item) => ( <Card post={item} key={item.id} /> ))} </main> )} </div> ); } export default App;
- useState와 useRef를 사용하여 각각의 상황에 맞게 상태를 관리합니다.
const [post, setPosts] = useState([]); const [user, setUser] = useState([]); const [isSearch, setIsSearch] = useState(false); const [search, setSearch] = useState(""); const inputRef = useRef(null);
- search 값이 바뀔 때 마다 API를 요청합니다.
- 해당 유저의 포스트만 필터링해서 가져올 수 있습니다.
useEffect(() => { const getPosts = async () => { let url = "https://jsonplaceholder.typicode.com/posts"; if (search) { url += `?userId=${search}`; // 유저 ID로 필터링 } const response = await fetch(url); if (response.ok) { const data = await response.json(); setPosts(data); } }; getPosts(); }, [search]);
- 컴포넌트 마운트 시 1번만 실행됩니다.
- 검색 시 username -> userId로 매핑하기 위해 user 데이터를 미리 받아옵니다.
useEffect(() => { const getUsers = async () => { const response = await fetch("https://jsonplaceholder.typicode.com/users"); if (response.ok) { const data = await response.json(); setUser(data); } }; getUsers(); }, []);
- Enter 키를 누르면 실행됩니다.
- 입력값과 유저 목록을 비교합니다.(소문자로 통일해서 대소문자를 무시)
- 일치하는 유저를 찾으면 search에 해당 ID를 저장합니다.(포스트 필터링)
- 일치하는 유저가 없으면 null을 저장합니다.
- null 값 저장 후에는 "유저가 존재하지 않습니다" 메시지를 표시합니다.
const userSearch = (e) => { if (e.key === "Enter") { const name = inputRef.current.value; const result = user.find((user) => user.username.toLowerCase().includes(name.toLowerCase()) ); if (result) { setSearch(result.id); setIsSearch(false); } else { setSearch(null); } } };
- isSearch === true → 검색 input 표시
- isSearch === false → 카드 리스트 표시
<Header setIsSearch={setIsSearch} /> {isSearch && <Input ref={inputRef} userSearch={userSearch} search={search} />} {!isSearch && ( <main className="grid ..."> {post.map((item) => ( <Card post={item} key={item.id} /> ))} </main> )}
상단 헤더 및 검색 버튼
- 앱 제목을 표시합니다.
- 검색 버튼(돋보기 아이콘)을 클릭 시 isSearch 상태를 토글합니다.
- 아이콘 클릭할 때마다 검색창이 열리고 닫힙니다. (true ↔ false)
- Tailwind로 아이콘에 커서 스타일이 추가되어있습니다.
<FontAwesomeIcon icon={faMagnifyingGlass} className="cursor-pointer" onClick={() => setIsSearch((prev) => !prev)} />
검색 입력창 컴포넌트
- 사용자가 입력한 텍스트로 유저를 검색합니다.
- 검색을 실패하면 에러 메시지를 출력합니다.
- forwardRef를 사용해 부모 컴포넌트(App)에서 ref를 전달받습니다.
- Enter 입력 시 userSearch 실행됩니다.
- search === null이면 검색 실패 메시지를 보여주게 됩니다.
<input type="text" ref={ref} onKeyUp={userSearch} placeholder="검색어를 입력해주세요!" className="border rounded px-3 py-2 focus:outline-none focus:ring" /> {search === null && ( <div className="py-10"> 유저가 존재하지 않습니다.</div> )}
포스트 하나를 카드 형태로 출력하는 역할
- 포스트 제목과 본문을 시각적으로 보여줍니다.
- truncate, line-clamp을 활용해 텍스트 길이 제한합니다.
<div className="font-bold text-xl truncate pb-4">{post.title}</div> <div className="line-clamp-3">{post.body}</div>
1. search 값은 userId
- 검색창에서 username입력 -> userId로 변환하여 사용
2. useRef 사용 이유
- input의 현재 값을 가져오기 위함(제어 컴포넌트는 아닙니다.)
3. forwardRef 쓰는 이유
부모 컴포넌트(App)에서 input의 값을 직접 가져오기 위함
4. line-clamp-3 사용 조건
- Tailwind plugin 설치 필요
5. 검색 실패 시 search === null 처리
- null은 API 호출 안 됨 → 따로 메시지 처리 필요
- 위에서 살펴본 예제는 Tailwind CSS를 활용해 전체 UI가 구성되어있습니다.
- CSS 파일을 따로 작성하지 않고, HTML 클래스 안에서 스타일을 직접 정의하였습니다.
전체 레이아웃 & 구조
- 전체적으로 보면 상단에 Header → 아래 Input or Cards를 순서대로 배치하는 구조입니다.
<div className="flex flex-col py-6 px-30">
flex
flex-col
py-6
px-30
상단 바 Tailwind CSS
<header className="flex justify-between items-center pt-3 pb-3">
flex
justify-between
items-center
pt-3 pb-3
검색 버튼 Tailwind CSS
<FontAwesomeIcon className="cursor-pointer" />
cursor-pointer
검색창 스타일
<input className="border rounded px-3 py-2 focus:outline-none focus:ring focus:ring-violet-400 z-10" />
border
rounded
px-3 py-2
좌우 0.75rem / 상하 0.5rem 패딩을 적용합니다.focus:outline-none
focus:ring
focus:ring-violet-400
z-10
포스트 하나의 카드 UI-1
<div className="shadow-md w-full">
shadow-md
중간 정도의 그림자 효과를 적용합니다.w-full
포스트 하나의 카드 UI-2
<div className="font-bold text-xl truncate pb-4">
font-bold
text-xl
truncate
pb-4
포스트 하나의 카드 UI-3
<div className="line-clamp-3">{post.body}</div>
line-clamp-3
포스트 리스트의 그리드 레이아웃
- Tailwind의 반응형 디자인은
sm:,md:,lg:,xl:,2xl:같은 접두사로 사이즈별 반응형 스타일을 쉽게 적용할 수 있습니다.<main className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
grid
gap-4
grid-cols-1
md:grid-cols-2
lg:grid-cols-3
xl:grid-cols-4
2xl:grid-cols-5
Tailwind CSS에서 제가 자주 사용되는 클래스들을 정리한 내용입니다.
| 상황 | 추천 클래스 |
|---|---|
| 전체 구조 정렬 | flex, flex-col, grid, justify-between, items-center |
| 여백 설정 | p-, m-, py-, px- 등 |
| 타이포그래피 | text-, font-, truncate, line-clamp-* |
| 스타일 요소 | rounded, shadow-*, border, ring |
| 반응형 처리 | md:, lg:, xl: 같은 prefix |
| 사용자 상호작용 | hover:, focus:, cursor-pointer 등 |
63일차 후기
- Tailwind CSS를 활용해 구조, 스타일, 반응형까지 모두 클래스 기반으로 처리했습니다.
- 별도의 CSS 작성 없이 UI를 빠르게 구성할 수 있었고, 특히 반응형 처리와 포커스 스타일, 텍스트 클램핑(line clamp) 같은 기능들이 눈에 띄게 편리했습니다.
- Tailwind를 사용하면 디자인과 기능을 한눈에 파악할 수 있어 좋은 것 같습니다.
- CSS의 선택자를 잘 사용하던 입장으로써.. div 태그가 길어지는 것은 보기 싫었습니다.
- 그래도 처음에만 어떤 클래스를 사용할지 헷갈리지.. 후에는 좋은 것 같습니다.
- 버튼 태그 같은 경우는 꾸미기 전까지는 일반 CSS와 다르게 기본 UI없이 글자만 화면에 보여지기 때문에 일반 텍스트 태그들과 착각하지 않도록 조심해야할 것 같습니다. 실습문제를 풀어보던 도중에 버튼 태그로 만들고.. 못 찾은적 있습니다..( ง⁼̴̀ω⁼̴ )ง⁼³₌₃