CodeStates/React

[React] 컴포넌트 디자인 및 구분

디스페어 2022. 4. 4.

CDD(Component Driven Development)

  • 페이지 단위로 UI 개발이 이루어지는 것과는 반대로 UI 컴포넌트들부터 만들고 이를 기반으로 페이지를 구성
  • 재사용할 수 있는 UI 컴포넌트로 부품 단위로 UI 컴포넌트를 만들어 나가는 개발 방식

 

 

Storybook

  • Component Driven Development를 지원하는 도구인 Component Explorer(컴포넌트 탐색기) 중 하나
  • 예시 : BBC, UN

 

Storybook 장점

  • 각각의 컴포넌트들을 따로 볼 수 있게 구성해 한 번에 하나의 컴포넌트에서 작업 가능
  • 복잡한 개발 스택을 시작하거나, 특정 데이터를 데이터베이스로 강제 이동하거나, 애플리케이션을 탐색할 필요 없이 전체 UI를 한눈에 보고 개발 가능
  • 재사용성을 확대하기 위해 컴포넌트를 문서화하고, 자동으로 컴포넌트를 시각화 하여 시뮬레이션할 수 있는 다양한 테스트 상태를 확인할 수 있어서 버그 사전 방지 가능
  • 테스트 및 개발 속도를 향상시키는 장점이 있으며, 애플리케이션 또한 의존성을 걱정하지 않고 빌드 가능
  • 독립적인 개발 환경에서 실행되므로 다양한 상황에 구애받지 않고 UI 컴포넌트 집중 개발 가능

 

Storybook 주요 기능

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

 

Storybook 설치 및 세팅

# Clone the template
npx degit chromaui/intro-storybook-react-template taskbox

cd taskbox

# Install dependencies
yarn

 

우분투 환경 Yarn 최초 설치 시 발생하는 오류

ERROR: There are no scenarios; must have at least one.

sudo apt remove cmdtest
sudo apt remove yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - 
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update sudo apt install yarn
  • 우분투 환경에서 apt install yarn으로 설치하면 이상하게 cmdtest가 설치된다
    둘이 어떤 관계인지 모르겠지만 yarn을 설치하고 싶다면 위의 코드를 사용하면 된다

 

스토리북이 실행되기 위해 필요한 코드

import React from "react";
import { Button } from "@storybook/react/demo";

export default {
  title: "Button",
  component: Button
};

export const Primary = () => (
  <Button>Hello Button</Button>
);

export const Secondary = () => (
 <Button>Bye Button</Button>
);
  • Button 컴포넌트가 있고 그 Button 컴포넌트를 custom 한 컴포넌트들이 Primary와 Secondary 컴포넌트
  • 이 Primary, Secondary function component들을 export 하면 storybook에서 UI 목업을 확인 가능

 

 

CSS 방법론

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

 

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

  • 모바일이나 태블릿을 비롯한 다양한 디바이스들의 등장으로 다양한 디스플레이를 커버하기 위해 CSS는 더욱 복잡해짐

 

1-1. CSS 전처리기(CSS Preprocessor) 등장 : SASS

  • CSS 전처리기(CSS Preprocessor) : CSS가 구조적으로 작성될 수 있게 도움을 주는 도구
    *CSS 전처리기 자체만으로는 웹 서버가 인지하지 못하기 때문에 각 CSS 전처리기에 맞는 Compiler를 사용하여 컴파일하면 CSS 문서로 변환됨
  • SASS(Syntactically Awesome Style Sheets) : CSS를 확장해 주는 스크립팅 언어(CSS를 만들어주는 언어)
  • 자바스크립트처럼 특정 속성(ex. color, margin, width 등)의 값을 변수로 선언하여 필요한 곳에 선언된 변수를 적용 가능
  • 반복되는 코드를 한 번의 선언으로 여러 곳에서 재사용할 수 있도록 해 줌
  • SCSS 코드를 읽어서 전처리한 다음 컴파일해서 전역 CSS 번들 파일을 만들어 주는 전처리기(preprocessor)의 역할을 함
  • 문제점 : 전처리기(preprocessor)가 내부에서 어떤 작업을 하는지는 알지 못한 채, 스타일이 겹치는 문제를 해결하기 위해 단순히 계층 구조를 만들어 내는 것에 의지하게 됨
    *컴파일된 CSS의 용량은 어마어마하게 커짐

 

