들어가기 전에

ISO 8601이란 ?

  • ISO 8601은 날짜와 시간을 나타내는 표준화된 형식을 정의한 국제 표준화 기구(ISO)의 표준입니다.
  • 이 표준은 날짜, 시간, 날짜 및 시간의 조합에 대한 표현 방법을 규정하고 있어 다양한 응용 분야에서 사용됩니다.

ISO 형식

  • 년-월-일 형식: 년도, 월, 일을 순서대로 표기합니다.
  • 시간 형식: 시, 분, 초를 순서대로 표기하며, 필요에 따라 소수 초를 추가로 표현할 수 있습니다.
  • UTC 표기: 날짜와 시간을 협정 세계시(UTC, 혹은 GMT)로 표기할 수 있습니다.
  • 시간대 정보: 시간대 정보를 포함하여 표기할 수 있습니다.
날짜: "2023-10-09"
날짜와 시간: "2023-10-09T14:30:00"
날짜와 시간, UTC: "2023-10-09T14:30:00Z"
날짜와 시간, 시간대 포함: "2023-10-09T14:30:00+03:00"

ISO 8601 표준을 보기 편하게 바꾸기

현재 상황

  • 모여타 개발 중 출발 시간을 표시해야 하는데 출발시간 데이터가 ISO 표준 형식으로 받아와지고있다.
  • 이를 잘 파싱하여 10월 06일 오후 7시 50분 과 같은 형태로 보여주고싶다.

년, 월, 일 파싱

  • 2023-10-09T14:30:00Z에서 년, 월, 일을 파싱하는 것은 쉽다.

  • Javascript의 slice 메소드를 이용하면 된다.

    
      const year = data.departureTime.slice(0, 4); // 2023
      const month = data.departureTime.slice(5, 7); // 10
      const date = data.departureTime.slice(8, 10); // 09

요일 구하기

영어로 된 요일 구하기

  • Javascript 메소드인 Date를 사용하여 영어로 된 요일을 구해줄 것이다.
    var newDate = new Date("2023-10-09T14:30:00"); // Mon Oct 09 2023 14:30:00 GMT+0900 (한국 표준시) 
  • 위 경우처럼 Date를 생성자로 호출할 경우 새로운 Date 객체를 반환한다.
  • 이 과정에서 우리는 영어로 된 요일을 얻을 수 있다.

한글로 바꿔주기

const days = ['일', '월', '화', '수', '목', '금', '토'];
var newDate = new Date("2023-10-09T14:30:00");
const day = days[newDate.getDay()];
  • getDay 메서드를 이용하면 요일에 따른 숫자를 반환한다.
    (Sun: 0, Mon : 1 등등)
  • 우리는 이를 한글로 바꿔주고 싶기 때문에 한글로 된 요일 배열 days를 만들고, getDay메소드로 받아온 숫자를 인덱스처럼 사용해주면 된다.

시간을 12시제로 바꾸기

  • 이제 남은 것은 14:30:00 등으로 표현된 시간을 오후 2시 30분으로 된 12시제로 바꿔주는 것이다.

숫자만 파싱해오기

     let timePart = date.match(/\d{2}:\d{2}/)[0];
  • 정규표현식을 이용해 "두자리 숫지 : 두자리 숫자"와 일치하는 패턴을 가져올 것이다.
  • 사용한 정규표현식을 간략히 설명하자면 \d는 숫자(digit)를 나타내고, {2}는 해당 패턴이 두 번 반복되어야 함을 나타낸다.
  • 즉 아래 첫 번째 소스 코드는 newDate에서 '숫자숫자:숫자숫자'와 매칭 되는 것을 찾으라는 뜻이다.
  • 그 중 0번째 요소를 반환하라고 했는데, 이는 우리가 원하는 숫자 외에 다른 데이터도 반환하기 때문이다.

  • timepart는 이제 숫자숫자:숫자숫자의 형태일 것이다. :를 기준으로 앞 뒤로 자르면 hour, minute으로 나눌 수 있다.
    const hour = timePart.split(':')[0];
    const minute = timePart.split(':')[1]; 

