반응형
m.logPrint() is working!
<eq> and <eqq> tags are rendered to MathJax format, being enclosed by \ ( \ ) and \ [ \ ].
docuK-1 scripts started!
If this log is not closed automatically, there must be an error somewhere in your document or scripts.
Table of Contents is filled out.
Auto numberings of sections (div.sec>h2, div.subsec>h3, div.subsubsec>h4), <eqq> tags, and <figure> tags are done.
<cite> and <refer> tags are rendered to show bubble reference.
<codeprint> tags are printed to corresponding <pre> tags, only when the tags exist in the document.
Current styles (dark/bright mode, font-family, font-size, line-height) are shown.
disqus.js with id="disqus-js" is loaded.
kakao.js with id="kakao-jssdk" is loaded.
New ShortKeys (T: Table of Contents, F: Forward Section, D: Previous Section, L: To 전체목록/[Lists]) are set.
m.delayPad=0;
m.wait=1024;
wait 1114ms.
<eq> and <eqq> tags are rendered to MathJax format, being enclosed by \ ( \ ) and \ [ \ ].
docuK-1 scripts started!
If this log is not closed automatically, there must be an error somewhere in your document or scripts.
Table of Contents is filled out.
Auto numberings of sections (div.sec>h2, div.subsec>h3, div.subsubsec>h4), <eqq> tags, and <figure> tags are done.
<cite> and <refer> tags are rendered to show bubble reference.
<codeprint> tags are printed to corresponding <pre> tags, only when the tags exist in the document.
Current styles (dark/bright mode, font-family, font-size, line-height) are shown.
disqus.js with id="disqus-js" is loaded.
kakao.js with id="kakao-jssdk" is loaded.
New ShortKeys (T: Table of Contents, F: Forward Section, D: Previous Section, L: To 전체목록/[Lists]) are set.
m.delayPad=0;
m.wait=1024;
wait 1114ms.







Mode: Bright; Font: Noto Sans KR; font-size: 18.0px (10.0); line-height: 1.6;
width: 1280, height: 720, version: 2.12.18
Canonical URI: https://kipid.tistory.com/entry/React-Router-에서-CSS-충돌을-막고-좀-더-개발-친화적으로-CSS-를-다룰-수-있게-해주는-CSS-module-을-배워봅시다-Learning-module-css
dg:plink (Document Global Permanent Link): https://kipid.tistory.com/371
document.referrer: Empty
width: 1280, height: 720, version: 2.12.18
Canonical URI: https://kipid.tistory.com/entry/React-Router-에서-CSS-충돌을-막고-좀-더-개발-친화적으로-CSS-를-다룰-수-있게-해주는-CSS-module-을-배워봅시다-Learning-module-css
dg:plink (Document Global Permanent Link): https://kipid.tistory.com/371
document.referrer: Empty







