코드스테이츠_S3U3_2,3W_목,금,월

윤뿔소·2022년 10월 27일
0

CodeStates

목록 보기
30/47

전에 배웠던 UI 컴포넌트들을 리액트로 만들어보는 시간을 갖겠다!
왜냐? CCD를 빌려 중복되는 기능은 복사해서 사용하면 되니까!
이거 배워서 리액트 리팩토링 그거 써ㅏ보자!!@

CDD

Component Driven Development, 중복되는 UI 디자인 컴포넌트들을 컴포넌트 중심으로 만들어 조립식(Bottom-up)으로 만드는 방법!걍 재사용가능한 UI를 개발하는 식으로 함

CSS의 진화

  • 기본 CSS는 기본적으로 노다가(태그 열고닫고, 색 찾고), 클래스와 상속 등으로 반복돼 CSS 자체가 무거워짐
  • 트렌드인 CDD 방식으로 개발함에 따라 각각에 맞는 CSS를 적용케 하는 기술이 필요해 졌음, 왜?!
    1. 프로젝트의 규모나 복잡도가 점점 커지고 함께 작업해야 할 팀원 수도 많아짐에 따라 CSS를 작성하는 일관된 패턴이 없다는 것
    2. 다양한 디바이스들의 등장으로 웹사이트들이 다양한 디스플레이를 커버해야 하기 때문에 CSS는 더 복잡

그래서 CSS를 보다 체계적이고 알아보기 쉽게 구조화하기 시작한 방법, 라이브러리들이 나오게 됐음

SASS

Syntactically Awesome Style Sheets, CSS를 확장해주는 '스크립팅 언어'이자 CSS 전처리기(CSS Preprocessor)
즉, JS처럼 프로그래밍 개념(변수, 함수, 상속 등)을 가져와 변수를 선언하고 여러번 재사용하게 만들어졌다!

구조화는 해결 됐지만 결국 메인 스크립트가 커지고 중복되는 컴포넌트가 많을 수록 중복을 처리 못해 CSS도 무거워지는 현상은 막지 못했다. 그러면?

이러한 SASS의 문제를 보완하기 위해 BEM, OOCSS, SMACSS 같은 CSS 방법론이 대두됐다. 서로 다르지만 같은 지향점을 가지고 있다.

  • 코드의 재사용
  • 코드의 간결화(유지 보수 용이)
  • 코드의 확장성
  • 코드의 예측성(클래스 명으로 의미 예측)

그러므로 팀원 간 CSS 규칙을 지켜서 원만한 협업토록 하자

BEM

Block, Element, Modifier로 구분하여 클래스명을 작성하는 방법, 각각 —와 __로 구분
BEM 방식의 이름을 사용해 재사용, 더 깔끔한 코드 생성 가능

하지만 이러한 방법론도 문제가 생긴다.

  • 클래스명 선택자가 장황
  • 이런 긴 클래스명 때문에 마크업이 불필요하게 커짐
  • 재사용하려고 할 때마다 모든 UI 컴포넌트를 명시적으로 확장

즉! 개발 방법의 트렌디화로 로직상 캡슐화(은닉화)가 대두됐지만 방법론들은 유일한 클래스명을 선택하는 것에 의존해 한계가 생겼다.

CSS-in-JS

결국 컴포넌트 단위의 개발로 진화하면서 캡슐화가 중요해졌다. CSS도 함께 컴포넌트의 영역으로 함께 가기위해 CSS-in-JS가 생겼고 대표적인 것이 'Styled-Component' 라이브러리다!

Styled-Component

기능적(Functional) 혹은 상태를 가진 컴포넌트들로부터 UI를 완전히 분리해 사용할 수 있는 아주 단순한 패턴을 제공

다음 항목을 보고 자세하게 알아보자

Styled-Component

  • 이런 CSS에 관한 문제 상황이 있었다.
    • class, id 이름을 짓느라 고민한 적이 있다.
    • CSS 파일 안에서 내가 원하는 부분을 찾기 힘들었다.
    • CSS 파일이 너무 길어져서 파일을 쪼개서 관리해본 적이 있다.
    • 스타일 속성이 겹쳐서 내가 원하는 결과가 나오지 않은 적이 있다.
  • 모두 없앨 수 있는 방법은 CSS를 컴포넌트화하고 그 라이브러리는 Styled-Component!