12시간제로 바꿔주기

  • 로직은 간단하다. 위에서 파싱한 hour이 12보다 크면 12를 빼주고, 아니면 그대로 가져가면 된다.
   if (hour < 12) {
      timePart = '오전 ' + hour + ':' + minute;
    } else {
      timePart = '오후 ' + (hour - 12) + ':' + minute;
    }

전체 코드

      const month = data.departureTime.slice(5, 7);
    const date = data.departureTime.slice(8, 10);

    const days = ['일', '월', '화', '수', '목', '금', '토'];

    const newDate = new Date(data.departureTime); // 요일을 영어로 얻기 위함
    const day = days[newDate.getDay()];

    let timePart = data.departureTime.match(/\d{2}:\d{2}/)[0];
    const hour = timePart.split(':')[0];
    const minute = timePart.split(':')[1];

    if (hour < 12) {
      timePart = '오전 ' + hour + ':' + minute;
    } else {
      timePart = '오후 ' + (hour - 12) + ':' + minute;
    }

구현 배경

  • 졸업작품 취뽀스테이션에서 채용 공고를 보여주는 페이지를 구현해야했습니다.
  • 고민하다 사람인에서 채용 공고를 크롤링해 보여주기로 결정했습니다.

개발환경 세팅

cheerio 설치

> npm install cheerio

- cheerio는 Node 환경에서 파싱을 도와주는 라이브러리입니다.
- 단, 파싱을 도와주는 라이브러리이기 때문에 크롤링 할 페이지는 axios를 이용해 가져와야합니다.
- JQuery 문법을 사용해 css 선택자, class이름, id 등으로 요소를 찾아 데이터를 수집할 수 있습니다.

axios 설치

> npm install axios

- axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트입니다.
- 요청 및 응답 데이터 변환, 응답 인터셉트 등의 역할을 합니다.
- 우리는 axios를 이용해 크롤링 하려는 페이지의 html 값을 가져올 것입니다.

크롤링 할 페이지의 html 값 가져오기

  • 잡코리아의 채용공고 페이지를 크롤링 해옵니다.
import axios from "axios";

function Crwaling() {

const getHtml = async (keyword) => {
    try {
      return await axios.get(
        `https://www.jobkorea.co.kr/Search/?stext=${keyword}&tabType=recruit&Page_No=1`
      );
    } catch (error) {
      console.error(error);
    }
  };

}

크롤링한 html 값 파싱해 넣기

  • 이를 cheerio를 이용해 파싱합니다.
const parsing = async (page) => {
    const $ = cheerio.load(page);
    const jobs = [];
    const $jobList = $(".post");
    $jobList.each((idx, node) => {
      const jobTitle = $(node).find(".title:eq(0)").text().trim();
      const company = $(node).find(".name:eq(0)").text().trim();
      const experience = $(node).find(".exp:eq(0)").text().trim();
      const education = $(node).find(".exp:eq(0)").text().trim();
      const regularYN = $(node).find(".option>span:eq(2)").text().trim();
      const region = $(node).find(".long:eq(0)").text().trim();
      const dueDate = $(node).find(".date:eq(0)").text().trim();
      const etc = $(node).find(".etc:eq(0)").text().trim();

      jobs.push({
        jobTitle,
        company,
        experience,
        education,
        regularYN,
        region,
        dueDate,
        etc,
      });
      setJobs(jobs);
    });
  };

서비스 적용 모습

  • 서비스에 직접 적용하면서 치명적인 오류가 있었습니다...
  • 바로 CORS에러가 났다는 점.. 제가 맡은 사람인 페이지와 팀원이 맡은 잡코리아 페이지 모두 CORS를 허용해주지 않아 에러가 발생했습니다.
  • 결국 서버를 만들어 서버에서 크롤링 값을 가져온 후 CORS를 허용해 클라이언트로 보내주는 방식으로 해결했습니다.

