[react] 토글/태그/모달/탭 구현하기
Categories: react
📌 개인적인 공간으로 공부를 기록하고 복습하기 위해 사용하는 블로그입니다.
정확하지 않은 정보가 있을 수 있으니 참고바랍니다 :😸
[틀린 내용은 댓글로 남겨주시면 복받으실거에요]
모달
모달을 구현하는 법 보다 CSS가 더 어려웠다.😥
ModalContainer
처음에 중앙으로 구현을 못해서 제일 상위에 해당하는 컨테이너에서 버튼을 중앙으로 배치하기 위해 조정했다.
1
2
3
4
5
6
export const ModalContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`
ModalBackdrop - modal 이 열렸을 때 뒷 배경 부분
1
2
3
4
5
6
7
8
export const ModalBackdrop = styled.div`
position: fixed;
width: 1280px;
height:450px;
opacity: 0.8;
background-color: gainsboro;
`;
position을 잘 몰라서 헤맸는데
- 기본값은 static이 적용된다 : HTML 문서 상에서 원래 있어야 하는 위치에 배치
-
relative로 설정하게 되면, 요소를 원래 위치에서 벗어나게 배치할 수 있는데 top, bottom, left, right 속성을 이용해서, 요소가 원래 위치에 있을 때의 상하좌우로부터 얼마나 떨어지게 할지를 지정 가능하다.
- fixed 는 스크롤을 위아래로 움직여도 화면에 고정되어서 움직이지 않는 것.
- absolute는 가장 가까운 상위 요소를 기준으로 top, left, bottom, right 속성을 적용한다.
- sticky 는 모달 또는 그 부분이 포함된 요소 내에서 고정된다,
-
[position: sticky] 적용 하면 아래와 같이 버튼과 컨테이너가 분리된다…
ModalButton
1
2
3
4
5
6
7
8
9
export const ModalBtn = styled.button`
background-color: pink;
text-decoration: none;
border: none;
padding: 20px;
color: white;
border-radius: 30px;
cursor: grab;
`;
-
버튼은 기본적으로 구성했고 버튼에 커서를 가까이 대면 커서가 바뀌도록 grab으로 지정했다. 실제로 가까이 가면 손 모양이 나온다.
-
cursor: grab;
: 요소를 잡을 수 있다는 손 모양이 나온다(즉, 끌 수 있다는 것).
-
cursor: grabbing;
: 해당 요소가 현재 잡히고 있음을 나타낸다(즉, 끌고 있음을 나타냄).
Modal Component
-
State & 이벤트
1 2 3 4 5 6
export const Modal = () => { const [isOpen, setIsOpen] = useState(false); const openModalHandler = (e) => { setIsOpen(!isOpen); };
- const [isOpen, setIsOpen] = useState(false); 로 상태 초기화,
setIsOpen
함수는 상태를 업데이트하는 데 사용 - 이벤트 처리 : openModalHandler ⇒ 버튼 클릭시 modal이 닫히면 열리고 열려있으면 닫히게 설정.
- 버튼을 눌러서 모달이 열렸을 때 Open, 닫혔을때는 Open Modal로 버튼 위 텍스트를 설정
- const [isOpen, setIsOpen] = useState(false); 로 상태 초기화,
-
모달 버튼
1 2 3 4
<ModalContainer> <ModalBtn onClick={openModalHandler}> {isOpen ? 'Opened!' : 'Open Modal'} </ModalBtn>
-
열려 있을 때(isOpen, true) 모달 배경과 뷰가 보이도록 함
1 2 3 4 5 6 7 8
{isOpen && ( <ModalBackdrop onClick={openModalHandler}> <ModalView onClick={(e) => e.stopPropagation()}> <h2>😛메롱👅</h2> <button onClick={openModalHandler}>Close</button> </ModalView> </ModalBackdrop> )}
- ModalBackdrop트리거를 클릭하면 openModalHandler모달이 닫힌다.
-
ModalView는 내부를 클릭해도 모달이 닫히지 않도록 하려면 클릭 이벤트가 배경으로(ModalBackdrop) 전파되는 것을 막아야 한다.
1
<ModalView onClick={(e) => e.stopPropagation()}>
이렇게 하면
ModalBackdrop
모달 뷰 내부를 클릭할 때 모달이 열린 상태로 유지된다
완성작
토글
ToggleContainer CSS
-
ToggleContainer
1 2 3 4 5
const ToggleContainer = styled.div` position: relative; margin-top: 8rem; left: 48%; cursor: pointer;
- 화면 중앙에 배치하기 위해서 margin-top: 8rem; left: 48%; 을 설정
-
.toggle-container , 토글 스위치 배경
1 2 3 4 5 6 7 8 9 10 11 12
> .toggle-container { width: 50px; height: 24px border-radius: 30px; background-color: #8b8b8b; &.toggle--checked{ background-color: pink; } }
- toggle—checked 될 경우 (토글이 겨질 경우) 배경색이 변하도록 지정.
-
.toggle-circle, 토글 스위치 내 버튼(원) 스타일
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
> .toggle-circle { position: absolute; top: 1px; left: 1px; width: 22px; height: 22px; border-radius: 50%; background-color: #ffffff; // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다. &.toggle--checked { transform: translateX(26px); } } `; const Desc = styled.div` font-size: 1em; `;
- 컨테이너 내부에 원을 넣기 위해서 position: absolute; top: 1px; left: 1px; 속성 지정
- 토글이 켜질 경우 원이 오른쪽으로 이동하게 함. 26px 만큼!
- transform 속성 - translate(이동) / scale(사이즈 조절) / rotate(회전) / skew (왜곡)
- https://developer.mozilla.org/ko/docs/Web/CSS/transform
- translateX : ??
- Dsec : toggle 아래에 description 부분
Toggle Component
1
2
3
4
5
6
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
setisOn(!isOn);
};
- 상태는 false로 초기화되어 있고 클릭시 바뀐다 → ON(
true
) 또는 OFF(false
)상태로 변경된다.
ToggleSwitch 렌더링
1
2
3
4
5
6
7
8
9
10
11
return (
<>
<ToggleContainer onClick={toggleHandler}>
<div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
<div className={`toggle-circle ${isOn ? "toggle--checked" : ""}`} />
</ToggleContainer>
<br/>
<Desc><center>{isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}</center></Desc>
</>
);
- <ToggleContainer onClick={toggleHandler}> : click 이 눌려지면 toggleHandler 를호출한다
- toggle-container와 toggle-circle은 isOn이 true라면 CSS에서 갖고 있는 toggle—checked의 지정된 속성으로 불러온다.
- toggle description 분은 isOn이 true/false일 경우 보이는 문장이 다르게 설정
완성작
탭
TabMenu - CSS
-
TabMenu - 전체 탭의 속성 지정.
1 2 3 4 5 6 7 8 9 10
const TabMenu = styled.ul` background-color: lightsalmon; 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;
- 컨테이너 내에서 탭을 가운데 정렬 하기 위해 justify-content: center; align-items: center; 스타일 지정
- display: flex 사용하여 수평으로 정렬
-
submenu : 개별 탭 스타일 지정
1 2 3 4
.submenu { cursor: pointer; transition: 0.3s ease; }
- 클릭하면 색상 전환시 0.3초동안 부드럽게 전환 하도록 trainsition : 0.3s ease 속성 넣었고 cusor는 탭 메뉴에 마우스 커서를 올리면 손가락이 가르키는 모양으로 변하게 설정
- ease 속성은 CSS에서 애니메이션의 시작과 끝을 천천히, 중간을 빠르게 진행하도록 하는 기본 가속 곡선
Dsec - CSS
1
2
3
const Desc = styled.div`
text-align: center;
`;
- Dsec(=Description)는 탭 콘텐츠를 표시하는 컨텐츠 영역의 스타일.
- 별도 지정없이 가운데 정렬만 할 수 있게 구현
Tab Component
1
2
3
4
5
6
7
8
9
10
11
12
13
export const Tab = () => {
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) => {
setCurrentTab(index);
};
- 상태 초기화 : const [activeIndex, setActiveIndex] = useState(0) ⇒ 선택한 tab의 index를 설정하고 set으로 업데이트 한다.
- 이벤트 처리 : selectMenuHandler 함수는 탭을 클릭하면 호출되어 클릭한 탭의 인덱스를 전달하고 클릭한 탭의 인덱스로 상태를 업데이트한다.
Return문 , 화면렌더링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
return (
<>
<div>
<TabMenu>
{menuArr.map((el,index)=>(
<li
key ={index}
className={`submenu ${currentTab===index?
'focused' : '' }`}
onClick={() => selectMenuHandler(index)}
>
{el.name}
</li>
))}
</TabMenu>
<Desc>
<p>{menuArr[currentTab].content}</p>
</Desc>
</div>
</>
);
- map을 사용해서 순회하여 각 요소에 대해 li 요소를 생성
key={index}
: 각li
요소에 고유한 키를 부여- className={submenu ${currentTab === index ? ‘focused’ : ‘’}}
현재 탭의 인덱스를 찾고 인덱스가 같으면
'submenu focused'
클래스를 적용하고, 그렇지 않으면'submenu'
클래스를 적용 - onClick={() => selectMenuHandler(index)}: 탭을 클릭하면 selectMenuHandler 함수가 호출되어 currentTab을 해당 index로 업데이트한다.
- {menuArr[currentTab].content} : 현재 선택된 tab의 content를 가져와서 보여주는 js 문법이고
-
는 위에 서 정의한
Desc
라는 styled-component를 사용하여 콘텐츠를 표시하기 위한 태그이다.
참고
나중에 프론트엔드 실무에 들어가면 idx를 사용하면 최적화가 안될 수 있다 ⇒ 삭제가 빈번할 때는 배열의 요소를 자르는 것이 효율이 좋지 않다. 외부 라이브러리나 다른 프롬프트를 사용하는 것이 좋음
태그
CSS
TagsInput
1
2
3
4
5
6
7
8
9
10
11
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;
min-height: 48px; width: 480px;
: 컨테이너의 최소 높이와 너비를 설정- flex-wrap 컨테이너가 더이상 아이템을 한줄에 담을 여유공간이 없을 때 줄바꿈 속성을 정하는 것.
- wrap 은 줄바꿈
- nowrap 삐져나감
- wrap-reverse 위로 올라감.
>ul
1
2
3
4
5
> ul {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 8px 0 0 0;
>
: 바로 하위에 있는 요소를 선택하는 것 (자식선택자라고 주로 얘기한다…) 즉 TagsInput의 바로 하위에 있는 자식인 ul을 선택하는 것.
>.tag
1
2
3
4
5
6
7
8
9
10
11
12
13
> .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);
- TagsInput 아래의 ul의 바로 아래 에 있는 tag의 스타일을 적용
>.tag-close-icon
1
2
3
4
5
6
7
8
9
10
11
12
13
> .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;
}
- TagsInput 아래의 ul의 바로 아래 에 있는 tag-close-icon의 스타일을 적용
>input
1
2
3
4
5
6
7
8
9
10
11
12
13
> 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);
}
- TagsInput의 바로 하위에 있는 자식인 input의 CSS를 적용
:focus { outline: transparent; }:
포커스가 있는 기본 윤곽선을 제거&:focus-within
: & 은 현재 선택자를 참조하여 중첩되는 스타일을 적용하는 것. 입력을 하고 있으면 테두리 색상이 변경된다.
Tag Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const Tag = () => {
const initialTags = ['Code', 'kimcoding'];
const [tags, setTags] = useState(initialTags);
const removeTags = (indexToRemove) => {
setTags(tags.filter((_, index) => index !== indexToRemove));
};
const addTags = (event) => {
const tag = event.target.value.trim();
if (event.key === 'Enter' && tag) {
if (!tags.includes(tag)) {
setTags([...tags, tag]);
event.target.value = '';
}
}
};
- 초기 값은 initialTags로 구현
- removeTags : tag 제거기능 - 선택한 요소의 idx를 찾아서 제거.
- 제거할 인덱스(indexToRemove)를 받고
- filter 메서드에서 첫번째 매개변수는 element 인데 사용히지 않기 때문에 언더바로 표시, 두번째 매개변수는 현재 요소의 인덱스이다.
index !== indexToRemove
조건을 검사합니다. 이 조건이true
일 경우 해당 요소는 새로운 배열에 포함되고,false
일 경우 포함되지 않는다.
- addTags는 enter를 눌렀을 때 새로운 태그 추가하는데 입력 값을 잘라내고 빈 문자열이 아닌지 확인 후 추가한다. 또한 중복된 값이 있는지 검증도 한다.
- 태그가 유효하고 고유하면 태그가
tags
상태에 추가되고 입력 필드가 지워진다 - 화면 렌더링 됨 - event.target.value = ‘ ’ ; tag가 만들어지면 inptu창이 비워져야 하므로 공백으로 변경된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)}>×</span>
</li>
))}
</ul>
<input
className="tag-input"
type="text"
onKeyUp={(event) => addTags(event)}
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
};
-
- 순서없는 목록으로 각 요소를 구성 → 각 요소는
- 에 해당
- map으로 배열을 순회하고 각 태그에 대한 <li>를 반환한다.
key={index}
:key
prop은 React에서 어떤 항목이 변경되었는지, 추가되었는지, 제거되었는지 식별하는 데 사용- {tag} : tag의 이름
- <span className=”tag-close-icon” onClick={() => removeTags(index)}>×</span> : tag의 닫기 아이콘, 클릭하면 removetTags라는 함수를 호출하고 인덱스를 인자로 전달한다.
- <input className=”tag-input” …/> : 새로운 태그를 입력하는 필드
- onKeyUp={(event) => addTags(event)} : 입력이 포커스 되어 있는 동안 Enter 키를 누렀다가 놓을 때 만 함수를 호출한다.
- keyup 과 keydown
keydown
이벤트는 키가 눌렸을 때 발생하고, 키를 계속 누르고 있는 동안 반복적으로 발생keyup
이벤트는 키를 눌렀다가 손을 뗐을 때 발생하며, 한 번만 트리거 되기때문에 중복 이벤트와 auto-repeat 현상이 없고, 보다 예측 가능하고 안정적인 동작을 제공
완성작
flex 어려워서 참고한 사이트 : https://studiomeal.com/archives/197
Leave a comment