사용법

// 설치
// with npm
$ npm install --save styled-components
// with yarn
$ yarn add styled-components
// package.json에 다음 코드를 추가(필수), 하나의 버전만 쓰게
{
  "resolutions": {
    "styled-components": "^5"
  }
}
// 파일에
import styled from "styled-components"

1. 컴포넌트 만들기

  1. 템플릿 리터럴 꼭꼭 사용
  2. 컴포넌트 선언 후 styled.태그종류를 할당
  3. CSS를 작성하던 문법과 똑같이 안에 그대로 작성
  4. 작성한 변수를 리액트 컴포넌트처럼 리턴에 기입
import styled from "styled-components";

// Styled Components로 컴포넌트를 만들고
const BlueButton = styled.button`
  background-color: blue;
  color: white;
`;
export default function App() {
  // React 컴포넌트를 사용하듯이 사용
  return <BlueButton>Blue Button</BlueButton>;
}

2. 컴포넌트 재활용 하기

  1. 컴포넌트 선언 후 styled()에 재활용할 컴포넌트를 전달
  2. 추가하고 싶은 스타일 속성을 작성
  3. 똑같이 리턴에 기입
// 만들어진 컴포넌트를 재활용해 컴포넌트를 만듦
const BigBlueButton = styled(BlueButton)`
  padding: 10px;
  margin-top: 10px;
`;

// 재활용한 컴포넌트를 재활용 가능
const BigRedButton = styled(BigBlueButton)`
  background-color: red;
`;

export default function App() {
  return (
    // 여러개니 열고 닫힌 태그 작성
    <>
      <BlueButton>Blue Button</BlueButton>
      <BigBlueButton>Big Blue Button</BigBlueButton>
      <BigRedButton>Big Red Button</BigRedButton>
    </>
  );
}

3. CSS의 Props화

React 컴포넌트처럼 props를 내려줄 수 있음

  1. 1번에서 했던 것처럼 선언 후 작성하고 CSS 속성을 작성하는데 템플릿 리터럴 작성하기
  2. Props 내려주는 것처럼 리턴에 값 기입해주기
import styled from "styled-components";
import GlobalStyle from "./GlobalStyle";
// 받아온 prop에 따라 조건부 렌더링이 가능
const Button1 = styled.button`
  background: ${(props) => (props.skyblue ? "skyblue" : "white")};
`;

export default function App() {
  return (
    <>
      <GlobalStyle />
      <Button1>Button1</Button1>
      // props 전달하기
      <Button1 skyblue>Button1</Button1>
    </>
  );
}
  1. Props의 값을 통째로 활용해서 컴포넌트 렌더링에 활용 가능
import styled from "styled-components";
import GlobalStyle from "./GlobalStyle";

// 받아온 prop 값을 그대로 이용해 렌더링 가능
const Button1 = styled.button`
  background: ${(props) => (props.color ? props.color : "white")};
`;
// 다음과 같은 형식으로도 활용 가능, or 연산자를 이용
const Button2 = styled.button`
  background: ${(props) => props.color || "white"};
`;

export default function App() {
  return (
    <>
      <GlobalStyle />
      <Button1>Button1</Button1>
      <Button1 color="orange">Button1</Button1>
      <Button1 color="tomato">Button1</Button1>
      <br />
      <Button2>Button2</Button2>
      <Button2 color="pink">Button2</Button2>
      <Button2 color="turquoise">Button2</Button2>
    </>
  );
}

4. 전역 스타일 지정하기

디테일한 스타일들을 지정해도 되지만 전역은 구조상 안된다. 그래서 따로 내장된 createGlobalStyle을 지정해서 사용하자

  1. 불러오기
    import { createGlobalStyle } from "styled-components";
  2. CSS처럼 작성하기
  3. 리턴에 최상위 컴포넌트로 사용하기
import styled from "styled-components";
import { createGlobalStyle } from "styled-components";
// CSS 처럼 작성
const GlobalStyle = createGlobalStyle`
	button {
		padding : 5px;
    margin : 2px;
    border-radius : 5px;
	}
`;

