화면이 아무리 복잡하고 다양하더라도 기본 구성에서 사용되는 UI들은 반복적으로 재사용되는 경우가 많습니다
오늘은 프로젝트 개발의 효율성을 높여주는
UI 컴포넌트들을 만들어보겠습니다
컴포넌트 ui - 모달
콘솔창을 띄워 element들을 눌러보면서 어떤 컴포넌트 위에 스타일이 들어가는 지를 잘 확인해야 합니다
일반 CSS와 달리 styled component는 class의 이름을 임의로 지정하여
(위에 "sc-dkPtyc ioRIXP" 라고 이상한 클래스 보이시져...)
스타일을 주기 때문에, 클래스의 이름이 확! 눈에 띄지 않을 수 있습니다.
하지만, 각 컴포넌트에 어떤 css 효과를 주었는지 촤르륵~ 볼 수 있어요!
( css 파일 열어서 html나 js 파일 왔다갔다, class 찾아 요리조리 하는것 보단 나음 ㅎ)
ModalView 컴포넌트의 경우, div 를 품고 있는데
div에 styled component를 주고 싶으면 ModalView ``(백틱) 안에 div{여기안에 코드 작성}를 넣어주면 됩니다
openModalHandler 컴포넌트 안에 삼항연산자를 사용해주었는데,
길게 작성할 필요없이 setIsOpen(!isOpen)이라고 적어줘도 됩니다
isOpen은 Boolean(true/false) 값이니 기존 isOpen의 반대로 상태를 변경해주면 되기 때문임~
자 이렇게 다 컴포넌트들을 다 선언해주고, return 으로 내려갑니다
컴포넌트가 여러개이기 때문에 헷갈리지 않게 나열해주어야 합니다
<모달컨테이너>
<모달버튼>
<모달버튼/>
{ isOpen이 참이면 모달뷰와 모달백드롭 보여줌, 거짓이면 둘다 안보여줌 }
<모달컨테이너/>
으로 짜야 합니다
{isOpen ?
<ModalBackdrop onClick={openModalHandler}>
<ModalView onClick={(event)=> event.stopPropagation()}>
{/*ModalBackdrop이 눌렸을때 ModalView까지 눌리지 않도록 눌림방지*/}
<div onClick={openModalHandler}>❌</div>
modal 창이 열렸음
</ModalView>
</ModalBackdrop>
: 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: 100%;
`;
export const ModalBackdrop = styled.div`
// TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
`;
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 ModalView = styled.div.attrs((props) => ({
// attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
role: 'dialog',
}))`
// TODO : Modal창 CSS를 구현합니다.
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
max-width: 400px;
height: 200px;
background-color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
div {
position: absolute;
top: 5%;
left: 50%;
transform: translateX(-50%);
}
`;
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
//isOpen state는 모달 창의 열고 닫힘 여부를 확인할 수 있습니다.
//필요에 따라서 state를 더 만들 수도 있습니다.
const openModalHandler = () => {
// TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
{isOpen === true ? setIsOpen(false) :setIsOpen(true)}
};
return (
<>
<ModalContainer>
{/* TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.*/}
{/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
<ModalBtn onClick={openModalHandler}>
{isOpen ? 'Opened!': 'Open Modal'}
</ModalBtn>
{/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
{isOpen ?
<ModalBackdrop onClick={openModalHandler}>
<ModalView onClick={(event)=> event.stopPropagation()}>
{/*ModalBackdrop이 눌렸을때 ModalView까지 눌리지 않도록 눌림방지*/}
<div onClick={openModalHandler}>❌</div>
modal 창이 열렸음
</ModalView>
</ModalBackdrop>
: null
}
</ModalContainer>
</>
);
};
컴포넌트 ui - 토글
styled-component에서 > 문자는 CSS에서 자식 선택자를 나타내는 기호임다.
'> '는 CSS에서 자식 선택자(child selector)를 나타내는 표기법입니다.
이 경우, ToggleContainer 컴포넌트 내부의 자식 요소들 중에서 클래스 이름이 'toggle-container'인 요소를 선택합니다!!
(이 코드에서는 클래스를 다루니까 '.'을 붙여주죠)
즉, ToggleContainer 컴포넌트 내부에서 클래스 이름이 'toggle-container'인 요소들에 대해서 스타일을 적용합니다.
이를 통해, ToggleContainer 컴포넌트 내부에 여러 개의 요소가 있을 경우, 특정 요소에만 스타일을 적용할 수 있겠져..
'&' 기호는 부모 요소에 대한 참조를 나타냅니다.
이 기호를 사용하면 자식 요소의 클래스나 가상 클래스에 대한 스타일을 부모 요소의 선택자와 결합하여 작성할 수 있습니다.
예를 들어, 위의 코드에서 '&.toggle--checked'는 'toggle-container' 또는 'toggle-circle' 클래스를 가진 요소가 'toggle--checked' 클래스를 가지고 있을 때, 해당 요소의 스타일을 지정합니다.
(기존에 있는 클래스를 연줄삼아 새로운 클래스를 붙이고 싶을때 사용하는거죠 ㅎ)
클래스도 삼항연산자로 자유롭게 작성할 수 있다
``(백틱)안에 ${여기 안에다 연산식 작성} 넣어준다
<div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
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;
// TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
&.toggle--checked{
background-color: #34c759;
}
transition: background-color 0.5s ease-in-out;
}
> .toggle-circle {
position: absolute;
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #ffffff;
// TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
&.toggle--checked{
left: 27px;
}
transition: left 0.5s ease-in-out;
}
`;
const Desc = styled.div`
// TODO : 설명 부분의 CSS를 구현합니다.
text-align: center;
font-size: 15px;
margin-top:15px;
`;
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
// TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
isOn ? setisOn(false) : setisOn(true)
};
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>{isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}</Desc>
</>
);
};
컴포넌트 ui - 탭
currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다. 라고 했으니
state와 state 변경함수를 선언해줍니다
아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정하라고 했으니, map 함수를 써줍니다
{
menuArr.map((a,i)=>{
return (
<li className={`${currentTab == i ? "submenu focused" : "submenu"}`}
onClick={()=>{selectMenuHandler(i)}}>{menuArr[i].name}</li>
)
}
{} 안에서는 a,i,currentTab 등 선언된 변수만 적어야 합니다...
만약 index를 쓰면 not defined가 뜬다.
(제가 많이 틀렸거든요,,, 자꾸 위에서 쓰인 index를 가져와서 not defined가 떳어요 ㅠㅠ)
currentTab이라는 선언된 변수를 적어주고, map 파라미터에서 선언된 i는 0,1,2 씩 늘어나는 정수이다
currentTab의 값이 i와 같을때, 조건문을 적어준다
그리고 onClick 작성할때도 유의해주셔야 합니다
<onClick 작성법>
함수 자체를 전달해야 하므로 onClick={함수명} or 파라미터를 전달할거면 onClick={()=>{함수(i)}}
/*
chat gpt의 답변
문제의 원인을 파악하기 위해 먼저 코드를 확인해보았습니다.
문제의 원인은 onClick 이벤트 핸들러 함수가 잘못 작성되어 있기 때문입니다. onClick 핸들러 함수는 이벤트가 발생할 때 실행되어야 하지만, 현재 코드에서는 렌더링될 때마다 함수가 실행되고 있습니다.
이렇게 되면 무한 루프에 빠져 브라우저가 멈추는 현상이 발생합니다.
이를 해결하기 위해서는 onClick 핸들러 함수를 적절하게 수정해야 합니다. onClick 핸들러 함수에는 함수 자체를 전달해야 하기 때문에, 아래와 같이 수정할 수 있습니다.
*/
import { useState } from 'react';
import styled from 'styled-components';
// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.
const TabMenu = styled.ul`
background-color: #F0EB8D;
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: 5rem;
margin-top: 30px;
.submenu {
${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
padding: 1rem;
cursor: pointer;
}
.focused {
${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다. */}
background-color: #fff;
color: #000;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16);
border-radius: 0.5rem;
}
& div.desc {
text-align: center;
}
`;
const Desc = styled.div`
text-align: center;
`;
export const Tab = () => {
// TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
// currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
const menuArr = [
{ name: 'Tab1', content: 'Tab menu ONE' },
{ name: 'Tab2', content: 'Tab menu TWO' },
{ name: 'Tab3', content: 'Tab menu THREE' },
];
const [currentTab, setCurrentTab] = useState(0)
const selectMenuHandler = (index) => {
// TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
// TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 state가 갱신되도록 함수를 완성하세요.
setCurrentTab(index)
};
return (
<>
<div>
<TabMenu>
{/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
{/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며,
나머지 2개의 tab은 'submenu' 가 됩니다.*/}
{
menuArr.map((a,i)=>{
return (
<li className={`${currentTab == i ? "submenu focused" : "submenu"}`} onClick={()=>{selectMenuHandler(i)}}>{menuArr[i].name}</li>
)// currentTab이라는 선언된 변수를 적어주고, i는 0,1,2 씩 늘어나는 정수이다
// currentTab의 값이 i와 같을때, 조건문을 적어준다
// 만약 index를 쓰면 not defined가 뜬다. {} 안에서는 a,i,currentTab 등 선언된 변수만 적어야 한다
//onClick 작성법
// 함수 자체를 전달해야 하므로 onClick={함수명} or onClick={()=>{함수(i)}}
/*
문제의 원인을 파악하기 위해 먼저 코드를 확인해보았습니다.
문제의 원인은 onClick 이벤트 핸들러 함수가 잘못 작성되어 있기 때문입니다. onClick 핸들러 함수는 이벤트가 발생할 때 실행되어야 하지만, 현재 코드에서는 렌더링될 때마다 함수가 실행되고 있습니다.
이렇게 되면 무한 루프에 빠져 브라우저가 멈추는 현상이 발생합니다.
이를 해결하기 위해서는 onClick 핸들러 함수를 적절하게 수정해야 합니다. onClick 핸들러 함수에는 함수 자체를 전달해야 하기 때문에, 아래와 같이 수정할 수 있습니다.
*/
})
}
</TabMenu>
<Desc>
{/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
<p>{menuArr[currentTab].content}</p>
</Desc>
</div>
</>
);
};
'React' 카테고리의 다른 글
Cmarket Hooks (프로젝트 구조 파악의 중요성) (0) | 2023.04.21 |
---|---|
props와 state의 차이점 (0) | 2023.04.21 |
Styled Components 문법 총정리 (0) | 2023.04.18 |
localStorage로 만드는 최근 본 상품 저장하기 (0) | 2023.04.16 |
Redux-toolkit : 장바구니 기능 관련 응용문제 (0) | 2023.04.13 |