1-2. CSS 방법론 대두 : BEM

Block : 전체를 감싸고 있는 블럭 요소
Element : 블럭이 포함하고 있는 한 조각
Modifier : 블럭 또는 요소의 속성(블록이나 엘리먼트의 외관이나 상태를 변화 가능하게 하는 부분)

  • Block, Element, Modifier로 구분하여 클래스명을 작성하는 방법
  • Block, Element, Modifier 각각은 —와 __로 구분
  • 클래스명 : BEM 방식의 이름을 여러 번 반복하여 재사용할 수 있도록 하며 HTML/CSS/SASS 파일에서도 더 일관된 코딩 구조를 만들어 줌
  • 문제점 : 클래스명 선택자가 장황해지고, 이런 긴 클래스명 때문에 마크업이 불필요하게 커지며, 재사용하려고 할 때마다 모든 UI 컴포넌트를 명시적으로 확장해야 함
  • SASS와 BEM의 공통적인 문제 : 언어 로직 상에 진정한 캡슐화의 개념이 없고, 이로 인해 개발자들이 유일한 클래스명을 선택하는 것에 의존할 수밖에 없었음
    *캡슐화(encapsulation) : 객체의 속성과 행위를 하나로 묶고 실제 구현 내용 일부를 외부에 감추어 은닉하는 개념

 

1-3. CSS-in-JS 등장 : Styled-Component

  • 컴포넌트 단위의 개발 : 캡슐화의 중요성 대두
  • 컴포넌트 기반의 방식을 위해 만들어진 적이 한 번도 없었던 CSS도 컴포넌트 영역으로 불러들이기 위해 CSS-in-JS가 탄생
  • 기능적(Functional) 혹은 상태를 가진 컴포넌트들로부터 UI를 완전 분리해 사용할 수 있는 아주 단순한 패턴을 제공
  • 기존 방식은 전체 페이지에 필요한 CSS를 모두 처음부터 로딩해서 style 태그를 생성했지만, CSS-in-JS는 그때그때 필요한 만큼만 style 태그를 생성해서 성능이 빠름

 

2. Styled-Component

const Button = styled.a`
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
`;
  • React의 컴포넌트 기반 개발 환경에서 스타일링을 위한 CSS의 성능 향상을 위해 탄생
  • 기존 CSS 문법으로도 별도의 CSS 파일 없이 스타일 속성이 추가된 React 컴포넌트를 만들 수 있음
  • JavaScript에서 변수를 선언하듯 Button을 만들고, tag의 속성을 정의하고(a 태그), back-tick(`) 안에 기존 CSS 문법을 이용하여 스타일 속성을 정의

 

2-1. 특징

1. Automatic critical CSS
: 화면에 어떤 컴포넌트가 렌더링 되었는지 추적해서 해당하는 컴포넌트에 대한 스타일을 자동으로 삽입
: 따라서 코드를 적절히 분배해 놓으면 사용자가 어플리케이션을 사용할 때 최소한의 코드만으로 화면이 띄워지도록 할 수 있음

2. No class name bugs
: 스스로 유니크한 className을 생성하여 className의 중복이나 오타로 인한 버그를 줄여줌

3. Easier deletion of CSS
: 모든 스타일 속성이 특정 컴포넌트와 연결되어 있기 때문에 컴포넌트를 더 이상 사용하지 않아 삭제할 경우 이에 대한 스타일 속:성도 함께 삭제

4. Simple dynamic styling
: className을 일일이 수동으로 관리할 필요 없이 React의 props나 전역 속성을 기반으로 컴포넌트에 스타일 속성을 부여하기 : 때문에 간단하고 직관적

5. Painless maintenance
: 컴포넌트에 스타일을 상속하는 속성을 찾아 다른 CSS 파일들을 검색하지 않아도 되기 때문에 유지보수 유리

6. Automatic vendor prefixing
: 개별 컴포넌트마다 기존의 CSS를 이용하여 스타일 속성을 정의만 하면 이외의 것들은 Styled Component가 알아서 처리

 

2-2. 설치

npm install --save styled-components
// package.json
{
  "resolutions": {
    "styled-components": "^5"
  }
}
  • package.json에 위 코드를 적어주면 여러 버전의 Styled Component가 설치되어 발생하는 문제를 줄여줌

 

2-3. 사용법

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>
  );
}

 

1) 스타일 속성을 지닌 컴포넌트를 정의할 때에 함수를 전달하는 경우

import styled from "styled-components";

const Button = styled.button`
  //Adapt the colors based on primary prop
  background: ${(props) => (props.primary ? "palevioletred" : "white")};
  color: ${(props) => (props.primary ? "white" : "palevioletred")};

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

export default function App() {
  return (
    <div className="App">
      <Button>Normal</Button>
      <Button primary>Primary</Button>
    </div>
  );
}
  • 스타일 속성을 지닌 컴포넌트를 정의할 때에 함수를 전달하고, 그 함수 안에서 props를 사용할 수 있음
  • Button 컴포넌트의 background와 color 속성은 primary라는 props의 전달 여부에 따라 컬러 값을 정의

 

2) 같은 스타일 속성을 지닌 컴포넌트들 중 일부만 변경하는 경우