export default function App() {
  return (
    <>
      // 최상위로 적용
      <GlobalStyle />
      <Button>전역 스타일 적용하기</Button>
    </>
  );
}

참고: 3번의 항목 예시처럼 파일로 먼저 전역스타일 지정 후 불러오는 것이 많음

+ SASS처럼 &로 자기 스타일을 지정 가능함 즉! 가상 클래스 선택자 같은, :hover같은 것도 할 수 있다!

const Button = styled.button`
  color: white;
  min-width: 120px;
  /* & 문자를 사용하여 Sass 처럼 자기 자신 선택이 가능 */
  &:hover {
    background-color: white;
    color: black;
  }
  & + button {
    margin-left: 1rem;
  }
`;

예제

  1. 버튼 만들기
import styled from "styled-components";
import { createGlobalStyle } from "styled-components";

const GlobalStyle = createGlobalStyle`
  * {
    margin: 0.5rem;
  }
`;
const Button1 = styled.button`
  padding: 1rem;
  font-size: 2rem;
  background-color: powderblue;
  border-radius: 1rem;
  transition: 0.5s;
  &:hover {
    background: cornflowerblue;
    color: white;
    transition: 0.5s;
  }
`;

export default function App() {
  return (
    <>
      <GlobalStyle />
      <Button1>Practice!</Button1>
    </>
  );
}

참고: 전역 스타일 지정은 꼭 태그 {}로 만들어야함, & 이걸로 자신을 지정함

Storybook

  • 각 컴포넌트의 UI 개발을 위함(CDD)
  • 왜 필요? 컴포넌트 만들기에 집중할 수 있어서
    • 독립적인 개발환경, 앱의 다양한 상황에 구애받지 않고 개발 가능
      • 복잡한 개발 스택을 시작할 때
      • 특정 데이터를 데이터베이스로 강제 이동하고 싶을 때
      • 애플리케이션을 탐색할 필요 없이 전체 UI를 한눈에 보고 개발하고 싶을때
    • 각각의 컴포넌트들을 따로 볼 수 있게 구성해줘 한 번에 하나의 컴포넌트에서 작업 가능
      • 재사용성을 확대하기 위해 컴포넌트를 문서화
      • 자동으로 컴포넌트를 시각화하여 시뮬레이션할 수 있는 다양한 테스트 상태를 확인 가능, 즉! 버그를 사전에 방지
      • 테스트 및 개발 속도를 향상시킴
      • 애플리케이션 또한 의존성을 걱정하지 않고 빌드 가능!
  • Storybook의 기능
    • UI 컴포넌트들을 카탈로그화하기
    • 컴포넌트 변화를 Stories로 저장하기
    • 핫 모듈 재 로딩과 같은 개발 툴 경험을 제공하기
    • 리액트를 포함한 다양한 뷰 레이어 지원하기

⭐️+스토리북도 배워서 포트폴리오화 할 수도 잇고 되게 좋음 프론트엔드한테는 무조건 알아야할!

설치 및 실행

  1. 설치: 리액트 CRA로 폴더 만든 후 명령
1. 폴더 만들기
npx create-react-app storybook-practice
2. 설치
npx storybook init
  1. 생성 뒤 구조
    • /.storybook 폴더: Storybook 관련 설정 파일
    • /src/stories 폴더: Storybook 예시 파일
  2. 실행
    • npm run storybook 입력 뒤 localhost:6006 접근돼 실행
    • 앱을 실행하고 이벤트를 통해 상태를 변경하는 과정을 거치지 않아도 상태 변화에 따른 컴포넌트의 변화를 확인 가능!

작성법

1. 리액트 컴포넌트 만들기

src/Title.js: 타이틀 만든 리액트 컴포넌트 만들고 export

import React from "react";

// title은 h1 요소의 textContent, textColor은 글자색이 되는 props
const Title = ({title, textColor}) => (
<h1 style={{color: textColor}}>{title}</h1>
);

export default Title;

2. story 만들기

src/Title.stories.js: .stories 붙이면 알아서 인식(.test 같이)

