상권's

TIL 21 (storybook, CSS방법론, styled-component, Ref)(2021.10.27) 본문

~2022 작성 글/TIL

TIL 21 (storybook, CSS방법론, styled-component, Ref)(2021.10.27)

라마치 2021. 10. 27. 16:43
다음의 조건을 만족하면서 현재의 비밀번호('curPwd')를 새 비밀번호(newPwd)로 변경하는 데 필요한 최소 동작의 수를 리턴해야 합니다.
한 번에 한 개의 숫자만 변경가능하다.4자리의 소수(prime)인 비밀번호로만 변경가능하다.
정리하면, 비밀번호가 계속 소수를 유지하도록 숫자 한 개씩을 바꿔갈 때 현재 비밀번호에서 새 비밀번호로 바꾸는 데 최소 몇 개의 숫자를 변경해야 하는지를 리턴해야 합니다
인자 1 : curPwd
number 타입의 1,000 이상 9,999 이하의 자연수
인자 2 : newPwd
number 타입의 1,000 이상 9,999 이하의 자연수
  // 소수(prime number)는 1보다 큰 자연수 중 1과 자기 자신만을 약수로 가지는 수
  // 예를 들어, 5는 1×5 또는 5×1로 수를 곱한 결과를 적는 유일한 방법이 
  // 그 수 자신을 포함하기 때문에 5는 소수이다. 그러나 6은 자신보다 작은 두 숫자의 곱(2×3)이므로 소수가 아닌데, 
  // 이렇듯 1보다 큰 자연수 중 소수가 아닌 것은 합성수라고 한다. 1과 그 수 자신 이외의 자연수로는 나눌 수 없는 자연수로 정의하기도 한다.
  // 학습했었던 소수를 구하는 방법을 이용해 본다. sqrt = parseInt(Math.sqrt(num)) for(let n = 3; n < sqrt; n++ ) sqrt % i !== 0 true
  // 소수들이 구해지면.. 
  // 첫번째 앞의 세개는 같고 맨 뒷자리만 다른 수를 찾는다.
  // 두번째 앞의 두개와 맨 뒷자리는 같고 10의 자리수가 다른 수를 찾는다.
  // 세번째 1000자리, 10자리, 1자리는 같고 100자리가 다른 수를 찾는다.
  // 네번째 1000자리만 다른 수를 찾는다.

첫 수도코드 => 현재 비밀번호와 새 비밀번호 사이의 소수들은 구현해냈지만, 자릿수별로 찾는 방법에 대해서는 구현을 못했습니다. 오늘 중으로 조금 더 고민해보고 해당 방법을 찾아보겠습니다.

앞 서 구현했었던 소수여부를 확인하는 코드.

function whatIsPrime(num) {
  let results = '2' //1.num이 2일 경우, 소수이기 때문에 for 문을 이용하지 않고 result값을 둔다.
  for( let n = 3; n <= num; n = n + 2 ) { //2.짝수는 소수가 아니기 때문에, 3부터 해당 숫자 사이에 홀수를 나타낸다.
    let isprime = true;
    let sqrt = parseInt(Math.sqrt(n)); //3.sqrt를 이용해서 n의 제곱근을 구한다.
    for( let j = 3; j <= sqrt; j = j + 2 ) { //4.제곱근이 된 수들 중, 홀수를 구한다.
      if ( n % j === 0 ) { //5.2에서 구한 홀수들이, n의 제곱근 이하에 있는 홀수들로 나눠질 경우 break가 걸린다.
        isprime = false;
        break;
      }
      }
    if ( isprime ) {
      results = results + `-${n}`;
    }
  }
  return results;
}

레프런스 코드를 봐도 이해가 잘 되지 않아 추후에 학습을 하고 난 다음 정리가 되면 그때 코드 리뷰를 하고 학습 방법에 대해서 알아보록 하겠습니다.

const isPrime = (num) => {
  if (num % 2 === 0) return false;
  let sqrt = parseInt(Math.sqrt(num));
  for (let divider = 3; divider <= sqrt; divider += 2) {
    if (num % divider === 0) {
      return false;
    }
  }
  return true;
};

// 4자리 수를 받아서 각 자리수의 수들의 배열로 변환하는 함수
const splitNum = (num) => {
  const digits = num.toString().split('');
  return digits.map((d) => Number(d));
};

// 길이의 4의 수 배열을 받아서, 4자리의 수로 변환하는 함수
const joinDigits = (digits) => Number(digits.join(''));