// 기존의 Button 컴포넌트에 Tomato 컴포넌트만을 위한 새로운 속성 추가
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>
  );
}
  • 상속받고자 하는 스타일 속성을 지닌 컴포넌트를 styled()로 감싼 뒤, 변경하고 싶은 속성만 새로 정의해 주면 기존 속성을 확장하여 사용 가능

 

3) props로 color 속성이 전달된 경우

import styled from "styled-components";

// Styled Component로 만들어진 Input 컴포넌트
const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  color: ${(props) => props.inputColor || "red"};
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

export default function App() {
  return (
    <div>
      //Input 컴포넌트에 지정된 inputColor(red)가 적용
      <Input defaultValue="소민" type="text" />
      //props로 전달된 커스텀 inputColor(blue)가 적용
      <Input defaultValue="까까" type="text" inputColor="blue" />
    </div>
  );
}
  • 해당 컴포넌트는 props로 전달된 속성을 우선 적용하며, 전달되는 속성이 없다면 기본으로 설정된 속성을 적용

 

3. useRef

React로 모든 개발 요구 사항을 충족할 수 없는 경우
focus
text selection
media playback
애니메이션 적용
d3.js, greensock 등 DOM 기반 라이브러리 활용
  • React에서 DOM 엘리먼트에 직접 접근하여 메소드를 이용할 방법이 제한적이기 때문에 DOM 엘리먼트의 특정 메소드를 활용해야 할 때 자주 사용
  • 위와 같은 예외적인 상황에서 useRef로 DOM 노드, 엘리먼트, 그리고 리액트 컴포넌트 주소 값을 참조
  • useRef의 리턴 값은 변화되어도 컴포넌트가 재렌더링이 되지 않기 때문에 대부분의 경우 어플리케이션에서 변화하는 값은 useState로 state로 지정하여 이용
  • 제시된 상황 제외한 대부분의 경우 기본 리액트 문법을 벗어나 useRef를 남용하는 것은 부적절
    *React의 특징이자 장점인 선언적 프로그래밍 원칙과 배치되기 때문에, 조심해서 사용해야 함

 

3-1. 사용법