추가적으로 공부한 점

  • 웹페이지와 DOM
    • 웹페이지는 HTML 형식으로 제공되는 문서입니다.
    • 웹 브라우저로 웹 페이지에 접근한다는 것은 서버로부터 해당 주소에서 제공하는 HTML 문서를 HTTP 통신으로 전달받는 것을 의미합니다.
    • 전달 받은 HTML 문서는 단순 텍스트 형태이기 때문에 프로그램에서 사용하기 좋은 데이터 구조로 표현해야하는데, 이 구조를 DOM이라고 부릅니다.
  • DOM 트리
    • 웹 페이지의 모든 요소를 Document 객체가 관리합니다.
    • 따라서 웹 페이지의 요소를 잘 관리하고 제어하기 위해서는 Document 객체가 웹 페이지 요소들을 잘 반영하는 자료구조를 가지고 있어야 합니다.
    • 그래서 Document 객체 모델인 DOM은 트리 자료구조의 형태를 가지고 있습니다.
  • HTML 코드로 DOM 알아보기
<!DOCTYPE html>
<html lang="ko">
<head>
    <title>옥돔 아니고 DOM</title>
    <meta charset="UTF-8">
</head>
<body>
    <h1>DOM이란?</h1>
    <p><strong>30분 걸려만든</strong>다이어그램입니다.</p>
</body>
</html>

들어가기 전에

  • SVG 파일을 public > svg 폴더에 넣어서 직접 import 해와서 사용하고 있었다.
  • 그러니 해당 SVG 파일에 width, height 등의 속성을 넣으니 아래 사진과 같은 에러가 발생했다.
  • 따라서 vite-plugin-svgr 라이브러리를 이용해 SVG를 ReactComponent로 바꿔 사용하고자 한다.

환경 설정

라이브러리 설치

npm install --save-dev vite-plugin-svgr
yarn add -D vite-plugin-svgr
pnpm add -D vite-plugin-svgr

vite.config.js 파일 수정

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),

    /* 여기서부터
    svgr(
      {
      exportAsDefault: true,
      svgrOptions: {
        icon: true,
      },
    }
    ),
    여기까지 */
  ],
  server: {
    port: 3000,
  },
});

custom.d.ts 생성

  • Root 폴더에 custom.d.ts 파일을 만들어 아래 코드를 복붙한다.
declare module '*.svg' {
  import React = require('react');
  export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
  const src: string;
  export default src;
}

tsconfig.json 수정

  • include에 custom.d.ts를 추가해준다.
  "include": ["src", "custohttp://m.d.ts"],

package.json 수정

  • script 부분에 "svgr"을 추가해준다.
  • public/svg에 있는 파일을 src/assets/svg 폴더에 tsx파일로 바꾸어 준다는 뜻이다.
    "svgr" : "npx @svgr/cli -d src/assets/svg --ignore-existing --typescript --no-dimensions public/svg"

사용하기

  • public에 svg 폴더를 생성 후 해당 폴더에 svg 파일을 넣는다.

  • pnpm svgr / yarn svgr / npm svgr 등 각자의 패키지 매니저 명령어와 함께 svgr을 입력한다.

  • src/assets/svg 폴더에 public/svg 폴더에 있던 svg 파일들이 tsx형태로 저장된다.

  • 이를 가져다 쓰면 된다

  • 이제 width, height를 지정해줘도 에러가 나지 않는다 !!!

0. 공부하게 된 계기

  • Hipspot 프로젝트를 진행하면서 상태관리 라이브러리로 recoil을 사용했습니다.
  • 그러나 Hipspot 프로젝트에 참여할 당시에 나는 UI도 겨우 짤 정도의 수준이었고, 기능구현은 구글링해가며 겨우 해냈었습니다.
  • 때문에 상태가 정확히 무엇인지도, 상태관리 라이브러리가 무엇인지도 몰랐고, Recoil의 Atom을 사용하는 방법도 몰랐습니다.
  • Hipspot을 진행하며 눈치껏 팀원들 코드 보며, 문서를 조금 읽으며 어느정도 사용하는 방법을 이해하게 되었습니다.
  • 프로젝트가 마무리되면서 상태관리 및 상태관리 라이브러리에 대해서 정확히 이해하고자 공부를 시작하게 되었습니다 !

 