React Router 에서 CSS 충돌을 막고 좀 더 개발 친화적으로 CSS 를 다룰 수 있게 해주는 CSS module 을 배워봅시다. (Learning module.css)
Table of Contents
T1.React Router
▼ Show/Hide
T1.2.사용방법
import { BrowserRouter, Route, Routes } from 'react-router-dom'; import ReactDOM from 'react-dom/client'; import './root.css'; import CommonsPage from './pages/CommonsPage.js'; import HomePage from './pages/HomePage.js'; import ItemsPage from './pages/ItemsPage.js'; import RegisPage from './pages/RegisPage.js'; import LogInPage from './pages/LogInPage.js'; import SignUpPage from './pages/SignUpPage.js'; import NotFoundPage from './pages/NotFoundPage.js'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <Routes> <Route path="/" element={<CommonsPage/>}> <Route index element={<HomePage/>}/> <Route path="items" element={<ItemsPage/>}/> <Route path="registration" element={<RegisPage/>}/> <Route path="login" element={<LogInPage/>}/> <Route path="signup" element={<SignUpPage/>}/> </Route> <Route path="*" element={<NotFoundPage/>}/> </Routes> </BrowserRouter> );
와 같이 page 분기, routing 을 할 수 있다. Single Page Application (SPA) 인 셈.
그런데 한 페이지에서 여러 components 들을 다루다 보니 css 충돌 문제가 심각했다.
css file 의 loading 순서도 중요했고, page 마다
main {} section {}
의 css 가 달라져야 했는데, 각 페이지 별로 (혹은 component 별로) css file import 를 다르게 해줘도 SPA 이다 보니 css 파일이 어느 페이지를 들어가든 이미 다 load 된 상태가 되어버려서 뒤죽박죽이 된 듯 했다.▲ Hide
T2.module.css (CSS module 을 사용해보자.)
▼ Show/Hide
속도 측면에서도
CSS in js
보다 빠르다고 하고 [01]
, 계층적 구조와 semantic 하게 css 를 써야 좋을거 같아서 Ref. [01] 카카오웹툰은 CSS를 어떻게 작성하고 있을까?, 2022-02-10, by 서현우(bono)
module.css
를 사용해 보기로 했다.On the left side of codes is there a hiden button to toggle/switch scrollability ({max-height:some} or {max-height:none}).
/* ItemsPage.module.css */ .main { max-width: 1920px; padding: 20px 0; display: flex; flex-direction: column; justify-content: space-evenly; gap: 20px; background-color: white; } .section { max-width: 1200px; width: 100%; padding: 0; margin: 0 auto; } .items_head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; margin-bottom: var(--text-xl); } .items_head h2 { font-size: var(--text-xl); font-weight: 700; } ul.items { list-style-type: none; display: flex; justify-content: space-between; flex-wrap: wrap; margin: 0; padding: 0; } ul.items.normal { display: grid; grid-template: repeat(2, auto) / repeat(5, 1fr); gap: 10px; } ul.items li { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-start; gap: 8px; margin-bottom: 30px; & .name { font-size: var(--text-md); font-weight: 500; line-height: var(--text-2xl); } & .price { font-size: var(--text-lg); font-weight: 700; line-height: var(--text-2xl); } & .favorite_count { font-size: var(--text-xs); font-weight: 500; line-height: var(--text-2lg); & .heart { font-size: 1.3em; } } } ul.items.best li { width: 24%; } ul.items.normal li { width: 100%; } ul.items li img { width: 100%; aspect-ratio: 1 / 1; border-radius: 16px; } .input_wrapper { position: relative; height: 43px; width: 325px; } .input_wrapper img { position: absolute; cursor: pointer; left: 20px; top: 14px; bottom: 14px; width: 17px; z-index: 10; } .input_wrapper input { position: absolute; inset: 0; border-radius: 12px; padding: 9px 20px 9px 45px; background-color: var(--secondary-color-gray100); color: var(--secondary-color-gray400); z-index: 1; } .items_head_query { display: flex; align-items: center; justify-content: flex-end; gap: 12px; } .post_product { padding: 12px 23px; font-size: var(--text-lg); font-weight: 600; color: var(--secondary-color-gray100); background-color: var(--primary-color-100); border-radius: 8px; display: inline-block; } .select_order_by { padding: 12px 20px; background-color: white; font-size: var(--text-lg); font-weight: 400; color: var(--secondary-color-gray800); } .pagenation { display: flex; align-items: center; gap: 12.5px; margin: 0 auto 45px; justify-content: center; } .pagenation div { display: inline-flex; align-items: center; justify-content: center; width: var(--text-4xl); height: var(--text-4xl); padding: 12.5px; background-color: white; color: var(--secondary-color-gray500); font-size: var(--text-lg); font-weight: 600; border-radius: 9999px; border: 2px solid var(--secondary-color-gray200); } .pagenation div:hover { background-color: var(--primary-color-300); color: white; } .pagenation div.disabled { background-color: rgb(216, 216, 216); color: var(--secondary-color-gray200); } .pagenation div.selected { background-color: var(--primary-color-100); color: white; } @media (max-width:1200px) { .section { flex-direction: column; margin: 0 auto; padding: 0 24px; } .main { padding: 15px; } .input_wrapper { width: 242px; } ul.items.normal { grid-template: repeat(2, auto) / repeat(3, 1fr); } ul.items.best>li { width: 49%; } } @media (max-width:744px) { .section { margin: 0 auto; padding: 0 16px; } .main { padding: 10px; } .head_row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; margin: 6px 0; width: 100%; gap: 10px; } .head_row h2 { flex-grow: 1; } .head_row .input_wrapper { flex-grow: 1; width: auto; min-width: 90px; } .input_wrapper { flex-grow: 1; } ul.items.normal { grid-template: repeat(2, auto) / repeat(2, 1fr); } ul.items.best>li { width: 100%; } }
와 같이 일반 css 작성하듯 css 를 작성하고
ItemsPage.js
에서 다음과 같이 className 을 설정하자.On the left side of codes is there a hiden button to toggle/switch scrollability ({max-height:some} or {max-height:none}).
import { useEffect, useState, useCallback } from "react"; import styles from './ItemsPage.module.css'; // 이렇게 import 하자. import useAsync from "../hooks/useAsync.js"; import BestItemsList from "../BestItemsList.js"; import ItemsList from "../ItemsList.js"; import PageNum from "../PageNum.js"; function ItemsPage() { // ... codes ... return ( <main className={styles.main}> <BestItemsList bestItems={bestItems}/> <ItemsList items={items} orderBy={orderBy} setOrderBy={setOrderBy} keyword={keyword} setKeyword={setKeyword} onSearch={handleSearch}/> <PageNum pageNum={pageNum} setPageNum={setPageNum} pageNumMax={pageNumMax}/> </main> ); } export default ItemsPage;
T2.1.작동되는 원리
정확한건 아니지만 내가 써보면서 느낀 작동원리를 설명하자면,
.class-selector .sub-class-selector tagName
과 같은 css 가 module.css
에 작성되었다고 치면, compile 단계에서 이런 class-selector
, sub-class-selector
의 이름들을 FileName_class-selector__hashxxx
, FileName_sub-class-selector__hashxxx
로 변환한 채로 css file 이 upload (client 입장에선 download) 된다. 어차피 bundling 된 상태로 올라갈거라 다른 react component 의 같은 이름의 class selector 에 걸리지 말라고 prefix 로 FileName 을, 뒷쪽엔 postfix 로 hashxxx random 문자열이 추가되는거 같다.그리고 jsx 에서
className={styles[".class-selector"]}
를 불러오면 그냥 class-selector
가 들어가는게 아니라 FileName_class-selector__hashxxx
형태로 올라가는듯 하다. (class-selector 를 javascript 에서 변수로 불러 올 수 있게 항상 class_selector 처럼 underbar 같은걸로 대체해야 하는 줄만 알았는데, 아니었다. 그냥 styles.class-selector 로 못 쓸 뿐, styles[".class-selector"] 로 충분히 접근 가능했다. 이거 때문에 엄청 삽질을 했는데 ㅠㅜ 다른 분들은 저같은 삽질 하지 마시고 시간 아끼시길...)그래서 @media query 도 쓸 수 있고, nested CSS 도 사용 가능하고,
tagName0 .class0 ul li
같은 selector 도 정상 동작한다.헷갈리는 부분들은 댓글로 알려주시길... 댓글에서 코드 블록은
```
시작 ```/
마지막으로 감싸면 prettyprint 되어서 나옵니다. (티스토리도 댓글 수에는 제한을 둬놔서 긴 코드는 못쓸거에요. 최대한 축약해서 올리시길...)▲ Hide
TRefs.References and Related Articles
▼ Show/Hide
- Ref. [01] 카카오웹툰은 CSS를 어떻게 작성하고 있을까?, 2022-02-10, by 서현우(bono)
- Ref. [02] 내가 하면 더 잘 만들 것 같아서 만들어 본 세상 귀여운 on-demand Atomic CSS Library. -Part.1, 2022-05-20, by teo.yu
- Ref. [03] 리액트에서 모듈 CSS를 쓸 때 이모저모, 2024-09-12, by 코드잇 풀스택 2기 주강사 이창신
- Ref. [04] React로 웹사이트 만들기 (Router/라우터) - 코드잇:
https://recoeve.net/user/kipid/mode/multireco?cat=[IT/Programming]--국비 지원 코딩/공부#https://www.codeit.kr/topics/building-a-website-with-react
https://www.codeit.kr/topics/building-a-website-with-react
▲ Hide







반응형