요구사항 정의
구현해야 할 기능
Button
- styled-components를 이용해 구현하며, props를 적절하게 활용할 것
- 버튼 label에 선택적으로 아이콘을 넣을 수 있도록 구현
Modal
- 취소, 확인이 있고, overlay 클릭 시 모달이 닫히지 않음
- 닫기 버튼만 있고, overlay 클릭 시 모달이 닫힘
- 모달을 on 시키는 버튼의 형태는 각각 달라야 하며 위에서 만든 버튼 재사용 가능
Inpnut
- 일반 형식의 Input
- 숫자를 넣었을 때 3자리 숫자마다 ,가 찍히는 input
- form을 구현하고 각 inpnut에 값 입력 후 저장 버튼 클릭 시 { name: '아무 텍스트', price: '콤마가 없는 금액' } 을 alert에 표시
Select
- select 클릭 시 option들이 나오고 해당 option 클릭 시 select 값 변경
- 부모 요소에 overflow: hidden이 있더라도 자식 컴포넌트가 부모 컴포넌트를 넘어갔을 때 가려지지 않아야 함
컴포넌트 분리 및 역할 분담
📦src
┣ 📂components
┃ ┣ 📜Button.jsx : Button을 모듈화한 컴포넌트 (최종 작업물 기준 본인 담당)
┃ ┣ 📜Buttons.jsx : 버튼을 불러오기 위한 컴포넌트
┃ ┣ 📜Input.jsx : Input 컴포넌트
┃ ┣ 📜Modal.jsx : Modal 컴포넌트
┃ ┗ 📜Select.jsx : Select 컴포넌트 (최종 작업물 기준 본인 담당)
┣ 📜App.jsx
┣ 📜index.js
┗ 📜reportWebVitals.js
중점을 둔 사항
Button
function Button({ size, color, icon, onClick, children }) {
const Button = styled.div`
${() => colorHandler(color)};
${() => sizeHandler(size)};
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 13px;
cursor: pointer;
&:active {
filter: brightness(70%);
}
`;
const colorHandler = (color) => {
switch (color) {
case 'primary':
return `border: 3px solid #55EFC4; background-color: #55EFC4`;
case 'negative':
return `border: 3px solid #FAB1A0; color: #D63031; background-color: #FAB1A0`;
}
};
const sizeHandler = (size) => {
switch (size) {
case 'large':
return `width: 185px; height: 45px; background-color: white; font-weight: bold;`;
case 'medium':
return `width: 120px; height: 40px;`;
case 'small':
return `width: 90px; height: 35px;`;
}
};
return (
<>
<Button
onClick={onClick}
>
{ children } { icon }
</Button>
</>
)
}
export default Button
버튼 구현 시 props 키워드에 따라 세 가지 사이즈, 두 가지 색상을 선택해서 버튼을 가져올 수 있도록 구현했다.
Select
function Select() {
const languageList = ['리액트', '자바', '스프링', '리액트네이티브'];
const [language, setLanguage] = useState('리액트');
const [showList, setShowList] = useState(false);
const toggleShowList = () => setShowList(!showList);
const liClickHandler = (index) => {
setLanguage(languageList[index]);
toggleShowList();
};
const selectWrapRef = useRef();
useEffect(() => {
const clickListOutside = (e) => {
if (selectWrapRef.current && !selectWrapRef.current.contains(e.target)) {
toggleShowList();
}
};
document.addEventListener('mousedown', clickListOutside);
return () => {
document.removeEventListener('mousedown', clickListOutside);
};
}, []);
return (
<>
<Wrap>
<h1>Select</h1>
<SelectButton
onClick={toggleShowList}
>
{language}
<FontAwesomeIcon icon={faCaretDown} />
</SelectButton>
{
showList &&
<div
ref={selectWrapRef}
>
<LanguageUl>
{
languageList.map((item, index) => {
return (
<LanguageLi
key={index}
onClick={() => liClickHandler(index)}
>
{ item }
</LanguageLi>
)
})
}
</LanguageUl>
</div>
}
</Wrap>
</>
)
}
const Wrap = styled.div`
border: 3px solid lightgrey;
margin-top: 30px;
overflow: hidden;
`;
const SelectButton = styled.button`
width: 245px;
height: 40px;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: white;
border: 1px solid lightgrey;
border-radius: 10px;
cursor: pointer;
`;
const LanguageUl = styled.ul`
width: 245px;
height: 40px;
margin: 0;
padding-left: 0;
list-style: none;
position: absolute;
`;
const LanguageLi = styled.li`
height: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
border-left: 1px solid lightgrey;
border-right: 1px solid lightgrey;
font-size: 13px;
cursor: pointer;
&:hover {
background-color: lightgrey;
border-radius: 5px;
}
&:first-child {
border-top: 1px solid lightgrey;
border-radius: 5px;
}
&:last-child {
border-bottom: 1px solid lightgrey;
border-radius: 5px;
}
`;
export default Select
예시 화면에서 select 기본 값이 리액트로 되어있었기 때문에 useState의 기본값을 '리액트'로 주었다. 이후 사용자가 클릭한 각 li 값을 감지하여 노출할 state값을 변경해 주었다.
부모 요소에 overflow: hidden이 있더라도 자식 컴포넌트가 가려지지 않도록 position: absolute값을 주었고, 사용자가 select 외부의 값을 클릭했을 때 select box가 닫히도록 true, false값과 useEffect를 활용하여 mousedown을 감지하는 이벤트를 설정했다.
Input
function Input(props) {
const [name, setName] = useState('');
const [price, setPrice] = useState(0);
const nameChangeHandler = (e) => setName(e.target.value);
const priceChangeHandler = (e) => {
const priceNum = Number(e.target.value.replace(/,/g, ''));
if (!Number.isNaN(priceNum)) {
setPrice(priceNum.toLocaleString());
}
}
const saveBtnClickHandler = (e) => {
e.preventDefault();
if(name === '') {
alert(`이름과 가격 모두 입력해주세요.`);
return;
}
alert(`{ name: ${name}, price: ${price.replace(/,/g, '')} }`);
}
return (
<>
<h1>Input</h1>
<section>
<div>
<Form>
<label htmlFor="name">
이름
<UserInput
type="text"
id="name"
value={name}
onChange={nameChangeHandler}
/>
</label>
<label htmlFor="price">
가격
<UserInput
type="text"
id="price"
value={price}
onChange={priceChangeHandler}
/>
</label>
<SmallBtn
onClick={saveBtnClickHandler}
color={props.color}
>
저장
</SmallBtn>
</Form>
</div>
</section>
</>
)
}
const Form = styled.form`
display: flex;
gap: 30px;
`;
const UserInput = styled.input`
width: 180px;
height: 40px;
padding: 0 10px;
border: 1px solid black;
border-radius: 10px;
`;
export default Input
사용자가 금액 input에 isNaN이 true일 경우만 setPrice를 하면서 toLocaleString()을 이용해 세 자리 수마다 , 를 찍어 화면에 출력시켰다. 또한 e.target.value값을 감지할 때는 replace로 , 를 제거해 값을 가져올 수 있도록 했다.
Modal - Quote
function ModalQuote(props) {
return (
<BackgroundDiv>
<Section>
<p>
닫기와 확인 버튼 2개가 있고,
외부 영역을 눌러도 모달이 닫히지 않아요.
</p>
<BtnAreaDiv>
<SmallBtn
color={props.color}
onClick={props.toggleModalQuote}
>
닫기
</SmallBtn>
<SmallBtn>
확인
</SmallBtn>
</BtnAreaDiv>
</Section>
</BackgroundDiv>
)
}
const BackgroundDiv = styled.div`
width: 100%;
height: 100%;
top: 0;
left: 0;
position: fixed;
background-color: rgba(228,228,228,0.8);
`;
const Section = styled.section`
width: 460px;
height: 260px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 15px;
`;
const BtnAreaDiv = styled.div`
display: flex;
justify-content: flex-end;
gap: 5px;
`;
export default ModalQuote
Modal - Single
function ModalSingle(props) {
const divNode = useRef();
const divClickHandler = (e) => {
if(e.target === divNode.current) {
props.toggleModalSingle();
}
}
return (
<BackgroundDiv
ref={divNode}
onClick={divClickHandler}
>
<Section>
<p>
닫기와 확인 버튼 1개가 있고, <br />
외부 영역을 누르면 모달이 닫혀요.
</p>
<CloseButton
onClick={props.toggleModalSingle}
>
X
</CloseButton>
</Section>
</BackgroundDiv>
)
}
const BackgroundDiv = styled.div`
width: 100%;
height: 100%;
top: 0;
left: 0;
position: fixed;
background-color: rgba(228,228,228,0.8);
`;
const Section = styled.section`
width: 300px;
height: 200px;
padding: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 15px;
`;
const CloseButton = styled.button`
width: 30px;
height: 30px;
margin-top: 10px;
margin-left: 10px;
border: none;
border-radius: 50%;
cursor: pointer;
`;
export default ModalSingle
모달에서 모달 외부영역 클릭 시 모달을 닫는 토글 이벤트가 실행되도록 다음과 같이 코드를 구성했다.
const divNode = useRef();
const divClickHandler = (e) => {
if(e.target === divNode.current) {
props.toggleModalSingle();
}
}
1. 모달 외부에 레이아웃을 하나 깔아두고 그곳에 ref를 걸어준다.
2. ref를 건 영역에 onClick 이벤트를 주고
3. 클릭된 공간이 바깥 공간이 맞으면 조건문으로 토글 확인하여 모달 토글 함수를 실행한다.
[리팩토링 후(최종 제출본)] https://github.com/hansololiviakim/react-styled-components-after
[리팩토링 전(최초 작성본)] https://github.com/hansololiviakim/react-styled-components-before
트러블슈팅
section 태그 안에서 h1의 크기가 작아지는 문제
브라우저의 디폴트 스타일 시트값 때문이다. h1 태그를 꼭 section 태그 안에 넣어야 하는 상황이 아니었기 때문에 section 태그 외부에서 h1 태그를 사용했다. 추후에는 reset.css를 이용해야겠다.
배경 색에만 opacity 효과주되 자식 요소에는 영향이 없도록 지정하는 법
방법 1. rgba로 배경 색에만 색상을 조정한다.
background-color: rgba(228,228,228,0.8);
방법 2. CSS의 ::after 가상 선택자를 사용한다.
질의사항
기술매니저님과의 질의
❓ 버튼들이 있는 Button.jsx에서 휴먼 에러를 줄이기 위해 단어들을 배열에 넣고 배열의 인덱스를 props로 내려주었는데 이렇게 하는 게 오히려 불편한 방법인지 궁금하다. 또한 지금과 같이 버튼을 컴포넌트화해서 사용하는 상황에서 프로젝트 규모가 커졌을 때는 어떻게 props를 내려주는지 궁금하다.
💡 (코드 리뷰 대기 중)
❓ select 영역 구현 시 select box 외부의 요소를 클릭했을 때 닫힐 수 있도록 useEffect와 addEventListener의 mousedown으로 구현했다. DOM을 직접적으로 조작하는 방법은 좋지 않다고 들었는데 현업에서도 이러한 방법을 사용하는지 다른 방법을 사용하는 게 좋을지 궁금하다.
💡 (코드 리뷰 대기 중)
❓ 리액트에서 숫자 세 자리마다 , 를 찍어 사용자에게 보여줄 때 useState로는 toLocaleString을 걸고, DB에 데이터를 저장할 때는 replace 등으로 , 를 떼고 DB에 저장하는지 아니면 , 가 붙어있는 상태로 DB에 저장하는지 궁금하다.
💡 (코드 리뷰 대기 중)
✅ 이번 과제에서는 4개의 요소를 2개씩 나눠 구현해 보고, 다음날 서로 안해본 요소 2개를 바꿔서 구현해보고, 코드리뷰를 한 다음 최종적으로 제일 좋다고 생각하는 코드를 제출하는 형식으로 진행했다. 단순 CSS 요소만 사용하는 과제인 줄 알고 금방 끝날 것이라 생각했는데 버튼을 props를 이용해 컴포넌트화 하는 것이나, 리액트 hook을 사용해 화면을 구현할 수 있도록 생각해 보는 시간이 생겼다.
스타일드 컴포넌트와는 전보다 좀 친해진 기분이 들지만 아직도 가독성 측면에서는 그렇게 좋아 보인다는 생각이 안 들어서 스타일드 컴포넌트가 어떤 장점이 있는지 찾아보았다. 스타일드 컴포넌트를 사용하는 것의 장점은 다음과 같다.
1. 컴포넌트 기반 스타일링
👉🏼 스타일드 컴포넌트 사용 시 컴포넌트와 스타일을 함께 작성할 수 있어 코드의 가독성과 유지보수성을 높인다.
2. 동적 스타일링
👉🏼 props, 상태 등과 같은 리액트의 기능을 활용해 동적 스타일링을 할 수 있다.
3. 스타일 유지 보수 용이성
👉🏼 스타일을 작성하고 적용하는 데 필요한 모든 것을 한 곳에서 처리할 수 있다. 이때 스타일 코드의 중복을 줄이고 유지보수성을 향상시킬 수 있다.
4. 스타일의 범위 제한
👉🏼 스타일드 컴포넌트를 사용하면 스타일이 전역적으로 적용되지 않도록 제한할 수 있다. 이는 스타일의 충돌을 방지하고 코드의 안정성을 높일 수 있다. (리액트에서 CSS를 사용할 경우의 단점과 반대된다고 한다.)
5. 서버 사이드 렌더링
👉🏼 스타일드 컴포넌트는 서버 사이드 렌더링을 지원하므로 초기 로딩 속도를 높일 수 있다.
리액트에서 CSS를 사용할 경우 전역 스코프로 스타일을 적용하게 되면 스타일이 충돌할 수 있다는 점은 스타일드 컴포넌트의 장점을 찾아보면서 처음 알게 되었다. 결국 스타일드 컴포넌트를 사용한다는 것은 스타일도 모듈화 시킨다는 것이고 CSS-in-JS을 사용하는 이유라고 한다.
왜 스타일드 컴포넌트를 사용하는지 알게 되었으니 스타일드 컴포넌트를 효율적으로 사용하려면 글로벌 스타일을 적용하는 법을 더 연습해 봐야겠다.
[참고 자료]
https://stackoverflow.com/questions/64389523/h1-size-differs-in-div-tag-and-section-tag
https://codingbroker.tistory.com/58
'Diary > Retrospective' 카테고리의 다른 글
[항해99 / TIL] 🚢 23-05-25 짧은 일기 (2) | 2023.05.25 |
---|---|
[항해99 / TIL] 🚢 23-05-03 리액트 query, axios로 게시판 만들기 (4) | 2023.05.03 |
[항해99 / TIL] 🚢 23-04-19 리액트 Redux로 투두리스트 만들기 (0) | 2023.04.19 |
[항해99 / TIL] 🚢 23-04-17 리액트 useState로 투두리스트 만들기 (2) | 2023.04.17 |
[항해99 / TIL] 🚢 23-04-15 chatGPT에게 리액트 기초 물어보기 (2) | 2023.04.16 |