1. 상태(State)는 무엇인가?

  • State란, 컴포넌트가 기억하는 것이라고 생각하면 편합니다.
  • 예를들어 input에 우리가 타이핑을 하면 그 타이핑된 값이 쳐져야 하고
  • 이미지를 옆으로 넘기는 버튼을 누르면 다음 이미지가 보여져야합니다.
  • 위 예시들에서 컴포넌트는 타이핑된 값을 보여주기 위해선 타이핑 내용을 기억해야 하고, 다음 이미지로 넘어가기 위해선 현재 이미지를 기억해야 합니다.
  • 이렇듯 컴포넌트가 기억해야 하는 값을 우리는 상태라고 부릅니다.
  • 함수형 컴포넌트에서는 ‘useState’라는 Hook을 이용해 state를 다룰 수 있습니다

1.1 State를 사용하는 이유, 지역변수의 값을 바꿔주는거로는 안되나?

export default function Count() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }
  return (
    <>
      <button onClick={handleClick}>
        +1
      </button>
      <h3>  
        {index}
      </h3>
    </>
  );
}
  • 이렇게 지역변수의 값을 바꿔주는 것으로는 우리가 의도한 것처럼 숫자가 올라가지 않습니다. 그 이유로는 두 가지가 있습니다.
  • 첫 째, 지역변수는 렌더링 되면 유지되지 않습니다. 따라서 리액트가 이 컴포넌트를 두 번 렌더링 한다면, 렌더링 이전에 변경된 지역변수를 고려하지 않고 처음부터 다시 렌더링 합니다.
  • 둘 째, 지역변수가 변경된다고 해서 렌더링이 이뤄지지 않습니다. 리액트는 새로운 데이터와 함께 컴포넌트를 렌더링할 필요를 느끼지 못합니다.

 

위와 같은 이유로 우리는 버튼 클릭시 숫자가 업데이트 되도록 하려면

  1. 버튼 클릭시 리렌더링 되어야 합니다.
  2. 리렌더링 되면서도 컴포넌트가 숫자 값을 기억하고 업데이트 할 수 있어야 합니다.

2. useState은 뭔데 ?

  • React Hook이란 React 버전 16.8부터 제공하는 요소이며, 기존 class 바탕의 코드를 작성할 필요 없이 여러 기능을 사용할 수 있습니다.
  • useState는 이러한 Hook 중 하나이며, 상태 관리할 때 사용되는 Hook입니다.

2.1 useState 사용하기

  • useState는 두 가지 요소로 구성된다고 볼 수 있다.
    • 첫 째로 상태변수(위 예시에선 index)인데, 상태변수는 렌더링시에도 데이터를 잃지 않고 유지합니다.
    • 둘 째로 setter함수(위 예시에선 setIndex)인데, setter 함수를 업데이트 하면 리액트도 리렌더링 되고, 상태변수를 업데이트 합니다.
  • 지역변수로 작성했던 코드 useState로 바꿔보자
    • 위 코드를 실행하면 버튼 클릭 시 onClick 이벤트가 실행되어 handleClick함수가 호출됩니다.
    • handleClick함수가 실행되면 setIndex가 실행되는데, 이 setter 함수가 실행됨에 따라 index 변수의 값은 1 더해지고, 컴포넌트가 리렌더링 됩니다.
    • index 변수는 상태변수이기에 렌더링시에도 값을 유지하기에 화면에는 초기값 0에 1이 더해진 1이 표시됩니다.
import {useState} from 'React'

function App() { 
	const [상태변수, setter함수] = useState(상태의 초기값);
    const [index, setIndex] = useState(0);
 }

 

2.2 상태 넘겨주기

  • 리액트에서 상태를넘겨줄 땐 Props를 이용합니다.