// 앞에서 작성한 컴포넌트를 불러옵니다.
import Title from "./Title";

// title : 컴포넌트 이름으로, '/'를 넣어 카테고리화 가능, 이후 예시에서 조금 더 자세히 설명
// component : 어떤 컴포넌트를 가져와서 스토리로 만들 것인지 명시
// argTypes : 컴포넌트에 필요한 전달인자의 종류와 타입을 정함
//            지금은 title, textColor이라는 전달인자에 text 타입이 필요함을 의미
export default {
    title: "Practice/Title", 
    component: Title,
    argTypes: {
        title: { control: "text" },
        textColor: { control: "text" }
    }
}

// 템플릿을 제작, 이 템플릿에서는 Title 컴포넌트가 args를 전달받아 props로 내려줌
const Template = (args) => <Title {...args} />

// Storybook에서 확인하고 싶은 컴포넌트는 export const로 작성
// 템플릿을 사용하여 Storybook에 넣어줄 스토리를 하나 제작
// Template.bind({});는 정해진 문법이라고 생각하고 사용
export const RedTitle = Template.bind({});

// 만들어준 스토리의 전달인자를 작성
RedTitle.args= {
    title: "Red Title",
    textColor: "red"
}

// 스토리를 하나 더 제작 가능
export const BlueTitle = Template.bind({});

// 스토리의 전달인자를 작성
BlueTitle.args= {
    title: "Blue Title",
    textColor: "blue"
}

3. 전달인자를 바로 받게끔

맨 아래에 작성 후 보면 하나가 더 생겼음, 이건 우리가 적어주면 그대로 적용됨

...
export const StorybookTitle = (args) =>{
    return <Title {...args} />
}

4. Styled-Components와 함께 Props 사용 가능

  1. Button.js 만들기: props 적용 돼있음
  2. Button.stories.js 만들기: 전달인자를 그대로 받고 있음
// 컴포넌트를 불러옴
import Button from "./Button";

export default {
    title: "Practice/Button",
    component: Button,
	// 이번에 작성한 전달인자의 타입은 Storybook을 보고 직접 확인
    argTypes: {
        color: { control: 'color'},
        size: { control: { type:'radio', options : ['big', 'small'] }},
        text: { control: 'text'}
    }
};
// 전달인자 그대로
export const StorybookButton = (args) => (
    <Button {...args}></Button>
)

핵심: control: ‘color’control: { type: ‘radio’, options: [] }이 어떻게 적용되는지?!

useRef

리액트 앱은 DOM을 직접 조작하는 건 안좋다고 배웠지만 건드려야하는 상황이 있을 수 있다. 그때 리액트의 useRef 라는 Hook 함수를 이용해 조작해야한다.

  • 리액트의 문제: DOM 엘리먼트의 주소값을 활용해야하는 경우 충족 불가능
    • focus
    • text selection
    • media playback
    • 애니메이션 적용
    • d3.js, greensock 등 DOM 기반 라이브러리 활용

사용법

  • 예외적인 상황에서 쓰기: DOM 노드, 엘리먼트, 그리고 React 컴포넌트 주소값을 참조
const 주소값을_담는_그릇 = useRef(참조자료형)
// 이제 주소값을_담는_그릇 변수에 어떤 주소값이든 담을 수 있음
return (
    <div>
      <input ref={주소값을_담는_그릇} type="text" />
        {/* React에서 사용 가능한 ref라는 속성에 주소값을_담는_그릇을 값으로 할당하면*/}
        {/* 주소값을_담는_그릇 변수에는 input DOM 엘리먼트의 주소가 담김 */}
        {/* 향후 다른 컴포넌트에서 input DOM 엘리먼트를 활용 가능 */}
    </div>);

이 주소값은 re-render하더라도 바뀌지 않음, 그래서 useRef 활용

  • 예시
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>);
}

참고: 제시된 상황 제외한 대부분의 경우 기본 React 문법을 벗어나 useRef 를 남용하는 것은 부적절하고, React의 특징이자 장점인 선언형 프로그래밍 원칙과 배치되기 때문에, 조심해서 사용

예제: focus