const primePassword = (curPwd, newPwd) => {
  if (curPwd === newPwd) return 0;
  // bfs를 위해 queue를 선언 너비우선탐색을 실시한다.
  let front = 0;
  let rear = 0;
  const queue = [];
  const isEmpty = (queue) => front === rear;
  const enQueue = (queue, item) => {
    queue.push(item);
    rear++;
  };
  const deQueue = (queue) => {
    return queue[front++];
  };

  // 각 4자리의 방문 여부를 저장하는 배열
  // 한 번 방문한 수(가장 최소의 동작으로 만든 수)는 다시 방문할 필요가 없다.
  const isVisited = Array(10000).fill(false);
  isVisited[curPwd] = true;
  // bfs를 위한 시작점
  // 큐에는 [필요한 동작 수, 비밀번호]가 저장된다.
  enQueue(queue, [0, curPwd]);
  // bfs는 큐가 빌(empty) 때까지 탐색한다.
  while (isEmpty(queue) === false) {
    const [step, num] = deQueue(queue);
    // 각 자리수 마다 변경이 가능하므로 4번의 반복이 필요하다.
    for (let i = 0; i < 4; i++) {
      const digits = splitNum(num);
      // 0부터 9까지 시도한다.
      for (let d = 0; d < 10; d++) {
        // 각 자리수마다 원래 있던 수(digits[i])는 피해야 한다.
        if (d !== digits[i]) {
          // 현재 자리수의 수를 변경하고,
          digits[i] = d;
          // 변경한 후 4자리 수를 구한다.
          const next = joinDigits(digits);
          // 만약 이 수가 새 비밀번호와 같다면 리턴한다.
          // next는 deQueue된 num으로부터 1단계 다음에 도달한 수이다.
          if (next === newPwd) return step + 1;
          // 1,000보다 큰 소수여야 하고, 방문된 적이 없어야 한다.
          if (next > 1000 && isPrime(next) && isVisited[next] === false) {
            // 방문 여부를 표시하고,
            isVisited[next] = true;
            // 큐에 넣는다.
            enQueue(queue, [step + 1, next]);
          }
        }
      }
    }
  }

  // 큐가 빌 때까지, 즉 모든 경우의 수를 탐색할 때까지 리턴되지 않은 경우
  // 현재 비밀번호에서 새 비밀번호를 만들 수 없다.
  return -1;
};

STORYBOOK

출처

 

Component Driven Development 가 트렌드로 자리 잡게 되면서 이를 지원하는 도구 중 하나인 Component Explorer (컴포넌트 탐색기) 가 등장했습니다. Component Explorer에는 많은 UI 개발도구가 다양하게 있는데 그중 하나가 Storybook 입니다.

 

Storybook은 UI 개발 즉, Component Driven Development를 하기 위한 도구입니다. 각각의 컴포넌트들을 따로 볼 수 있게 구성해주어 한 번에 하나의 컴포넌트에서 작업할 수 있습니다. 복잡한 개발 스택을 시작하거나, 특정 데이터를 데이터베이스로 강제 이동하거나, 애플리케이션을 탐색할 필요 없이 전체 UI를 한눈에 보고 개발할 수 있습니다.

 

Storybook은 재사용성을 확대하기 위해 컴포넌트를 문서화하고, 자동으로 컴포넌트를 시각화하여 시뮬레이션할 수 있는 다양한 테스트 상태를 확인할 수 있습니다. 이를 통해 버그를 사전에 방지할 수 있도록 도와줍니다. 또한 테스트 및 개발 속도를 향상시키는 장점이 있으며, 애플리케이션 또한 의존성을 걱정하지 않고 빌드할 수 있습니다.

 

Storybook은 기본적으로 독립적인 개발환경에서 실행됩니다. 개발자는 애플리케이션의 다양한 상황에 구애받지 않고 UI 컴포넌트를 집중적으로 개발 할 수 있습니다.

 

Storybook에서 지원하는 주요 기능은

  • UI 컴포넌트들을 카탈로그 화하기
  • 컴포넌트 변화를 Stories로 저장하기
  • 핫 모듈 재 로딩과 같은 개발 툴 경험을 제공하기
  • 리액트를 포함한 다양한 뷰 레이어 지원하기

=> 아직 사용방법이 익숙하지 않아 공식문서를 통해서 사용방법을 익혀야 할 거 같습니다.


구조적인 CSS 작성 방법의 발전

 

다양한 환경(디바이스)에서 인터넷을 사용하기 시작하며 개발자들의 CSS 작성 방법 진화

=> 프로젝트의 규모와 복잡도가 커지면서 일관된 CSS 작성 패턴이 요구(구조화된 CSS)

