[TIL No.18]React_Custom_Components
오늘의 TIL은 내가 겪은 시행착오와 함께 지금까지 React 다루면서 한 번은 해봤지만 기억이 나지 않는 부분들과 새롭게 알게된 내용들을 과제진행의 흐름에 따라 서술해보겠다.
(CSS를 진짜 잘하시는 페어를 만나서 그냥 듣기만해도 CSS실력이 레벨업해버렸다.. ㅎㄷㄷ 그 내용도 중간중간 나올 것이다.)
오늘의 과제 - React로 custom components 만들기
만들어볼 컴포넌트들의 목록은 다음과 같다.
- modal
- toggle
- tap
- tag - 오늘 포스팅은 여기까지!!
- autocomplete
- click_To_Edit
페어분의 막힘없는 React 활용능력에 힘입어 뇌정지가 거의 없이 과제를 수행했으므로, 아예 새 파일로 다시 구현해보면서 다시 한 번 코드를 뜯어보며 글을 써보고자한다.
#1. modal 구현
modal구현은 CSS 도움을 정말 많이 받은 부분이다. 다른 컴포넌트들은 CSS가 구현이 되어있었는데 아마, modal이 처음 나오는 컴포넌트라 CSS도 구현해보는 부분을 많이 넣은 것 같다.
현재 깔끔하게 잘 작동되는 모달창이다.
CSS를 초기화해보고 다시 하나하나 적용해보자.
일단, Open Modal이라는 버튼의 크기와 위치가 맘에 들지 않는다. CSS를 어떻게 손봐야할지 모르겠어서 헤메다가 가장 먼저 전체 구조를 살펴야한다는 것을 뒤늦게 깨달았다.
먼저 Custom Component 이 페이지에서 modal,toggle,tap,tag,autocomplete,click_To_Edit 각각의 기능들은 class = 'box'로 동일한 클래스를 부여받아 병렬로 연결되어있다. 총 6개의 박스와 React Custom Component라는 'title'이란 id를 부여받은 div와도 동일한 계층이어서 타이틀 div 1개와 기능을 구현한 box div 6개, 총 7개의 div이 container라는 id를 가진 div에 자식들로 나란히 묶여있다.
그 중에서 지금 구현할 부분은 첫 번째 box div인 modal이고 그 안에 Modal이라고 적힌 부분과 Open Modal이라고 적혀있는 버튼 두 개가 box div의 자식으로 들어가 있는데, 이 부분을 CSS를 통해 보기 좋은 모달창으로 만드는 것이다. 거기에 더해 Open Modal을 누른 경우 팝업되는 창과 배경의 처리 역시 CSS를 통해서 적절하게 보여지도록 해야한다.
이렇게 정리하고나니 Open Modal 버튼의 위치가 거슬리는 것이 훨씬 수월하게 조정할 수 있게 되었다.
그런데, CSS gird는 여전히 잘 할줄 모르고 box model 중에서도 flex box만 몇 번 적용해 본 나로서는 display:flex 와 함께 justfy-content와 align-items를 둘 다 center로 놓는 만능 치트키를 쓴다고 바로 써봤는데 절반의 성공밖에 거두지를 못했다.
이 때 도움받은 것이, 현재 modal 텍스트와 버튼을 감싸고 있는 지금 편집중인 container div이 부모로서 크기가 정해지지 않아 버튼 크기에 딱 맞게 설정이 자동으로 되어버렸고 그 상태에서 중앙은 위아래 여백이 전혀 없기 때문에 현재 위치에 고정이 되어버리는 것이다.
그에따라 height 150, 300을 각각 주면 다음과 같이 변한다.
높이를 300px정도 주니 거의 정가운데가 되었다.(좌우도 modal text의 존재를 무시하고 가운데에 놓고 싶은데 보통 쉬운일은 아닐거라 생각이 들어 현재 상태에 일단 만족했다.)
이 상황에서 모달창을 구분하고 팝업된 창이 어떻게 나오는지 살펴보자.
열리긴 열렸는데 버튼 위에 오버레이 되는 느낌 없이 옆에 div 하나가 추가된 것처럼 보인다.
크기를 조정해보지만.. 원하는 느낌이 나오지 않았는데... 모달 창이 모달이 열렸을 때 ModalBackdrop이라고 이름 붙여진 배경과 함께 열리는데 이 모달 배경의 CSS가 작성이 안되어서 배경의 크기가 정해지지않아 아까와 같이 자식의 크기에 배경이 딱 맞추어지는 현상이 일어난 것이다. 이는 다음 사진에서 확인이 가능하다.
그래서 일단 배경의 CSS를 다음과 같은 상태에서
export const ModalBackdrop = styled.div`
background-color: rgba(0, 0, 0, 0.5);
`;
이렇게 바꿔주면...
export const ModalBackdrop = styled.div`
position: fixed;
background-color: rgba(0, 0, 0, 0.5);
top: 0;
left: 0;
right: 0;
bottom: 0; // 마진을 전부 없애주었다.
display: flex; // flex 적용과 함께 자식인 모달 창을 가운데 정렬을 해주었다.
justify-content:center;
align-items: center;
`;
다음과 같은 화면이 구현 가능하다.
자 이제 완성을 했으니 엣지케이스를 잡아볼까..?ㅋㅋㅋㅋ(구현한 코드는 밑에서~~)
1.modal버튼 바깥을 클릭해도 모달이 열리는 문제
오늘 세션에서도 설명해주셨지만, event bubbling 때문에 일어난 문제이다. event bubbling은 깊이있게 공부해보고 싶어서 공부 중이었으나, 좀 더 정리해서 다음에 포스팅하도록 하고 개념만 간단히 이해하고 문제를 바로 해결해보도록 하자.
버블링이란 간단히말해 자식 컴포넌트에서만 호출되기를 바랬던 이벤트가 부모 컴포넌트에서도 호출이 가능한(?) 또는 호출이 되버리는(??!) 현상이다.
영상과 같이 모달버튼 바깥을 클릭해도 버튼을 클릭할 때와 같은 효과가 있다.
화면에 나오는 코드를 살펴보면
return (
<> // 음?? 이렇게하면 뭐가 생기는거지...??!!
<ModalContainer onClick={openModalHandler}> //어제의 결과값인데 내가 넣었던걸까..?
<ModalBtn onClick={openModalHandler}>
{isOpen ? 'Opened!' : 'Open Modal'}
</ModalBtn>
{isOpen === false ? null
: <ModalBackdrop>
<ModalView>
<Exitbtn onClick={openModalHandler}>X</Exitbtn>
<div>Hello Code States!</div>
</ModalView>
</ModalBackdrop>
}
</ModalContainer>
</>
);
};
현재 이런 상황으로 모달 컨테이너에도 onClick이 있으므로 당연히 bubbling이 없어도 실행이된다고 생각할 것이다. 그렇다면 컨테이너에 onClick을 없애도 작동이 된다면 bubbling이 일어난다는 것을 확신할 수 있다.
앗... 근데 버블링 안일어... 나는데요...?? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ(동공지진....🥲)
일단은 다른 기능들을 구현해보고...ㅋㅋㅋㅋ 업데이트가 일부 사항에 대해서는 늦게 반영되는 경우도 자주 있으므로 일단 보류해놓자.
2. 토글버튼이 위로 올라오는 불편한 상황
이게 실제 웹페이지라고 한다면 모달이 켜진상태에서 예를들면 다크모드 토글같은 것이 작동이 안되어야 맞는 것인데 모달 창이 토글과 동일한 레이어(?)에 있어서 그런 것인지 모달창이 켜진상태에서 작동이 되어 이 문제를 수정하고 싶다.
(to be continue...)
잡다한 추가정보
1) attrs
사실 이건 당최 뭔지... 잘 모르겠어서 찾아보았다. (활용법)
export const ModalView = styled.div.attrs((props) => ({
// attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
role: 'dialog',
}))`
... (중략)
align-items: center;
`;
ModalView에 커서를 올려보니 속성이 추가되어있는 것을 알 수 있다.
이렇게 설정해주면 내재적으로 설정이 되는데, 다른 기능도 속성을 자동으로 추가할 수 있는지 궁금했다. 그래서 모달을 여는 버튼에 name이라는 속성에 "Open or Close" 이라는 값을 주고 저장해보니 다음과 같이 버튼이 생성되면서 자동 적용되는 것을 알 수 있었다.
이는 Modalbtn을 재사용해서 만든 Exitbtn에도 동일하게 들어가는 것을 확인할 수 있었다..!! 조금 더 심화적인 내용도 나중에 계속 활용하면서 알아보고 싶어졌다. 이게 상속되는 것이면 상속을 끊는 방법도 있는 것인지.. 그런 것들 말이다.
2) 모달 npm
모달을 구현해서 설치해서 쓸 수 있는 npm도 있었다. https://www.npmjs.com/package/react-modal
react-modal
Accessible modal dialog component for React.JS. Latest version: 3.16.1, last published: 4 months ago. Start using react-modal in your project by running `npm i react-modal`. There are 2527 other projects in the npm registry using react-modal.
www.npmjs.com
npm이 총 몇십만개였나..? 그렇다고 들었는데 이런 것까지 다 만들어놔서 그런가보다 싶었다..ㅋㅋㅋㅋ 초보 입장에서 활용할 줄만 알면 당장 가져다쓰고 나중에 코드를 뜯어보면서 실력 늘리기 좋은 것 같다.(근데 나도 초보...니까 이런 코드들을 더 열심히 뜯어서 고수가 되자🐻)
2. toggle 구현
토글은 조금 더 체계적으로 어떻게 구현할지 살펴보고 코드와 함께 구현한 내용을 돌아보자.
주어진 상황
- Toggle 컴포넌트에는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
- isOn state는 토글 버튼의 on/off 여부를 확인하는 상태이다.
- ToggleContainer 컴포넌트는 토글 버튼 제어를 위해 핸들러 함수 toggleHandler를 작성합니다.
- toggleHandler 함수는
- ToggleContainer 클릭 시 발생되는 change 이벤트 핸들러입니다.
- 클릭할 때마다 상태가 Boolean 값으로 변경됩니다.
- toggleHandler 함수는
구현 과제 목록
- Styled Components 라이브러리를 활용해 ToggleContainer Desc 컴포넌트의 CSS를 자유롭게 구현합니다.
- ToggleContainer : Toggle을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 합니다.
- Desc : Toggle Switch의 상태를 설명하는 텍스트를 담는 컴포넌트입니다.
- ToggleContainer 내부에 .toggle-container .toggle-circle 클래스를 가진 div 요소를 각각 생성합니다.
- 생성한 요소에 조건부 스타일링을 활용해 Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 두 요소 모두에 추가합니다.
- 기본 CSS에서는 템플릿 리터럴과 삼항 연산자를 활용해 조건부 스타일링을 적용할 수 있습니다.
1 <div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
- 조건부 렌더링을 활용해 Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON' 으로 Toggle Switch가 OFF인 상태일 경우에는 'Toggle Switch OFF'로 변경합니다.
- 토글 스위치가 부드럽게 옮겨지는 애니메이션 효과를 주기 위해서는 CSS의 transition 속성을 활용할 수 있습니다. 토글을 기능을 다 구현했다면 시도해 보세요!
(1) Styled Components를 활용해 Desc 컴포넌트의 CSS 구현하기
마음 같아서는 전부 다시 해보고 여기에 적어두고 싶으나.. 원상복귀하기도 힘들고(귀찮고...) 블로깅의 효율성을 위해 기능구현을 중심으로 살펴보자. CSS적으로 큰 변화라면,,,
토글을 토글 창 중앙에 오도록 한 것과, 예시와 비슷하도록 토글 circle의 크기를 조정하고 활성화 되었을 때 색을 변경한 정도이다.
(2) 토글의 기능 구현
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
// isOn의 상태를 변경하는 메소드, 토글이 켜져있으면 true, 꺼진 경우 false 값을 가지도록 한다.
setisOn(!isOn);
};
return (
<>
<ToggleContainer onClick={toggleHandler}>
{/*클릭하면 토글이 켜진 상태(isOn)를 boolean true로 변경하는 메소드가 실행되도록 설정.*/}
<div className={`toggle-container ${isOn ? "toggle--checked" : null}`}/>
<div className={`toggle-circle ${isOn ? "toggle--checked" : null}`}/>
{/* 두 개의 div이 토글이 켜졌는지에 따라 "toggle--checked" class를 가졌다가 가지지 않았다가 전환된다.*/}
</ToggleContainer>
<Desc>{isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}</Desc>
{/* 토글의 on/off 여부에 따라 Desc 컴포넌트 내부의 text를 변경하도록 설정.*/}
</>
);
};
토글의 상태는 on/off 여부를 담을 수 있도록 useState를 활용해 isOn 한 가지를 통해 관리한다.
ToggleContainer에는 click 될 시 toggle의 isOn 상태를 변경하는 함수 toggleHandler를 실행하도록 onClick 속성 값을 준다.
컨테이너 안의 div들의 클래스명과 컨테이너와 형제인 Desc 컴포넌트의 text가 토글의 on/off 에 따라 변경되도록 isOn의 값에 따라 변경되도록 조건부 스타일링을 이용했다.
여기서 꿀팁을 또 얻었는데,
<div className={`toggle-container ${isOn ? "toggle--checked" : null}`}/>
3. tap 구현
tap 기능 역시 주어진 과제와 상황을 나열해놓고 내가 구현한 기능에 중심을 둬서 정리해보자⭐
주어진 상황
- Tab 컴포넌트는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
- currentTab state는 현재 tab의 index를 확인할 수 있습니다.
- TabMenu 컴포넌트는 토글 버튼 제어를 위해 핸들러 함수 selectMenuHandler 를 가집니다.
- selectMenuHandler 함수는
- TabMenu 클릭 시 발생되는 change 이벤트 핸들러입니다.
- 클릭할 때마다 상태가 index 값으로 변경됩니다.
- selectMenuHandler 함수는
- li 요소를 이용해 메뉴를 생성하고, 각 메뉴를 눌렀을 때 뷰가 전환되도록 handler(selectMenuHandler) 함수를 작성합니다.
- 조건부 스타일링과 currentTab 상태를 이용하여 클릭한 Tab 메뉴만 className(submenu focused)과 CSS 가 변경되도록 구현합니다. 조건부 스타일링은 앞서 토글에서 구현한 것과 같이 템플릿 리터럴과 삼항 연산자를 활용할 수 있습니다.
구현 과제 목록
Component
- Styled Components 라이브러리를 활용해 TabMenu Desc 컴포넌트의 CSS를 자유롭게 구현합니다.
- TabMenu : Tab을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 합니다.
- Desc : Toggle Switch의 상태를 설명하는 텍스트를 담는 컴포넌트입니다.
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로 바뀌어야 합니다.
기본 tap 메뉴와 선택된 tap에 적용되는 CSS를 적당히 파란색으로 적용했다... 넘어가자.
💡각각의 tab의 width를 33.3%로 적용해서 나누었는데, width : calc(33.3%)와 동일한 효과이며, calc의 경우 calc(33% -1em)이런 식으로 다양한 변형이 가능해서 더 유용하게 느껴졌다.
MDN calc의 활용
위 기능들을 구현하면서 CSS나 조건부 렌더링에 대해 어느정도 알아보았으므로, 상태관리와 기능구현 메소드 중 주요한 부분만 알아보고 자잘한 부분들은 생략하고자 한다.
(1) Tab에 존재하는 상태
선택된 tab이 어떤 tab인지 index로 관리하는 currentTab이라는 state가 존재한다. menuArr라는 tab 메뉴 객체가 담겨있는 배열에서 각각의 index와 currentTab의 상태 값을 일치시켜서 원하는 화면을 렌더링 할 수 있도록 관리한다.
(2) selectMenuHandler 메서드
실행 시 index값을 받아서 currentTab의 값을 index 값과 일치하도록 만들어주는 함수.
(3) tab 기능의 구현
위와 같은 상태와 메서드를 이용하여 구현된 코드는 다음과 같다.
return (
<>
<div>
<TabMenu>
{menuArr.map((menu, index) => {
return (
<li key={index} className={currentTab === index? 'submenu focused' : 'submenu'}
onClick={() => selectMenuHandler(index)}>{menu.name}</li>
)
}
)}
{/* currentTab의 값과 일치하는 index의 요소만 submenu, focused 두 가지의 속성을 가지며, 나머지 요소는 submenu 속성만 가지도록 한다.*/}
</TabMenu>
<Desc>
<p>{menuArr[currentTab].content}</p>
{/* 선택된 탭의 내용을 보여주는 부분 */}
</Desc>
</div>
</>
);
};
4.tag 구현
tag의 경우 tags라는 상태를 하나 가진다. 이 상태를 통해 추가되는 태그와 삭제되는 태그를 모두 관리할 수 있다. tags에는 현재 존재하는 태그들이 문자열의 형태로 배열의 요소로 들어있다.
const initialTags = ['Tag', 'made by'];
// 초기 태그들이 변수로 선언되어 tags의 초기값으로 들어간다.
const [tags, setTags] = useState(initialTags);
이 상태를 변경하는 이벤트 핸들러 함수가 두 가지가 있다. addTags와 removeTags이다. 각각 태그를 추가 및 제거하는 함수이다.
addTags 메소드는 태그 추가기능 외에도 (1) 이미 입력되어있는 태그인지 검사하고 이미 있으면 추가하지 않도록하고,
(2) 아무것도 입력되지 않은 채 Enter 입력시 메소드를 실행하지 말아야하며,
(3) 태그가 추가되면 input 창을 비우는 기능을 해야한다.
const addTags = (event) => {
const inputValue = event.target.value;
// inputValue가 비어있지않고 태그가 이미 포함되어있지않은 경우에만 엔터입력 시 작동하도록 함.
if(event.key === "Enter" && inputValue !== '' && !tags.includes(inputValue)){
setTags([...tags, inputValue]);
event.target.value = '';
// 작동 후 if문 빠져나가기 전에 input 창을 초기화 해준다.
}
};
논리연산자로 깔끔하게 엣지케이스가 발생하지않도록 위에서 요구하는 조건을 모두 만족시켰다.
removeTags 메소드는 해당 태그 안에 있는 span을 클릭할 경우 작동되게 되어있는 것을 이용하여 tags 상태에 있는 목록 중 클릭한 태그만 제외하고 나머지를 return하도록 filter를 사용하여 한번에 변경해주도록 하였다.
const removeTags = (indexToRemove) => {
setTags(tags.filter((tag, index) => index !== indexToRemove));
};
그리하여 리턴되는 코드와 화면을 살펴보면 다음과 같다.
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)}>X</span>
</li>
))}
</ul>
<input
className="tag-input"
type="text"
onKeyUp={(event) => {addTags(event);}}
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
태그들이 나열된 li를 ul로 감싸주었고 이 ul가 input창과 나란히 TagInput 엘리먼트의 자식으로 존재한다. 각각의 li 요소 하나하나는 각 태그들로 태그명을 표시하는 span과 태그를 삭제할 수 있는 X모양의 버튼처럼 보이는 span으로 구성되어있다.
auto-complete를 만들다가 시간이 다 되어서 못했는데 자동완성 기능과 클릭 투 에딧 기능은 내일 같이 정리해보도록 하자.
추가적인 정보 2
Styled-component에 hover 적용하는 방법