만들어해보기
각각 ref로 주소를 연결시켜 엔터를 누르고 조건에 맞으면 포커스되고 초기화 되게끔 만들어놈

import React, { useRef } from "react";

const Focus = () => {
  const firstRef = useRef(null);
  const secondRef = useRef(null);
  const thirdRef = useRef(null);

  const handleInput = (event) => {
    console.log(event.key, event);
    if (event.key === "Enter") {
      if (event.target === firstRef.current) {
        secondRef.current.focus();
        event.target.value = "";
      } else if (event.target === secondRef.current) {
        thirdRef.current.focus();
        event.target.value = "";
      } else if (event.target === thirdRef.current) {
        firstRef.current.focus();
        event.target.value = "";
      } else {
        return;
      }
    }
  };

  return (
    <div>
      <h1>타자연습</h1>
      <h3>각 단어를 바르게 입력하고 엔터를 누르세요.</h3>
      <div>
        <label>hello </label>
        <input ref={firstRef} onKeyUp={handleInput} />
      </div>
      <div>
        <label>world </label>
        <input ref={secondRef} onKeyUp={handleInput} />
      </div>
      <div>
        <label>codestates </label>
        <input ref={thirdRef} onKeyUp={handleInput} />
      </div>
    </div>
  );
};

export default Focus;

예제: media playback

import { useRef } from "react";

export default function App() {
  const videoRef = useRef(null);

  const playVideo = () => {
    videoRef.current.play();
    console.log(videoRef.current);
  };

  const pauseVideo = () => {
    videoRef.current.pause();
    videoRef.current.remove();
  };

  return (
    <div className="App">
      <div>
        <button onClick={playVideo}>Play</button>
        <button onClick={pauseVideo}>Pause</button>
      </div>
      <video ref={videoRef} width="320" height="240" controls>
        <source
          type="video/mp4"
          src="https://player.vimeo.com/external/544643152.sd.mp4?s=7dbf132a4774254dde51f4f9baabbd92f6941282&profile_id=165"
        />
      </video>
    </div>
  );
}

페어 과제

자주 나오는 디자인 패턴을 구조화해서 디자인 시스템을 직접 구현해보자!

  • 참고
    • 디자인 시스템이란 서비스를 만드는 데 사용한 공통 컬러, 서체, 인터랙션, 각종 정책 및 규정에 관한 모든 컴포넌트를 정리해놓은 것이며 불필요한 커뮤니케이션을 없애기 위해 체계적으로 정리한 시스템
    • UI 컴포넌트는 사용자 인터페이스를 이루는 조각들의 시각적이고 기능적인 속성을 캡슐화하도록
    • 최근의 UI들은 다양한 UX를 위해 수백 개의 모듈식 UI 컴포넌트가 재배열된 구조로 제작
    • 스토리북은 최고임!

기본 전제

npm script

  • npm run storybook : 각각의 UI 컴포넌트들을 애플리케이션 외부의 독립된 환경에서 개발할 수 있도록 도와줌. 스크립트 실행 후 http://localhost:6006 에서 Storybook을 확인

구조

├── /React Custom Component
│   ├── README.md
│   ├── /public  # create-react-app 폴더로 yarn/npm start로 실행에 쓰임
│   └── /src
│        ├── /components    # 단일 UI React 컴포넌트가 들어가는 폴더
│        ├─── /__test__                 # 테스트 케이스가 들어가는 폴더
│        ├─── /AdvancedChallenges       # Advanced Challenges 를 위한 폴더
│        ├─── /BareMinimumRequirements  # Bare Minimum 을 위한 폴더
│        ├── /stories       # Storybook이 작동하는 데 필요한 파일들이 들어가는 폴더
│        ├── app.css
│        ├── App.js         # React Custom Component App
│        ├── index.js
├  package.json
└ .gitignore

시작: 베어미니멈

모달, 토글, 탭, 태그를 구현하자

모달

  1. isOpen state 존재, 모달 창의 열고 닫힘 여부를 확인하는 상태임
  2. openModalHandler()로 상태 조정 함수를 만듦
  3. 모달버튼을 누르면 isOpentrue가 되고 ModalBackdropModalView가 있는 형태로 레이어를 까는 형태(팝업x)
  4. 모달이 열리면 ModalBtn의 내부 텍스트가 바뀜(Opened!)
  5. Styled-Components로 CSS 구현