=> CSS 전처리기(CSS Preprocessor) 개념 등장

CSS 전처리기(CSS Preprocessor)란 CSS가 구조적으로 작성될 수 있게 도움을 주는 도구입니다.

CSS 파일들을 잘 구조화할 수 있게 되었고, 최소한 CSS 파일을 몇 개의 작은 파일로 분리할 수 있는 방법 출현

 

CSS 전처리기 중에서 가장 유명한 SASS는 Syntactically Awesome Style Sheets의 약자로 CSS를 확장해 주는 스크립팅 언어로, SCSS 코드를 읽어서 전처리한 다음 컴파일해서 전역 CSS 번들 파일을 만들어 주는 전처리기(preprocessor)의 역할

 

=> CSS를 만들어주는 언어로서 자바스크립트처럼 특정 속성(ex. color, margin, width 등)의 값(ex. #ffffff, 25rem, 100px 등)을 변수로 선언하여 필요한 곳에 선언된 변수를 적용할 수도 있고, 반복되는 코드를 한번의 선언으로 여러 곳에서 재사용할 수 있도록 해 주는 등의 기능 구현.

하지만 컴파일 CSS파일 거대해진다는 문제 발생.

=> CSS 전처리기의 문제를 보완하기 위해 BEM, OOCSS, SMACSS 같은 CSS 방법론이 대두.

 

 

 

CSS 방법론 중 BEM이란 Block, Element, Modifier로 구분하여 클래스명을 작성하는 방법으로, Block, Element, Modifier 각각은 —와 __로 구분합니다. 클래스명은 BEM 방식의 이름을 여러 번 반복하여 재사용할 수 있도록 하며 HTML/CSS/SASS 파일에서도 더 일관된 코딩 구조 제공.

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

 

또한 SASS와 BEM도 고치지 못했던 몇 가지 문제들은 언어 로직 상에 진정한 캡슐화(encapsulation : 객체의 속성과 행위를 하나로 묶고 실제 구현 내용 일부를 외부에 감추어 은닉하는 개념)의 개념이 없다는 것이었고, 이로 인해 개발자들이 유일한 클래스명을 선택하는 것에 의존할 수 밖에 없음.

 

CSS도 컴포넌트 영역(캡슐화)으로 불러들이기 위해서 CSS-in-JS가 탄생


Styled Component

Styled Component 는 React 의 컴포넌트 기반 개발 환경에서 스타일링을 위한 CSS의 성능 향상을 위해 탄생하였습니다. Styled Component 를 사용하면 기존 CSS 문법으로도 스타일 속성이 추가된 React 컴포넌트를 만들 수 있습니다.

Styled Component 이용하여 어플리케이션 내에 다른 웹페이지로 이동하는 기능을 가진 Button 구현

JavaScript에서 변수를 선언하듯이(혹은 React 에서 컴포넌트를 만들듯이) Button 을 만들고, 

const Button = styled.a` //tag 의 속성을 정의하고 (여기서는 a tag)
  display: inline-block;  // back-ticks (``) 안에 기존 CSS 문법을 이용하여 스타일 속성 정의
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
`;

Styled Component 의 특징은 아래와 같습니다.

  • Automatic critical CSS => 화면에 어떤 컴포넌트가 렌더링 되었는지 추적해서 해당하는 컴포넌트에 대한 스타일을 자동으로 삽입합니다. 따라서 코드를 적절히 분배해 놓으면 사용자가 어플리케이션을 사용할 때 최소한의 코드만으로 화면이 띄워지도록 할 수 있습니다.
  • No class name bugs => 스스로 유니크한 className 을 생성, className 의 중복이나 오타로 인한 버그를 줄여줍니다.
  • Easier deletion of CSS => 모든 스타일 속성이 특정 컴포넌트와 연결되어 있기 때문에 만약 컴포넌트를 더 이상 사용하지 않아 삭제할 경우 이에 대한 스타일 속성도 함께 삭제됩니다.
  • Simple dynamic styling => className을 일일이 수동으로 관리할 필요 없이 React 의 props 나 전역 속성을 기반으로 컴포넌트에 스타일 속성을 부여하기 때문에 간단하고 직관적입니다.
  • Painless maintenance => 컴포넌트에 스타일을 상속하는 속성을 찾아 다른 CSS 파일들을 검색하지 않아도 되기 때문에 코드의 크기가 커지더라도 유지보수가 어렵지 않습니다.
  • Automatic vendor prefixing => 개별 컴포넌트마다 기존의 CSS 를 이용하여 스타일 속성을 정의하면 될 뿐입니다. 이외의 것들은 Styled Component 가 알아서 처리해 줍니다.

설치

$ npm install --save styled-components

package.json에 아래의 코드를 추가하면 여러 버전의 Styled Component가 설치되어 발생하는 문제를 줄여줍니다.

{
  "resolutions": {
    "styled-components": "^5"
  }
}

예제

import styled from "styled-components";

// <h1> 태그를 렌더링 할 title component를 만듭니다.
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

// <section> 태그를 렌더링 할 Wrapper component를 만듭니다.
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

export default function App() {
  // 일반적으로 컴포넌트를 사용하는 것처럼 Title과 Wrapper를 사용하시면 됩니다!
  return (
    <Wrapper>
      <Title>Hello World!</Title>
    </Wrapper>
  );
}

예제2

import "./styles.css";
import styled from "styled-components";

// 스타일 속성을 지닌 컴포넌트를 정의할 때에 함수를 전달하고, 그 함수 안에서 props 를 사용할 수도 있습니다.
// <Button> 컴포넌트의 background 와 color 속성은 primary 라는 props 의 전달 여부에 따라 
// 컬러값을 정의하고 있습니다.
const Button = styled.button`
  background: ${(props) => (props.primary ? "palevioletred" : "white")};
  color: ${(props) => (props.primary ? "white" : "palevioletred")};
// primary 라면 적용되는 색상을 달리할 수 있다

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

// 상속받고자 하는 스타일 속성을 지닌 컴포넌트를 styled() 로 감싼 뒤, 
// 변경하고 싶은 속성만 새로 정의해 주면 기존 속성을 확장하여 사용할 수 있습니다.
// Button 속성을 상속 받으면서 color와 border-color만 따로 지정할 수 있다.
const Tomato = styled(Button)`
  color: tomato;
  border-color: tomato;
`;

export default function App() {
  return (
    <div className="App">
      <Button>Normal</Button>
      <Button primary>Primary</Button>
      <Tomato>Tomato</Tomato>
    </div>
  );
}

예제3

import styled from "styled-components";

const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  color: ${(props) => props.inputColor || "red"};
  // inputColor 가 없다면 red, 있다면 해당 색상 적용
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

export default function App() {
  return (
    <div>
// 컴포넌트에 props 로 스타일 속성이 전달된다면 해당 컴포넌트는 props 로 전달된 속성을 우선 적용하며, 
// 전달되는 속성이 없다면 기본으로 설정된 속성을 적용
      <Input defaultValue="김코딩" type="text" />
      // inputColor가 없기 때문에 red가 적용
      <Input defaultValue="박해커" type="text" inputColor="blue" />
      // inputColor가 있기에 기본 색상이 아닌 blue가 적용
    </div>
  );
}

Ref와 DOM 출처

Refrender 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공합니다.

일반적인 React의 데이터 플로우에서 props는 부모 컴포넌트가 자식과 상호작용할 수 있는 유일한 수단입니다. 자식을 수정하려면 새로운 props를 전달하여 자식을 다시 렌더링해야 합니다. 그러나, 일반적인 데이터 플로우에서 벗어나 직접적으로 자식을 수정해야 하는 경우도 가끔씩 있습니다. 수정할 자식은 React 컴포넌트의 인스턴스일 수도 있고, DOM 엘리먼트일 수도 있습니다. React는 두 경우 모두를 위한 해결책을 제공합니다.

Ref를 사용해야 할 때

Ref의 바람직한 사용 사례는 다음과 같습니다.

  • 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
  • 애니메이션을 직접적으로 실행시킬 때.
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.

React는 이런 예외적인 상황에서 useRef으로 DOM 노드, 엘리먼트, 그리고 리액트 컴포넌트 주소값을 참조할 수 있습니다.

선언적으로 해결될 수 있는 문제에서는 ref 사용을 지양하세요.

Ref를 남용하지 마세요
ref는 애플리케이션에 “어떤 일이 일어나게” 할 때 사용될 수도 있습니다. 그럴 때는 잠시 멈추고 어느 컴포넌트 계층에서 상태를 소유해야 하는지 신중하게 생각해보세요. 대부분의 경우, 상태를 소유해야 하는 적절한 장소가 더 높은 계층이라는 결론이 날 겁니다. 상태를 상위 계층으로 올리는 것에 대한 예시는 상태 끌어올리기 가이드에서 확인하실 수 있으십니다.
const 주소값을_담는_그릇 = useRef(참조자료형)
// 이제 주소값을_담는_그릇 변수에 어떤 주소값이든 담을 수 있습니다.
return (
    <div>
      <input ref={주소값을_담는_그릇} type="text" />
        {/* React에서 사용 가능한 ref라는 속성에 주소값을_담는_그릇을 값으로 할당하면*/}
        {/* 주소값을_담는_그릇 변수에는 input DOM 엘리먼트의 주소가 담깁니다. */}
        {/* 향후 다른 컴포넌트에서 input DOM 엘리먼트를 활용할 수 있습니다. */}
    </div>
  );

이 주소값은 컴포넌트가 re-render 되더라도 바뀌지 않습니다. 이 특성을 활용하여 아래의 제한된 상황에서 useRef를 활용할 수 있습니다.

Action Item 1 : focus

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);
// ref 어트리뷰트가 HTML 엘리먼트에 쓰였다면, ref는 자신을 전달받은 DOM 엘리먼트를 
// current 프로퍼티의 값으로서 받습니다.
    if (event.key === "Enter") {
    // 눌러진 키가 enter일 경우,
      if (event.target === firstRef.current) {
      // event.target이 firstRef의 input일 경우
        secondRef.current.focus();
        // secondRef이 사용된 input으로 focus가 작용합니다.
        event.target.value = "";
        // enter가 눌러진 곳의 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;

Action Item 2 : 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>
  );
}

Array.prototype.splice()

splice() 메서드는 배열의 기존 요소를 삭제 또는 교체하거나 새 요소를 추가하여 배열의 내용을 변경합니다.

구문

array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

매개변수

start => 배열의 변경을 시작할 인덱스입니다. 배열의 길이보다 큰 값이라면 실제 시작 인덱스는 배열의 길이로 설정됩니다. 음수인 경우 배열의 끝에서부터 요소를 세어나갑니다. (원점 -1, 즉 -n이면 요소 끝의 n번째 요소를 가리키며 array.length - n번째 인덱스와 같음). 값의 절대값이 배열의 길이 보다 큰 경우 0으로 설정됩니다.

deleteCount Optional =>배열에서 제거할 요소의 수입니다.deleteCount를 생략하거나 값이 array.length - start보다 크면 start부터의 모든 요소를 제거합니다.deleteCount가 0 이하라면 어떤 요소도 제거하지 않습니다. 이 때는 최소한 하나의 새로운 요소를 지정해야 합니다.

item1, item2, ... Optional => 배열에 추가할 요소입니다. 아무 요소도 지정하지 않으면 splice()는 요소를 제거하기만 합니다.

const months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');
// delete가 0일 경우, 인덱스1에 Feb 를 추가한다.

console.log(months);
// expected output: Array ["Jan", "Feb", "March", "April", "June"]

months.splice(4, 1, 'May');
// 인덱스4에서 하나를 삭제하고, May를 추가한다.

console.log(months);
// expected output: Array ["Jan", "Feb", "March", "April", "May"]

출처 MDN

Array.prototype.every()

every() 메서드는 배열 안의 모든 요소가 주어진 판별 함수를 통과하는지 테스트합니다. Boolean 값을 반환합니다.

const isBelowThreshold = (currentValue) => currentValue < 40;

const array1 = [1, 30, 39, 29, 10, 13];

console.log(array1.every(isBelowThreshold));
// expected output: true

Array.prototype.forEach()

forEach() 메서드는 주어진 함수를 배열 요소 각각에 대해 실행합니다.

const array1 = ['a', 'b', 'c'];

array1.forEach(element => console.log(element));

// expected output: "a"
// expected output: "b"
// expected output: "c"

Array.prototype.some()

some() 메서드는 배열 안의 어떤 요소라도 주어진 판별 함수를 통과하는지 테스트합니다.

const array = [1, 2, 3, 4, 5];

// checks whether an element is even
const even = (element) => element % 2 === 0;

console.log(array.some(even));
// expected output: true

Array.prototype.reverse()

reverse() 메서드는 배열의 순서를 반전합니다. 첫 번째 요소는 마지막 요소가 되며 마지막 요소는 첫 번째 요소가 됩니다.

const array1 = ['one', 'two', 'three'];
console.log('array1:', array1);
// expected output: "array1:" Array ["one", "two", "three"]

const reversed = array1.reverse();
console.log('reversed:', reversed);
// expected output: "reversed:" Array ["three", "two", "one"]

// Careful: reverse is destructive -- it changes the original array.
console.log('array1:', array1);
// expected output: "array1:" Array ["three", "two", "one"]
Comments