why not
[React] Custom Component 본문
1. Styled Components
- 앞서 배운 CSS in JS 라는 개념이 대두되면서 나오게 되었다.
- CSS 코드를 다루면서 불편했던 점을 CSS를 컴포넌트화 시킴으로써 해결해주는 React 환경에서 사용이 가능한 라이브러리이다.
- 기존에 HTML, CSS, JS 파일로 쪼개서 개발하던 방법 -> React 등의 라이브러리의 등장으로 컴포넌트 단위 개발이 주로 이루어졌지만, CSS는 그렇지 못했다는 점에서 출발하였다.
- CSS in JS 라이브러리를 사용하면 CSS도 쉽게 Javascript 안에 넣어줄 수 있으므로, HTML + JS + CSS까지 묶어서 하나의 JS파일 안에서 컴포넌트 단위로 개발할 있다.
- CSS in JS 라이브러리 중에서 현재 가장 인기 있는 라이브러리이다.
1-2. Styled Components 문법
1-2.1) 컴포넌트 만들기
- Styled Components는 ES6의 Templete Literals 문법을 사용한다. -> 따옴표가 아닌 백틱(`)을 사용
- 컴포넌트를 선언한 후 styled.태그종류를 할당하고, 백틱 안에 기존에 CSS를 작성하던 문법과 똑같이 스타일 속성을 작성
- 이렇게 만든 컴포넌트를 React 컴포넌트를 사용하듯 리턴문 안에 작성해주면 스타일이 적용된 컴포넌트가 렌더되는 것을 확인이 가능하다.
1-2.2) 컴포넌트를 재활용해서 새로운 컴포넌트 만들기
- 컴포넌트를 선언하고 styled() 에 재활용할 컴포넌트를 전달해준 다음, 추가하고 싶은 스타일 속성을 작성해주면 된다.
1-2.3) Props 활용하기
- Styled Component로 만든 컴포넌트도 React 컴포넌트처럼 props를 내려줄 수 있다.
- 내려준 props 값에 따라서 컴포넌트를 렌더링하는 것도 가능하다.
- Styled Components는 템플릿 리터럴 문법( ${ } )을 사용하여 JavaScript 코드사용 -> props를 받아오려면 props를 인자로 받는 함수를 만들어 사용
2. Bare minimum Requirement
2-1. Modal Component
import { useState } from "react";
import styled from "styled-components";
export const ModalContainer = styled.div`
// TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
display: flex;
// 요소들을 플렉스 박스로 설정
justify-content: center;
// 플렉스 박스의 가로 방향 정렬을 가운데로 설정
align-items: center;
// 플렉스 박스의 세로 방향 정렬을 가운데로 설정
height: 100vh;
// 해당 요소의 높이를 브라우저 창의 세로 높이를 100vh로 설정
`;
export const ModalBackdrop = styled.div`
// TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
// fixed는 보이는 곳에서 고정, absolute는 전체에서 고정
position: fixed;
// 해당 요소의 위치를 뷰포트를 기준으로 고정
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
right: 0;
bottom: 0;
// 해당 요소의 위치를 뷰포트의 상단, 좌측, 우측, 하단에 맞게 설정 -> 전체 화면에서 가운데로 고정
background-color: rgba(0, 0, 0, 0.5);
// 배경색을 검은색으로 설정하고 0.5의 투명도로 설정
`;
export const ModalBtn = styled.button`
// 모달 열고 닫기 버튼 구성
// background-color: var(--coz-purple-600);
background-color: rgb(237, 219, 199);
// 해당 요소의 배경색을 사전에 정의된 색상 변수(var)인 --coz-purple-600으로 설정
font-weight: bold;
text-decoration: none;
// 내부의 텍스트의 밑줄을 제거
border: none;
// 해당 요소의 테두리를 제거
padding: 20px;
// 내부 여백을 20px으로 설정
color: rgb(167, 114, 125);
// 해당 요소 내부의 텍스트 흰색으로 설정
border-radius: 30px;
// 해당 요소의 모서리를 30px 값으로 둥글게 처리
cursor: grab;
// 해당 요소에 마우스 커서를 올렸을 때, grab 커서를 표시하도록 설정
`;
// 버튼 클릭시 나오는 모달 창 구현
export const ModalView = styled.div.attrs((props) => ({
// attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
role: "dialog",
}))`
// TODO : Modal창 CSS를 구현합니다.
border-radius: 10px;
background-color: #ffffff;
width: 300px;
height: 100px;
margin-bottom: 200px;
display: flex;
flex-direction: column;
// 해당 요소의 자식 요소를 수직(column) 방향으로 정렬
align-items: center;
> div.desc {
font-size: 15px;
color: rgb(233, 100, 121);
font-weight: bold;
// div.desc: 해당 요소의 자식 요소 중 클래스 이름이 desc인 요소 선택
}
`;
export const Exitbtn = styled(ModalBtn)`
background-color: #ffffff;
color: black;
margin: 10px;
padding: 5px 10px;
// 상하의 여백 지정
`;
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
// useState Hook을 사용하여 isOpen과 setIsOpen 상태 변수를 정의
// isOpen은 현재 모달이 열려있는지 여부를 나타내는 Boolean 값
// setIsOpen은 isOpen 값을 변경하는 함수
const openModalHandler = () => {
// TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
setIsOpen(!isOpen);
};
// 현재 isOpen 값을 반대로 변경하여 모달이 열리거나 닫힘 -> isOpen 값이 true이면 false로, false이면 true로 변경됨
// isOpen 상태 변수를 이용하여 모달이 열려있는지 여부를 체크 -> 모달의 열고 닫는 모달의 상태를 관리
return (
<>
<ModalContainer>
{/* ModalContainer -> 모달을 감싸는 컨테이너 역할 */}
<ModalBtn
// onClick 함수가 openModalHandler 함수를 호출하여 모달의 열리고 닫힘 상태를 변경
// TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
onClick={openModalHandler}
>
{isOpen ? "감사해영!" : "양말 한짝"}
{/* 버튼의 라벨은 isOpen 변수를 이용하여 "Open Modal" 또는 "Opened!"로 표시 */}
{/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
</ModalBtn>
{/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
{isOpen ? (
<ModalBackdrop onClick={openModalHandler}>
{/* isOpen 변수를 이용하여 모달이 열려있는지 여부를 체크
열려있는 경우 ModalBackdrop과 ModalView 컴포넌트가 렌더링
배경을 클릭하면 onClick 함수가 호출되며, 이 함수는 openModalHandler 함수를 호출하여 모달을 닫음 */}
{/* 자식요소는 이벤트 실행안되도록. '이벤트 버블링'이라는 현상을 막아줌*/}
<ModalView
onClick={(event) => {
event.stopPropagation();
}}
// 배경 클릭 시 닫히지 않도록 stopPropagation() 함수를 사용하여 이벤트 버블링 현상을 막아줌
>
{/* x 버튼 */}
<Exitbtn onClick={openModalHandler}>×</Exitbtn>
<div className="desc">도비는 자유에옹!</div>
</ModalView>
</ModalBackdrop>
) : null}
</ModalContainer>
</>
);
};
2-2. Toggle Component
import { useState } from "react";
import styled, { css } from "styled-components";
// 스타일드 컴포넌트를 사용하여 토글 스위치 스타일 정의
const ToggleContainer = styled.div`
position: relative;
margin-top: 8rem;
left: 47%;
// 상대 위치로 배치
cursor: pointer;
// 마우스 커서가 올라가면 손가락 모양의 커서로 표시
> .toggle-container {
// ToggleContainer 내부의 자식 요소 중 클래스 이름이 "toggle-container"인 요소를 선택
width: 50px;
height: 24px;
border-radius: 30px;
background-color: #8b8b8b;
transition: 0.2s;
// TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
&.toggle--checked {
// &.toggle--checked는 .toggle-container에 toggle--checked 클래스가 추가된 경우에 대한 스타일을 지정
// toggle--checked 클래스는 토글 스위치가 "on" 상태일 때 추가 -> background-color 속성 지정
background-color: rgb(167, 114, 125);
transition: 0.2s;
// transition -> 스위치 상태 변화시 애니메이션
}
}
// toggle switch의 원형 버튼 toggle-circle의 스타일링
> .toggle-circle {
position: absolute;
// toggle-circle의 위치를 절대적으로 설정
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #ffffff;
// TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
&.toggle--checked {
left: 27px;
transition: 0.2s;
// toggle-container와 toggle-circle 사이의 전환 효과
}
}
`;
const Desc = styled.div`
// TODO : 설명 부분의 CSS를 구현합니다.
position: relative;
top: 5px;
left: 44%;
/* color: ${(props) => (props.isChecked ? "rgb(233, 100, 121)" : "black")}; */
/* color: ${({ isChecked }) =>
isChecked ? "rgb(167, 114, 125)" : "black"}; */
/* ${(props) => {
if (props.isChecked)
return css`
color: rgb(167, 114, 125);
font-weight: bold;
`;
}}*/
${(props) =>
props.isChecked &&
css`
color: rgb(167, 114, 125);
font-weight: bold;
`}/* &.toggle--checked {
color: red;
} */
`;
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
// TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
setisOn(!isOn);
};
return (
<>
{/* 리액트 컴포넌트로 토글 스위치 구현 */}
<ToggleContainer
// TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
onClick={toggleHandler}
>
{/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
{/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
<div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
<div className={`toggle-circle ${isOn ? "toggle--checked" : ""}`} />
</ToggleContainer>
{/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
{/* TIP: Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
{/* <Desc className={isOn ? "toggle--checked" : ""}> */}
<Desc isChecked={isOn}>
{" "}
{isOn ? "이제 일해라 닝겐!" : "미이야~아~옹!"}
</Desc>
{/* Desc 컴포넌트로 토글 스위치의 상태를 나타내는 텍스트를 렌더링
스위치가 켜진 상태이면 "Toggle Switch ON", 꺼진 상태이면 "Toggle Switch OFF */}
</>
);
};
2-3. Tab Component
import { useState } from "react";
import styled, { css } from "styled-components";
// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.
const TabMenu = styled.ul`
background-color: rgb(249, 245, 231);
color: rgba(73, 73, 73, 0.5);
font-weight: bold;
display: flex;
flex-direction: row;
justify-items: center;
align-items: center;
list-style: none;
margin-bottom: 7rem;
padding: 0;
.submenu {
${"" /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
background-color: rgb(249, 245, 231);
width: calc(100% / 3);
height: 30px;
text-align: center;
transition: 0.5s;
padding: 10px;
}
.focused {
${"" /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다. */}
//background-color: var(--coz-purple-600);
background-color: rgb(167, 114, 125);
color: white;
width: calc(100% / 3);
height: 30px;
text-align: center;
transition: 0.5s;
}
`;
const Desc = styled.div`
text-align: center;
/* ${(props) =>
props.isChecked &&
css`
color: rgb(167, 114, 125);
font-weight: bold;
`} */
`;
export const Tab = () => {
// TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
// currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
const [focusedTab, setFocusedTab] = useState(0);
const menuArr = [
{
name: "상견니",
content: "너라면, 나를 절대 잊지 않을거고, 결국엔 나를 찾을거라고 믿어!",
},
{
name: "깨비깨비도깨비",
content:
"너와 함께한 시간 모두 눈부셨다. 날이 좋아서, 날이 좋지 않아서, 날이 적당해서...",
},
{ name: "한산", content: "지금 우리에겐 압도적인 승리가 필요하다!" },
];
const selectMenuHandler = (index) => {
// TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
// TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
setFocusedTab(index);
};
return (
<>
<div>
<TabMenu>
{/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
{/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며,
나머지 2개의 tab은 'submenu' 가 됩니다.*/}
{/* 핸들러 함수 연결 해줄 때 함수 실행 형태로 넣으면 안됨! 콜백함수 형태로 넣어야함!
selectMenuHandler(index) 이렇게만 넣으면 실행 형태가 됨*/}
{menuArr.map((el, index) => {
return (
<li
key={index}
className={`submenu${focusedTab === index ? " focused" : ""}`}
onClick={() => selectMenuHandler(index)}
>
{el.name}
</li>
);
})}
</TabMenu>
<Desc isChecked={focusedTab}>
{/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
<p>{menuArr[focusedTab].content}</p>
</Desc>
</div>
</>
);
};
2-4. Tag Component
import { useState } from "react";
import styled from "styled-components";
// TODO: Styled-Component 라이브러리를 활용해 여러분만의 tag 를 자유롭게 꾸며 보세요!
export const TagsInput = styled.div`
margin: 8rem auto;
// 위, 아래 여백을 8rem, 좌우 여백을 자동으로 설정
display: flex;
// flex박스를 사용하여 요소를 배치
align-items: flex-start;
// 주축을 따라 아이템을 위쪽으로 정렬
flex-wrap: wrap;
// flex박스에서 아이템이 한 줄을 넘어갈 때 줄을 바꿈
min-height: 48px;
// 최소 높이를 48px로 설정
width: 480px;
padding: 0 8px;
// 위아래 여백을 0, 좌우 여백을 8px로 설정
border: 3px solid rgb(167, 114, 125);
// 테두리를 3px의 굵기
border-radius: 6px;
> ul {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 8px 0 0 0;
> .tag {
width: auto;
height: 32px;
display: flex;
align-items: center;
// 세로 방향 정렬을 가운데로 설정
justify-content: center;
// 가로 방향 정렬을 가운데로 설정
color: rgb(167, 114, 125);
padding: 0 8px;
font-size: 14px;
list-style: none;
border-radius: 6px;
margin: 0 8px 8px 0;
// background: var(--coz-purple-600);
background: rgb(248, 234, 216);
> .tag-close-icon {
display: block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
font-size: 14px;
margin-left: 8px;
// color: var(--coz-purple-600);
color: rgb(167, 114, 125);
border-radius: 50%;
background: rgb(249, 245, 231);
cursor: pointer;
}
}
}
> input {
flex: 1;
// input 요소가 부모 요소의 남은 공간을 모두 차지할 수 있도록 설정
border: none;
height: 46px;
font-size: 14px;
padding: 4px 0 0 0;
:focus {
outline: transparent;
// input 요소가 포커스될 때 outline을 투명하게 처리합니다.
}
}
&:focus-within {
border: 1px solid rgb(167, 114, 125);
// var(--coz-purple-600);
// 해당 요소 또는 해당 요소의 자식 요소 중 하나가 포커스될 때, 요소에 1px의 실선 테두리의 색상을 설정
}
`;
export const Tag = () => {
const initialTags = ["일상그램", "도비는가끔눈물을흘린다"];
const [tags, setTags] = useState(initialTags);
//useState 훅을 사용 -> tags 배열과 tags 배열을 변경하는 함수 setTags를 선언
const removeTags = (indexToRemove) => {
// 인덱스(indexToRemove)를 전달받아 해당 인덱스의 태그를 제거
// TODO : 태그를 삭제하는 메소드를 완성하세요.
let newTags = tags.slice();
// tags 배열을 복사한 새로운 배열을 newTags 변수에 할당
newTags.splice(indexToRemove, 1);
// newTags 배열에서 indexToRemove 인덱스의 요소를 1개 제거
setTags(newTags);
// 변경된 newTags 배열을 setTags 함수를 사용하여 tags 배열에 저장
};
// addTags 함수 -> 이벤트(event) 객체를 전달받아 새로운 태그를 추가하는 역할
const addTags = (event) => {
// TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
let newTag = event.target.value.trim();
if (
!tags.includes(newTag) &&
newTag.length !== 0 &&
event.key === "Enter"
// 태그를 추가할 수 있도록 if문을 사용하여 조건을 검사 (3가지)
// -> 새로운 태그가 이전에 추가된 적이 있는지 검사하여 이미 있는 태그면 추가X
// -> 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행X
// -> 태그가 추가되면 input 창 비워주기
) {
let newTags = tags.slice();
// tags 배열을 복사한 새로운 배열을 newTags 변수에 할당
newTags.push(newTag);
// newTag를 newTags 배열에 추가
// setTags 함수로 변경된 newTags 배열 tags 배열에 저장 -> 새로운 태그 추가 완료
setTags(newTags);
event.target.value = "";
// 이벤트의 대상(target) 요소의 값을 빈 문자열("")로 초기화 -> 입력 필드 비워두기
}
};
return (
<>
<TagsInput>
<ul id="tags">
{tags.map((tag, index) => (
<li key={index} className="tag">
<span className="tag-title">{tag}</span>
<span
className="tag-close-icon"
onClick={() => removeTags(index)}
>
{/* TODO : tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
x
</span>
</li>
))}
</ul>
<input
className="tag-input"
type="text"
onKeyUp={addTags}
// input 요소에서 키보드가 눌렸을 때
// 발생하는 이벤트(onKeyUp)에 입력된 값을 검증하여 태그 목록에 새로운 태그를 추가하는 addTags 함수를 연결시킴
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
};
'CodeStates > 블로깅 챌린지' 카테고리의 다른 글
[React] 상태 관리 (0) | 2023.02.23 |
---|---|
Create React App을 이용해서 리액트 설치하기 (0) | 2023.02.23 |
React 가상돔 (0) | 2023.02.21 |
React 오리엔테이션 (0) | 2023.02.20 |
UI/UX (0) | 2023.02.17 |