import { useState } from "react";
import styled from "styled-components";

export const ModalContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  position: relative;
`;

export const ModalBackdrop = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: rgba(0, 0, 0, 0.8);
`;

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;
  &:hover {
    background-color: var(--coz-purple-400);
  }
`;

export const ModalCloseBtn = styled.button`
  background-color: #fff;
  color: #000;
  text-decoration: none;
  border: none;
  position: absolute;
  top: 10%;
  cursor: pointer;
  font-size: 20px;
`;

export const ModalView = styled.div.attrs((props) => ({
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가
  role: "dialog",
}))`
  position: absolute;
  top: calc(50vh - 100px);
  left: calc(50vw - 200px);
  background-color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 10px;
  width: 400px;
  height: 150px;
`;

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = (event) => {
    // TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
    setIsOpen(!isOpen);
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn
          // TODO : 클릭하면 isOpen의 boolean 타입으로 변경되는 메소드가 실행
          onClick={openModalHandler}
        >
          {isOpen ? "Opened" : "Open Modal"}
        </ModalBtn>
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태일 때만 모달창과 배경이 뜰 수 있게 구현 */}
        // 이벤트 버블링으로 하얀색을 눌러도 꺼짐 해결해야함 
        // 1. 부모관계를 끊거나(CSS 수정,,) 
        // ⭐️2. 모달창 그 자체에 이벤트를 걸기: stopPropagatio
        {isOpen ? (
          <ModalBackdrop onClick={openModalHandler}>
            <ModalView onClick={(event) => event.stopPropagation}>
              <ModalCloseBtn onClick={openModalHandler}>x</ModalCloseBtn>
              안녕 나는 코드스테이츠!🦏
            </ModalView>
          </ModalBackdrop>
        ) : null}
      </ModalContainer>
    </>
  );
};

핵심

  1. CSS 복습하자,, 가운데 정렬(flex), relative-absolute와 LRUB위치 설정 등
  2. null을 설정해 의도적으로 렌더링 되지 않게 할 수 있음!
  3. ⭐️부모에 이벤트를 적용시키면 '이벤트버블링'이 일어나 의도치않은 이벤트를 할당할 수도 있다.(모달의 하얀색을 눌러도 꺼짐)
    • 그럴 땐 원치 않는 곳에 이벤트를 넣어주자 event.stopPropagation()
    • 리액트(속성)는 {(event) => event.stopPropagation}
    • DOM은
      const clickEvent = (e) => {
      e.stopPropagation();
      console.log(e.currentTarget.className);
      };

토글

  1. 모달 버튼과 비슷하게 isOn이라는 상태가 존재, toggleHandler로 변경
  2. 토글을 누르면 isOn의 불린데이터 값에 따라 Desc 멘트에 ON/OFF 바뀌고, 스위치가 좌우로 왔다갔다함
  3. Styled-Components로 CSS 구현: 동그라미 위치 지정(노가다;;), 색 바뀌게
import { useState } from "react";
import styled from "styled-components";

const ToggleContainer = styled.div`
// Styled-Component 중략
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  text-align: center;
  margin: 20px;