2.2.1 State vs Props

State

부모 컴포넌트에서 자녀 컴포넌트로 데이터를 보내는 것이 아닌 해당 컴포넌트 내부에서 데이터를 전달할 때 State를 이용합니다.

  • 예를들어 검색 창에 글을 입력할 때 글이 변하는 것은 State을 바꿉니다.
  • State는 변경 가능합니다.
  • State가 변하면 re-render 됩니다.
State = {
    message: ' ',
    attachFile : undefined,
    openMenu : false,
};

 

Props

  • Properties의 줄임말입니다.
  • Props는 상속하는 부모 컴포넌트로부터 자녀 컴포넌트에 데이터등을 전달하는 방법입니다.
  • Props는 읽기 전용으로 자녀 컴포넌트 입장에서는 변하지 않습니다. 변경하고자 하면 부모 컴포넌트에서 state를 변경시켜주어야 합니다.

2.2.2 Props로 전달

const [todoData, setTodoData] = useState({title: 'todo1', time : 1});
<Lists todoDatas={todoData}/> // 이런식으로 props를 넘겨준다
<자녀컴포넌트 이름 자녀컴포넌트에서 사용할 이름 = {부모컴포넌트에서의 이름} />

//List.js todoDatas 객체 통째로 받고
function List (todoDatas) {
    return (
        <>
            //컴포넌트 내에서 직접 접근
            <div>할 일 : {todoDatas.name}    </div>
            <div>걸리는 시간 : {todoDatas.time}    </div>
        </>
    )
}

//List.js, 애초에 받을 때 구조분해 할당으로 받아버려
function List ({name, time}) {
    return (
        <>
            <div>할 일 : {name}    </div>
            <div>걸리는 시간 : {time}    </div>
        </>
    )
}

 

3. 그래서 뭐가 문제야 ? (Props Drilling)

 

  • 해당 그림은 Root에서 state를 만들고, 이를 Props Drilling 방식으로 하위 컴포넌트에게 state를 내려주고있습니다.
  • 만일 G 컴포넌트에서 상태가 변경되었고, 이를 J 컴포넌트에서 사용해야한다면
  • G → E → C → A → Root → H → J 순서로 상태가 전달이 것입니다.
  • 코드를 통해서 보면 (참고자료) App > FirstComponent > SecondComponent > ThirdComponent > ComponentNeedingProps
  • ComponentNeedingProps 컴포넌트에서 해당 Props를 사용하기 위해선 이렇게 전달하는 과정을 거쳐야 합니다.
import React from "react";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <FirstComponent content="Who needs me?" />
    </div>
  );
}

function FirstComponent({ content }) {
  return (
    <div>
      <h3>I am the first component</h3>;
      <SecondComponent content={content} />
    </div>
  );
}

function SecondComponent({ content }) {
  return (
    <div>
      <h3>I am the second component</h3>;
      <ThirdComponent content={content} />
    </div>
  );
}

function ThirdComponent({ content }) {
  return (
    <div>
      <h3>I am the third component</h3>;
      <ComponentNeedingProps content={content} />
    </div>
  );
}

function ComponentNeedingProps({ content }) {
  return <h3>{content}</h3>;
}

 

3.1 Props Drilling

3.1.1 Props Drilling이란?

  • Props Drilling이란 props를 ‘하위 컴포넌트로 전달하는 용도로만 쓰이는’ 컴포넌트를 거치며 React Component 트리의 한 부분에서 다른 부분으로 데이터를 전달하는 과정이다.
  • 이는 컴포넌트가 3~4개일 때는 문제가 되지 않지만, 여러 개의 컴포넌트가 있을 때는 문제가 될 수 있다.

3.1.2 Props Drilling의 장점

  • 컴포넌트 간에 데이터를 가장 쉽고 빠르게 전달할 수 있다.
  • 작은 규모의 application일 경우, 컴포넌트를 잘게 분해해서 props drilling을 한다면, 코드를 실행하지 않고 정적으로 따라가는 것만으로도 어떤 데이터가 어디서 사용됐는 지 파악하기 쉽고 수정도 용이하다