const 주소값을_담는_그릇 = useRef(참조자료형)
// 주소값을_담는_그릇 변수에 어떤 주소값이든 담을 수 있음
return (
    <div>
      <input ref={주소값을_담는_그릇} type="text" />
        // React에서 사용 가능한 ref라는 속성에 주소값을_담는_그릇을 값으로 할당
        // 주소값을_담는_그릇 변수에는 input DOM 엘리먼트의 주소가 담김
        // 이 주소값은 컴포넌트가 re-render 되더라도 바뀌지 않음
	// 향후 다른 컴포넌트에서 input DOM 엘리먼트를 활용할 수 있음
    </div>
  );
  • useRef의 초기값(useRef의 첫 번째 인자)에는 어떤 참조 자료형도 할당할 수 있지만, DOM 엘리먼트나 React 엘리먼트의 주소 값을 할당하여 사용하는 경우가 더 많음
  • ref 속성을 활용해 DOM 엘리먼트, React 엘리먼트의 주소값을 useRef의 리턴 값에 전달

 

3-2. 예시

import React, { useRef } from "react";

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

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

  return (
    <div>
      <div>
        <label>hello </label>
        <input ref={firstRef} onKeyUp={handleInput} />
      </div>
      <div>
        <label>world </label>
        <input ref={secondRef} onKeyUp={handleInput} />
      </div>
    </div>
  );
};

export default Focus;
import { useRef } from "react";

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

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

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

  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>
  );
}

 

 

Class형 컴포넌트와 function형 컴포넌트

  • 기본적인 동작은 동일

 

1. Class형 컴포넌트

import React from 'react'

class App extends React.Component {
  // React.Component : lifecyle 메소드
  // React 컴포넌트 class를 정의하려면 React.Component를 상속받아야 함
  // render 함수 필수, 보여 주어야 할 JSX를 반환
  render() {
    const name = 'Despair'
    return <div>{name}</div>
  }
}

export default App
  • state를 정의할 수 있고 render함수의 return값에 따라 원하는 UI를 보여줄 수 있음
  • state 기능 및 lifecyle 기능을 사용할 수 있으며 임의 메소드를 정의할 수 있음
  • React.Component lifecyle 메소드

 

2. function형 컴포넌트

useState
: state를 제공
useEffect
: lifecyle API를 제공
import React from 'react'

function App() {
  const name = 'Despair'
  return <div>{name}</div>
}

export default App
  • 클래스형 컴포넌트보다 선언하기가 좀 더 편하고, 메모리 자원을 덜 사용
  • state는 물론이고, lifecyle에 정의된 함수를 사용하지 못했으나 React Hooks의 등장으로 사용 가능해짐

 

 

Presentational 컴포넌트와 Container 컴포넌트

  • React에서 가장 유명한 패턴인이자 가독성과 생산성을 고려한 방법
  • 화면을 표현하는 부분(Presentational Components)와 데이터를 다루는 부분(Container Components)으로 나눠서 개발하는 패턴
    *재사용성과 유지보수의 장점을 지님

 

1. Presentational 컴포넌트

import React from 'react'

import InputContainer from './InputContainer'

export default function App() {
  return (
    <InputContainer />
  )
}
  • 어떻게 보여지는가와 관련
  • Redux 등 상태 관리 라이브러리와의 연관성 없음
  • props에서 데이터를 읽고, props에서 콜백을 호출하여 데이터 변경

 

2. Container 컴포넌트

import React from 'react'

import { useDispatch, useSelector } from 'react-redux'

import Input from './Input'

import {
  updateTaskTitle,
  addTask,
} from './actions'

export default function InputContainer() {
  const { taskTitle } = useSelector((state) => ({
    taskTitle: state.taskTitle,
  }));

  const dispatch = useDispatch()

  function handleChangeTitle(event) {
    dispatch(updateTaskTitle(event.target.value))
  }

  function handleClickAddTask() {
    dispatch(addTask())
  }

  return (
    <Input
      value={taskTitle}
      onChange={handleChangeTitle}
      onClick={handleClickAddTask}
    />
  )
}
  • 어떻게 동작하는가와 관련
  • Redux 등 상태 관리 라이브러리와의 연관성 존재
  • Redux의 State에 접근하여 데이터를 읽음(useSelector)
  • Redux Action으로 데이터 변경
  • Presentational 컴포넌트보다 재사용성이 뛰어남

 

 

References

Class형 컴포넌트와 function형 컴포넌트
Presentational 컴포넌트와 Container 컴포넌트

반응형

댓글