File Structure
/
├── /React Custom Component
│ ├── README.md
│ ├── /public # create-react-app이 만들어낸 폴더로 yarn/npm start로 실행 시에 쓰입니다
│ └── /src
│ ├── /components # 단일 UI React 컴포넌트가 들어가는 폴더
│ ├─── /__test__ # 테스트 케이스가 들어가는 폴더
│ ├─── /AdvancedChallenges # Advanced Challenges 를 위한 폴더
│ ├─── /BareMinimumRequirements # Bare Minimum Requirements 를 위한 폴더
│ ├── /stories # Storybook이 작동하는 데 필요한 파일들이 들어가는 폴더
│ ├── app.css
│ ├── App.js # React Custom Component App이 작성되어 있습니다.
│ ├── index.js
├ package.json
└ .gitignore
Bare minimum Requirement
- React, Styled Components, Storybook을 활용하여 React-custom-component를 완성하기!
- Modal, Toggle, Tab, Tag 컴포넌트 구현하기!
Modal Component 완성하기
· Modal UI : 기존의 브라우저 페이지 위에 새로운 윈도우 창이 아닌, 레이어를 까는 것
· TO DO
- isOpen state는 모달 창의 열고 닫힘 여부를 확인할 수 있음 (필요에 따라 state를 더 만들 수도 있음)
- ModalBtn 컴포넌트는 모달 창 제어를 위해 핸들러 함수 openModalHandler를 작성
→ openModalHandler 함수는 ModalBtn 클릭 시 발생되는 change 이벤트 핸들러임 (클릭할 때마다 상태가 불린 값으로 변경)
- ModalView 컴포넌트를 작성하고 isOpen 상태가 true일 경우에만 렌더링
- ModalBackdrop 컴포넌트를 작성하고 isOpen 상태가 true일 경우에만 렌더링
· 요구사항
- ModalContainer : Modal을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 함
- ModalBackdrop : Modal이 떴을 때 배경을 깔아주는 역할
- ModalBtn : Modal 창을 키고 끌 수 있는 버튼
- ModalView : Modal 창 컴포넌트
Modal.js
1st Try!
- 조건부 렌더링을 통해서 isOpen이 true인 상태에서는 모달 버튼 내부가 'Open!'로, isOpen이 false인 상태에서는 내부가 'Open Modal'이 되도록 구현되어야 한다는 것을 빼먹고 구현함!
- value 값을 활용해서 구현해 본 버전!
..(생략)..
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = (e) => {
//console.log(e.target.value)
// TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
if(e.target.value === "open"){
setIsOpen(true);
} else if (e.target.value === "close"){
setIsOpen(false);
}
};
return (
<>
<ModalContainer>
<ModalBtn
// TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
value={'open'} onClick={openModalHandler}
>
Open Modal
</ModalBtn>
{isOpen ?
<ModalBackdrop>
<ModalView>
<CloseBtn value={'close'} onClick={openModalHandler}>X</CloseBtn>
<h4>HELLO CODESTATES!</h4>
</ModalView>
</ModalBackdrop>
: null}
</ModalContainer>
</>
);
};
Modal.js
2nd Try!
- 모달 창 밖을 클릭하면 모달창이 닫히도록 해야함
- 그래서 state를 바꿀 때 isOpen이 false이면 계속 null값이고 모달 버튼을 눌러 상태가 바뀌면 모달 창이 나타나도록 함
- 다른 이벤트를 준 곳은 눌으면 창이 사라지게 됨
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 : 300px;
`;
export const ModalBackdrop = styled.div`
// TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
position: fixed;
background-color: rgba(0,0,0,0.15);
top : 0;
left : 0;
right : 0;
bottom : 0;
display: flex;
justify-content : center;
align-items : center;
`;
export const ModalBtn = styled.button`
background-color: var(--coz-purple-600);
text-decoration: none;
border: none;
padding: 20px;
color: white;
border-radius: 30px;
cursor: grab;
`;
export const CloseBtn = styled.button`
background-color: white;
border: none;
margin-top: 20px;
`;
export const ModalView = styled.div.attrs((props) => ({
// attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
role: 'dialog',
}))`
// TODO : Modal창 CSS를 구현합니다.
border-radius: 10px;
background-color: #ffffff;
width: 500px;
height: 200px;
margin-top : 30px;
display : flex;
flex-direction: column;
align-items : center;
> h4 {
margin-top: 50px;
}
`;
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = (e) => {
// TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
setIsOpen(!isOpen)
};
return (
<>
<ModalContainer onClick={openModalHandler}>
<ModalBtn
onClick={openModalHandler}
>
{isOpen ? 'Opened!' : 'OpenModal'}
</ModalBtn>
{isOpen === false ? null
: <ModalBackdrop>
<ModalView>
<CloseBtn onClick={openModalHandler}>X</CloseBtn>
<h4>HELLO CODESTATES!</h4>
</ModalView>
</ModalBackdrop>
}
</ModalContainer>
</>
);
};
Modal.js
해설해 주신 코드
추가적으로 학습하게 된 내용
- 불리언 타입의 상태를 바꿀 때의 국룰
→ 현재 상태 false라면 이번 과제를 예로 들었을 때 setIsOpen(!false) === setIsOpen(true)
→ 현재 상태 true라면 setIsOpen(!true) === setIsOpen(false)
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = (e) => {
setIsOpen(!isOpen)
//? 국룰
// 현재 상태 false
//setIsOpen(!false) === setIsOpen(true)
// 현재 상태 ture
//setIsOpen(!true) === setIsOpen(false)
};
return (
<>
<ModalContainer onClick={openModalHandler}>
<ModalBtn
onClick={openModalHandler}
>
{isOpen ? 'Opened!' : 'OpenModal'}
</ModalBtn>
: <ModalBackdrop>
<ModalView onClick={(event) => event.stopPropagation()}>
<CloseBtn onClick={openModalHandler}>X</CloseBtn>
<h4>HELLO CODESTATES!</h4>
</ModalView>
</ModalBackdrop>
}
</ModalContainer>
</>
);
};
// ? 이벤트 버블링
// 거품이 올라가는 것처럼 자식 컴포넌트르 눌렀을 때 부모 이벤트가 발생한다
// 눌렀을 때 이벤트가 발생되면 안되는 곳에가서 프로퍼게이션을 주면 됨
// <ModalView onClick={(event) => event.stopPropagation()}>
Toggle Component 완성하기
· Toggle UI : 두 가지 상태만 가지고 있는 스위치.
· TO DO
- isOn state는 토글 버튼의 on/off 여부를 확인할 수 있음 (필요에 따라 state를 더 만들 수도 있음)
- ToggleContainer 컴포넌트는 토글 버튼 제어를 위해 핸들러 함수 togglelHandler를 작성
→ toggleHandler 함수는 ToggleContainer 클릭 시 발생되는 change 이벤트 핸들러임 (클릭할 때마다 상태가 불린 값으로 변경)
· 요구사항
- ToggleContainer : Toggle을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 함
- Desc : Toggle Switch의 상태를 설명하는 텍스트를 담는 컴포넌트
- ToggleContainer 내부에 .toggle-container, .toggle-circle 클래스를 가진 div 요소를 각각 생성
- 생성한 요소에 조건부 스타일링을 활용해 Toggle Switch가 ON인 경우에만 toggle--checked 클래스를 두 요소에 추가
- 조건부 렌더링을 활용해 Toggle Switch가 ON인 상태일 경우 Desc 컴포넌트 내부의 택스트를 'Toggle Switch ON'으로 반대의 경우 OFF 버전으로 변경하기
- 토글 스위치가 부드럽게 옮겨지게 transition 속성 추가 해보기!
Toggel.js
- 특정 상태일 때 클래스를 추가해서 스타일링을 주는 것을 학습함
import { useState } from 'react';
import styled from 'styled-components';
const ToggleContainer = styled.div`
position: relative;
margin-top: 8rem;
left: 47%;
cursor: pointer;
> .toggle-container {
width: 50px;
height: 24px;
border-radius: 30px;
background-color: #8b8b8b;
}
> .toggle--checked {
background-color: var(--coz-purple-600);
transition: all 0.5s ease-in-out;
}
> .toggle-circle {
position: absolute;
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #ffffff;
transition: all 0.5s ease-in-out;
}
> .move {
left : 27px !important;
transition: all 0.5s ease-in-out;
}
`;
const Desc = styled.div`
// TODO : 설명 부분의 CSS를 구현합니다.
position: relative;
margin-top: 1rem;
text-align: center;
`;
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
// TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
//console.log('Click!')
setisOn(!isOn);
};
return (
<>
<ToggleContainer
onClick={toggleHandler}
>
<div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
<div className={`toggle-circle ${isOn ? "toggle--checked move" : ""}`} />
</ToggleContainer>
{isOn === false ? <Desc>Toggle Switch OFF</Desc> : <Desc>Toggle Switch ON</Desc>}
</>
);
};
Toggel.js
해설 해주신 코드
추가적으로 학습한 내용
- 위에서 내가 구현한 방법은 circle div에는 아예 move라는 클래스를 추가적으로 줬는데 &을 이용해 구현
import { useState } from 'react';
import styled from 'styled-components';
const ToggleContainer = styled.div`
position: relative;
margin-top: 8rem;
left: 47%;
cursor: pointer;
> .toggle-container {
width: 50px;
height: 24px;
border-radius: 30px;
background-color: #8b8b8b;
&.toggle--checked{
background-color: var(--coz-purple-600);
}
}
> .toggle-circle {
position: absolute;
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #ffffff;
transition: all 0.5s ease-in-out;
&.toggle--checked{
left : 27px;
}
}
`;
const Desc = styled.div`
position: relative;
margin-top: 1rem;
text-align: center;
`;
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
setisOn(!isOn);
};
return (
<>
<ToggleContainer
>
<div onClick={toggleHandler} className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
<div onClick={toggleHandler} className={`toggle-circle ${isOn ? "toggle--checked move" : ""}`} />
</ToggleContainer>
<Desc>{isOn === false ? 'Toggle Switch OFF' : 'Toggle Switch ON'}</Desc>
</>
);
};
Tab Component 완성하기
· Tab UI : 동일한 메뉴라인에서 뷰를 전환할 때 사용
· TO DO
- currentTab state는 현재 tab의 index를 확인할 수 있음 (필요에 따라 state를 더 만들 수도 있음)
- TabMenu 컴포넌트는 버튼 제어를 위해 핸들러 함수 selectMenuHandler 를 작성
→ toggleHandler 함수는 TabMenu 클릭 시 발생되는 change 이벤트 핸들러임 (클릭할 때마다 상태가 index 값으로 변경)
- li 요소를 이용해 메뉴를 생성하고, 각 메뉴를 눌렀을 때 뷰가 전환되도록 handler(selectMenuHandler) 함수를 작성하기!
- 조건부 스타일링과 currentTab 상태를 이용하여 클릭한 Tab 메뉴만 className(submenu focused)과 CSS가 변경되도록 구현 --> 템플릿 리터럴과 삼항 연산자 활용하기!
· 요구사항
- TabMenu
→ TabMenu 내부에 .submenu 클래스 명을 가진 li 요소들을 map을 이용한 반복을 통해 생성함
→ TabMenu 내부에 .submenu 클래스 명을 가진 li 요소들의 textContent는 각 요소의 name임
- currentTab
→ 조건부 렌더링을 활용해서 Tab 메뉴가 선택된 상태일 때, 선택된 Tab 메뉴 li 요소의 클래스명만 submenu focused가 되어야 하고, 선택되지 않은 나머지는 submenu가 되도록 구현하기
→ TabMenu를 클릭하면 현재 선택된 탭의 인덱스 값을 전달받아 currentTab 상태를 변경하는 selectMenuHandler 메서드가 실행되어야 함
→ TabMenu를 클릭하면 현재 선택된 탭 메뉴만 .focused CSS가 적용되어야 함
→ TabMenu를 클릭하면 Desc 컴포넌트의 content의 내용이 해당 탭의 content로 바뀌어야 함
Tab.js
1st Try!
- 주어진 메뉴 배열에 id를 임의로 부여해서 구현은 성공함!
- but, 임의로 추가한 id로 테스트는 성공하지 못함..
..(생략)..
export const Tab = () => {
const [currentTab, setCurrentTab] = useState(0);
const menuArr = [
{ id:0, name: 'Tab1', content: 'Tab menu ONE' },
{ id:1, name: 'Tab2', content: 'Tab menu TWO' },
{ id:2, name: 'Tab3', content: 'Tab menu THREE' },
];
const selectMenuHandler = (li) => {
setCurrentTab(li.target.value);
};
return (
<>
<div>
<TabMenu>
{ menuArr.map((li) => {
return (
<li value={li.id} className="submenu" onClick={selectMenuHandler}>{li.name}</li>
);
})}
</TabMenu>
<Desc>
{ menuArr.filter(li => currentTab === li.id).map((li) => {
return (
<p>{li.content}</p>
);
})}
</Desc>
</div>
</>
);
};
Tab.js
2nd Try!
- TabMenu에 map을 이용해서 뿌려줄 때 menuArr 배열에 요소를 뿌리면서 key값을 줌 → 뿌리면서 index라는 이름으로 key 값 생성
- 그리고 li를 클릭하면 selectMenuHandler에 index를 인자로 전달하면서 실행 → 실행되면서 상태가 변경되고 재랜더링
import { useState } from 'react';
import styled from 'styled-components';
const TabMenu = styled.ul`
background-color: #dcdcdc;
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;
.submenu {
height: 50px;
width: 30%;
display: flex;
align-items: center;
padding: 15px;
transition: all 0.18s ease-in-out;
}
.focused {
background-color: var(--coz-purple-600);
color: #ffff;
}
& div.desc {
text-align: center;
}
`;
const Desc = styled.div`
text-align: center;
`;
export const Tab = () => {
// TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
// currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
const [currentTab, setCurrentTab] = useState(0);
const menuArr = [
{ name: 'Tab1', content: 'Tab menu ONE' },
{ name: 'Tab2', content: 'Tab menu TWO' },
{ name: 'Tab3', content: 'Tab menu THREE' },
];
const selectMenuHandler = (index) => {
// TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
// TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
//console.log(index);
setCurrentTab(index);
};
return (
<>
<div>
<TabMenu>
{/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다. - done!*/}
{/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 나머지 2개의 tab은 'submenu' 가 됩니다.*/}
{ menuArr.map((tab,index) => {
return (
<li key={index} className={`${currentTab === index ? 'submenu focused' : 'submenu'}`} onClick={() => selectMenuHandler(index)}>{tab.name}</li>
);
})}
</TabMenu>
<Desc>
<p>{menuArr[currentTab].content}</p>
</Desc>
</div>
</>
);
};
Tag Component 완성하기
· Tag UI : 레이블 지정을 통해 구성이나 분류에 도움이 되는 키워드 집합을 만들 때 자주 사용 됨.
· TO DO
- tags state는 배열의 형태, 초기값으로 initalTages를 가지고 있음 (필요에 따라 state를 더 만들 수도 있음)
- TagsInput 컴포넌트는 핸들러 함수 addTags를 가짐
→ addTags 함수는 input창에 Enter키를 누를 때만다 발생하는 change 이벤트 핸들러 (Enter 입력할 때마다 입력된 값이 state에 추가 됨)
- span.tag-close-icon 에는 아직 실제 작동하는 삭제 아이콘(X)이 없음! → 삭제 아이콘을 만들고, 이 버튼이 동작하도록 handler(removeTags) 함수를 작성하기!
· 요구사항
- input 기능 테스트
→ input 창에 텍스트를 입력 후 Enter 키를 누르면 태그가 추가되어야 함!
→ Enter 키를 통해 태그가 추가되도록 하며, Enter 키가 눌리면 태그르 추가하는 addTags 메서드가 실행되어야 함!
→ addTags 메서드는 태그 추가 기능 이외에 아래 세가지 기능도 수행할 수 있어야 함
1. 이미 입력되어 있는 태그인지 검사하야 이미 입력되어 있다면 추가히지 말아야 함
2. 아무것도 입력하지 않은 상태에서는 Enter 키를 눌러도 addTags 메서드가 실행되지 않아야 함
3. 태그가 추가되고 나면 input 창이 비워져야 함
- 삭제 기능 테스트
→ 기본적으로 tags 배열 안에 모든 태그들이 화면에 보여야 함
→ 태그 이름 옆에 삭제 아이콘이 표시되도록 하고, 아이콘을 클릭하면 해당 태그를 삭제하는 removeTag 메서드가 실행되어야 함
→ removeTags 메서드가 삭제 아이콘이 눌린 태그를 삭제하도록 removeTags 메서드를 완성해야 함
Tag.js
- 구현은 다 했는데 몇 개의 테스트를 통과하지 못했음
- 삭제 버튼을 구현할 때 삭제를 의미하는 X를 p태그로 감싸고 p태그에 onClick 이벤트를 줬더니 통과하지 못했음 → span 태그 자체에 이벤트를 옮겨줘서 통과
- input에 addTags 메서드를 연결할 때, 키를 눌렀다가 땔 때의 이벤트를 받아와서 전달해주는 과정에서 고전함
import { useState } from 'react';
import styled from 'styled-components';
export const TagsInput = styled.div`
margin: 8rem auto;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
min-height: 48px;
width: 480px;
padding: 0 8px;
border: 1px solid rgb(214, 216, 218);
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: #fff;
padding: 0 8px;
font-size: 14px;
list-style: none;
border-radius: 6px;
margin: 0 8px 8px 0;
background: var(--coz-purple-600);
> .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);
border-radius: 50%;
background: #fff;
cursor: pointer;
}
}
}
> input {
flex: 1;
border: none;
height: 46px;
font-size: 14px;
padding: 4px 0 0 0;
:focus {
outline: transparent;
}
}
&:focus-within {
border: 1px solid var(--coz-purple-600);
}
`;
export const Tag = () => {
const initialTags = ['CodeStates', 'kimcoding'];
const [tags, setTags] = useState(initialTags);
const removeTags = (indexToRemove) => {
//console.log('Click!') //--> 확인 됨!
//console.log(indexToRemove); //--> key 값이 뜸
console.log(tags);
tags.splice(indexToRemove,1)
setTags([...tags])
};
const addTags = (event) => {
const newTag = event.target.value;
// TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성.
// - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
// - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
// - 태그가 추가되면 input 창 비우기
//console.log(event.key) //--> Enter 키 누르면 나오는 거 확인함
//console.log(event.target.value) //--> 확인 됨
if(event.key === 'Enter' && !tags.includes(newTag) && newTag){
setTags([...tags, newTag]);
event.target.value = '';
}
};
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={(index) => removeTags(index)}>
<p>X</p>
</span>
</li>
))}
</ul>
<input
className="tag-input"
type="text"
onKeyUp={(event) => {addTags(event)}}
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
};
'💻 > [과제]' 카테고리의 다른 글
[과제] React Hooks 적용하기 (0) | 2023.01.26 |
---|---|
[과제] C Market 상태 관리하기 (0) | 2022.12.29 |
[과제] JSON.stringify (0) | 2022.12.16 |
[과제] Tree UI (0) | 2022.12.16 |
[과제] 나만의 아고라 스테이츠 서버 만들기 (0) | 2022.12.11 |