`;

export const Toggle = () => {
  const [isOn, setIsOn] = useState(false);

  const toggleHandler = (event) => {
    // TODO : isOn의 상태를 변경하는 메소드를 구현
    setIsOn(!isOn);
  };

  return (
    <>
      <ToggleContainer
        onClick={toggleHandler}
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
      >
        {/* 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>
    </>
  );
};

import { useState } from "react";
import styled from "styled-components";

const TabMenu = styled.ul`
// Styled-Component 중략
`;

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)는 X
    // TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성
    setCurrentTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 됨.*/}
          {/* <li className="submenu">{menuArr[0].name}</li>
          <li className="submenu">{menuArr[1].name}</li>
          <li className="submenu">{menuArr[2].name}</li> */}
          {menuArr.map((el, idx) => {
            return (
              // onClick에서 그냥 'selectMenuHandler(idx)'이 아니라 '() => selectMenuHandler(idx)'인 이유??
              // 인자가 들어가려면 화살표 함수 써서 자체가 아닌 함수 실행을 넣어야함
              <li 
              className={`submenu${idx === currentTab ? " focused" : ""}`} 
			  onClick={() => selectMenuHandler(idx)}
			  >
                {el.name}
              </li>
            );
          })}
        </TabMenu>
        <Desc>
          {/*TODO: 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

⭐️참고: 왜 매개변수가 있을 땐 실행될 함수로 넣을까?: 그 이유는 컴포넌트 return의 속성에는 직접적으로 함수 실행값이 들어가면 안되는 규칙이 있으니 함수를 또 집어 간접적으로 되게끔 하기!!()=>func(prop)
직접 넣으면 에러: 상태변경의 함수인데 직접 넣으면 바로 렌더링 돼서 너무 많이 리렌더링 된다고 오류, 또 함수 실행 값에 리턴 값이 없으면 undefined가 되니 원하는 기능을 얻어내지 못함

태그

import { useState } from "react";
import styled from "styled-components";

export const TagsInput = styled.div`
// Styled-Component 중략
`;

export const Tag = () => {
  const initialTags = ["CodeStates", "kimcoding"];

  const [tags, setTags] = useState(initialTags);
  const removeTags = (indexToRemove) => {
    // TODO : 태그를 삭제하는 메소드를 완성하세요.⭐️
    setTags(
      tags.filter((el, idx) => {
        return indexToRemove !== idx;
      })
    );
  };

  const addTags = (event) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    // 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기
    let value = event.target.value;
    if (window.event.keyCode === 13 && !tags.includes(value) && value) {
      setTags([...tags, value]);
      event.target.value = "";
    } else if (window.event.keyCode === 13 && tags.includes(value)) {
      alert("중복 오류! 다시 입력해 주세요");
    }
    // else if (window.event.keyCode == 13 && !value) {
    //   alert("오류! 태그를 입력해 주세요");
    // }
  };

  return (
    <>
      <TagsInput>
        <ul id="tags">
          {tags.map((tag, idx) => (
            <li key={idx} className="tag">
              <span className="tag-title">{tag}</span>
              <span className="tag-close-icon" onClick={() => removeTags(idx)}>
                x
                {/* TODO: tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                          삭제 아이콘을 click 했을 때 removeTags 메소드가 실행. */}
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          // 키보드의 Enter 키에 의해 addTags 메소드가 실행
          onKeyUp={(event) => {
            addTags(event);
          }}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

핵심: 삭제하는 메소드를 filter로 사용하여 거르는 로직을 사용해 삭제된 것 처럼

시작: 어드밴스드

자동완성(Autocomplete)

import { useState, useEffect } from "react";
import styled from "styled-components";

const deselectedOptions = [
  "rustic",
  "antique",
  "vinyl",
  "vintage",
  "refurbished",
  "신품",
  "빈티지",
  "중고A급",
  "중고B급",
  "골동품",
];

/* TODO : 아래 CSS를 자유롭게 수정하세요. */
const boxShadow = "0 4px 6px rgb(32 33 36 / 28%)";
const activeBorderRadius = "1rem 1rem 0 0";
const inactiveBorderRadius = "1rem 1rem 1rem 1rem";

export const InputContainer = styled.div`
// 중략
`;

export const DropDownContainer = styled.ul`
// 중략
`;

export const Autocomplete = () => {
  /**
   * Autocomplete 컴포넌트는 아래 3가지 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
   * - hasText state는 input값의 유무를 확인할 수 있습니다.
   * - inputValue state는 input값의 상태를 확인할 수 있습니다.
   * - options state는 input값을 포함하는 autocomplete 추천 항목 리스트를 확인할 수 있습니다.
   */
  const [hasText, setHasText] = useState(false);
  const [inputValue, setInputValue] = useState("");
  const [options, setOptions] = useState(deselectedOptions);
  const [selected, setSelected] = useState();

  // useEffect를 아래와 같이 활용할 수도 있습니다.
  useEffect(() => {
    if (inputValue === "") {
      setHasText(false);
    }
    if (inputValue !== "") {
      setOptions(
        deselectedOptions.filter((el) => {
          return el.includes(inputValue);
        })
      );
    }
  }, [inputValue]);

  // TODO : input과 dropdown 상태 관리를 위한 handler가 있어야 합니다.
  const handleInputChange = (event) => {
    /**
     * handleInputChange 함수는
     * - input값 변경 시 발생되는 change 이벤트 핸들러입니다.
     * - input값과 상태를 연결시킬 수 있게 controlled component로 만들 수 있고
     * - autocomplete 추천 항목이 dropdown으로 시시각각 변화되어 보여질 수 있도록 상태를 변경합니다.
     *
     * handleInputChange 함수를 완성하여 아래 3가지 기능을 구현합니다.
     *
     * onChange 이벤트 발생 시
     * 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
     * 2. input값 유무 상태인 hasText가 적절하게 변경되어야 합니다.
     * 3. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
     * Tip : options의 상태에 따라 dropdown으로 보여지는 항목이 달라집니다.
     */
    setInputValue(event.target.value);
    setHasText(true);
  };

  const handleDropDownClick = (clickedOption) => {
    /**
     * handleDropDownClick 함수는
     * - autocomplete 추천 항목을 클릭할 때 발생되는 click 이벤트 핸들러입니다.
     * - dropdown에 제시된 항목을 눌렀을 때, input값이 해당 항목의 값으로 변경되는 기능을 수행합니다.
     *
     * handleInputChange 함수를 완성하여 아래 기능을 구현합니다.
     *
     * onClick 이벤트 발생 시
     * 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
     * 2. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
     */
    setInputValue(clickedOption);
  };

  const handleDeleteButtonClick = () => {
    /**
     * handleDeleteButtonClick 함수는
     * - input의 오른쪽에 있는 X버튼 클릭 시 발생되는 click 이벤트 핸들러입니다.
     * - 함수 작성을 완료하여 input값을 한 번에 삭제하는 기능을 구현합니다.
     *
     * handleDeleteButtonClick 함수를 완성하여 아래 기능을 구현합니다.
     *
     * onClick 이벤트 발생 시
     * 1. input값 상태인 inputValue가 빈 문자열이 되어야 합니다.
     */
    setInputValue("");
  };

  // Advanced Challenge: 상하 화살표 키 입력 시 dropdown 항목을 선택하고, Enter 키 입력 시 input값을 선택된 dropdown 항목의 값으로 변경하는 handleKeyUp 함수를 만들고,
  // 적절한 컴포넌트에 onKeyUp 핸들러를 할당합니다. state가 추가로 필요한지 고민하고, 필요 시 state를 추가하여 제작하세요.

  return (
    <div className="autocomplete-wrapper">
      <InputContainer>
        {/* TODO : input 엘리먼트를 작성하고 input값(value)을 state와 연결합니다. handleInputChange 함수와 input값 변경 시 호출될 수 있게 연결합니다. */}
        {/* TODO : 아래 div.delete-button 버튼을 누르면 input 값이 삭제되어 dropdown이 없어지는 handler 함수를 작성합니다. */}
        <input value={inputValue} onChange={handleInputChange}></input>
        <div className="delete-button" onClick={handleDeleteButtonClick}>
          &times;
        </div>
      </InputContainer>
      {/* TODO : input 값이 없으면 dropdown이 보이지 않아야 합니다. 조건부 렌더링을 이용해서 구현하세요. */}
      {hasText ? <DropDown options={options} handleComboBox={handleDropDownClick} /> : null}
      {/* {hasText && <DropDown options={options} handleComboBox={handleDropDownClick} />} */}
    </div>
  );
};

export const DropDown = ({ options, handleComboBox }) => {
  return (
    <DropDownContainer>
      {/* TODO : input 값에 맞는 autocomplete 선택 옵션이 보여지는 역할을 합니다. */}
      {options.map((option, idx) => (
        <li key={idx} onClick={() => handleComboBox(option)}>
          {option}
        </li>
      ))}
    </DropDownContainer>
  );
};

ClickToEdit

🦏

profile
코뿔소처럼 저돌적으로

0개의 댓글