3.1.3 Props Drilling의 문제점

  • 필요보다 많은 props를 전달하다가, 컴포넌트 분리하는 과정에서 필요하지 않은 props가 계속 남거나 전달되는 문제
  • props 전달을 누락했는데 default props가 사용되어 props의 미전달을 인지하기 어려운 문제
  • props의 이름이 전달중에 변경되어 데이터를 추적하기 어려워지는 문제

3.1.4 Props Drilling을 피하려면

  • 렌더링 될 컴포넌트를 불필요하게 여러 컴포넌트로 나누지 않는다.
    • React는 단 하나의 컴포넌트에 application 전체를 작성하더라도 기술적인 제약이 없다.따라서 불필요한 컴포넌트 쪼개기를 할 필요가 없다.컴포넌트를 재사용해야할 상황을 기다렸다 분할해도 괜찮으며, 불필요한 props drilling을 방지할 수 있다.
  • defaultprops를 필수 컴포넌트에 사용하지 않는다.
    • deafultProps를 사용하면 필요한 props가 전달되지 못한 상황임에도 오류가 가려지게된다. 따라서 defaultProps를 필수적이지 않은 컴포넌트에만 사용하면 props drilling으로 인한 문제를 막을 수 있다.
  • 가능한 관련성이 높은 곳에 state를 위치한다.
    • 어떤 데이터가 application의 특정 위치에서만 필요하면 최상위 컴포넌트에 state를 위치시키는게 아닌, 해당 state를 필요로하는 컴포넌트들의 최소 공통 부모 컴포넌트에서 관리하는 것이 효율적이다.
  • 상태관리 도구를 사용한다.
    • 데이터를 필요로하는 컴포넌트가 props drilling의 깊숙히 위치한다면, React의 Context API를 사용하거나, Redux, Recoil 등의 외부 전역 상태관리 라이브러리를 사용해서 문제를 해결할 수 있다.
  • Children을 사용한다. (Legacy API)
    • children을 사용하여 리팩토링을 진행하면, 하나의 컴포넌트에서 값을 관리하고, 그 값을 하위요소로 전달할 때 코드추적이 어려워지지 않게됩니다.
    • 그러나 리액트 공식문서에도 나와있듯, children은 잘 사용되지 않고 빈약한 코드 (fragile code)로 이어질 수 있습니다.

 

4. Context

  • Context란 application에서 사용할 상태들을 context 내에서 관리하여 보다 컴포넌트들이 편하게 접근 가능하게 합니다.
  • 그림으로 알 수 있듯 Context를 사용하면 Props Drilling을 막을 수 있습니다.

4.1 Context 사용방법

  1. createContext 메서드를 사용해 context를 생성합니다.
  2. 생성된 context를 가지고 context provider로 컴포넌트 트리를 감쌉니다.
  3. value props를 사용해 context provider에 원하는 값을 입력합니다.
  4. context consumer를 통해 필요한 컴포넌트에서 그 값을 불러옵니다.

 

5. 상태관리 라이브러리란 ?

  • 리액트에서 사용하는 데이터를 담는 변수인 State를 전역적으로 관리하는 툴입니다.
  • 상태관리 라이브러리에 대한 자세한 설명은 추후 포스팅하도록 하겠습니다.

 

cf ) 공식문서 보면서 알게된 어휘인데 ‘from scratch’가 처음부터라는 뜻이었다. 그래서 ‘render from scratch’라 하면 ‘처음부터 렌더링된다’ 라고 해석가능합니다.

 

 

참고자료

https://devowen.com/459

https://react.dev/learn/scaling-up-with-reducer-and-context

https://react.vlpt.us/basic/22-context-dispatch.html

https://redux.js.org/tutorials/essentials/part-1-overview-concepts

https://slog.website/post/13

https://codingpracticenote.tistory.com/56

+ Recent posts