반응형

🎞️ 롤링 슬라이드

주식이나 뉴스같은 곳에서 한줄로 끊임없이 텍스트가 물처럼 한방향으로 흐르거나 요즘 트렌드의 스타일로 작성된 사이트들을 구경하다 보면 자주 접할 수 있는 스타일의 UI이다.
텍스트뿐만 아니라 관련된 이미지를 통해서도 좀 더 인터렉티브하고 동적으로 움직이다보니 시각적으로 집중이 되는 효과를 확인 할 수 있다.
대표적으로 찾은 기능으로 네이버의 vibe사이트에서 추천 플레이리스트 부분에 아티스트들의 앨범 정보가 끊임없이 흐르는 애니메이션도 볼 수 있는데, 해당 기능을 참고하여 구현하고자 한다.


바이브 url: https://vibe.naver.com/about/.

🤔 동작 방식

구현하기 전 구현에 대한 설명이 필요하다.

  1. 먼저 슬라이드 형태로 흘러가야할 요소들을 한줄로 세워준다.
  2. 똑같은 요소들을 하나 더 만들어준다.
  1. css 속성을 통해 한줄로 이어서 붙여준다. (부모속성에 flex와 nowrap처리를 통해 쉽게 구현이 가능하다.)
  1. 한줄이 된 슬라이드를 각기 다른 옵션으로 애니메이트를 처리한다.
  2. 첫번째 원본 영역을 10초간 이동하면 그동안 빈 공백의 10초를 기다려야 하는데, 이때 복사 영역으로 똑같이 붙여서 10초간 이동시키고 무한으로 돌린다.
  3. 슬라이드 영역에 마우스를 올리면 animation-play-state의 값을 paused처리 하는 클래스를 토글 형태로 처리하면 된다.
    .stop{
     animation-play-state: paused;
    }

⚙️ 구현에 필요한 기술

css 레이아웃 개념과 animation의 개념, @keyframe 사용법 익히면 별다른 라이브러리 없이 구현이 가능하다.
hook또한 useState하나만 사용하여 처리 할 예정이다.
아래 구현 예시를 확인해보자.

🪄 구현 시작

HomePage.tsx

import { useState } from "react";
import "./Homepage.scss";

const slides = [
    { color: "red", target: "#" },
    { color: "orange", target: "#" },
    { color: "yellow", target: "#" },
    { color: "green", target: "#" },
    { color: "blue", target: "#" },
    { color: "navy", target: "#" },
    { color: "purple", target: "#" },
];

export default function Homepage() {
    const [animate, setAnimate] = useState(true);
    const onStop = () => setAnimate(false);
    const onRun = () => setAnimate(true);

    return (
        <div className="wrapper">
            <div className="slide_container">
                <ul
                    className="slide_wrapper"
                    onMouseEnter={onStop}
                    onMouseLeave={onRun}
                >
                    <div
                        className={"slide original".concat(
                            animate ? "" : " stop"
                        )}
                    >
                        {slides.map((s, i) => (
                            <li
                                key={i}
                                className={i % 2 === 0 ? "big" : "small"}
                            >
                                <div
                                    className="item"
                                    style={{ background: s.color }}
                                ></div>
                            </li>
                        ))}
                    </div>
                    <div
                        className={"slide clone".concat(animate ? "" : " stop")}
                    >
                        {slides.map((s, i) => (
                            <li
                                key={i}
                                className={i % 2 === 0 ? "big" : "small"}
                            >
                                <div
                                    className="item"
                                    style={{ background: s.color }}
                                ></div>
                            </li>
                        ))}
                    </div>
                </ul>
            </div>
        </div>
    );
}

Homepage.scss

* {
    padding: 0;
    margin: 0;
}
ul,
li {
    list-style: none;
}

.wrapper {
    .slide_container {
        overflow: hidden;

        .slide_wrapper {
            display: flex;
            flex-wrap: nowrap;
        }
        .slide {
            display: flex;
            align-items: center;
            flex-wrap: nowrap;
            position: relative;
            border-top: 1px solid #bbb;
            border-bottom: 1px solid #bbb;
            padding: 40px 0;

            &::before {
                content: "";
                display: block;
                width: 100%;
                height: 1px;
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background-color: #bbb;
                z-index: 1;
            }
            &.original {
                animation: 10s linear infinite normal none running
                    infiniteAnimation1;
            }
            &.clone {
                animation: 10s linear infinite infiniteAnimation2;
            }
            &.stop {
                animation-play-state: paused;
            }

            li {
                margin: 0 80px;
                cursor: pointer;
                z-index: 2;
                transition: 0.3s;
                transform: scale(1);
                &:hover {
                    transform: scale(0.98);
                    &::after {
                        content: "";
                        position: absolute;
                        top: 0;
                        left: 0;
                        bottom: 0;
                        right: 0;
                        width: 100%;
                        height: 100%;
                        background-color: rgba(0, 0, 0, 0.2);
                    }
                }
                &.big {
                    width: 280px;
                    height: 280px;
                }
                &.small {
                    width: 200px;
                    height: 200px;
                }

                .item {
                    width: 100%;
                    height: 100%;
                }
            }
        }
    }
}

@keyframes infiniteAnimation1 {
    0% {
        transform: translateX(0%);
    }
    50% {
        transform: translateX(-100%);
    }
    50.1% {
        transform: translateX(100%);
    }
    100% {
        transform: translateX(0%);
    }
}
@keyframes infiniteAnimation2 {
    0% {
        transform: translateX(0%);
    }
    100% {
        transform: translateX(-200%);
    }
}

✨ 구현 결과물

아주 잘된다.
바이브처럼 뒷부분에 선도 그어봤다.
괜찮은 배경이미지와 색상 대신 이미지를 활용하면 더욱 좋은 결과물을 볼 수 있을 것 같다.

(아래는 위 소스를 기반으로 작성한 리액트 샘플 소스를 받아볼 수 있는 github주소입니다.)

github: [https://github.com/myhappyman/infinite_rolling]

 

반응형
반응형

Semantic tag

웹 페이지 레이아웃을 잡을때 div만을 사용해서 사용할 수도 있지만, 시멘틱 태그를 사용하므로서 웹 사이트의 각 구조에 대한 내용을 명확하게 전달할 수 있다. 머리글은 header 바닥글은 footer와 같은 태그를 사용하고 문서의 내용은 section을 사용할 수도 있다.

div태그만으로 레이아웃을 잡을 수 있는데, 시멘틱 태그를 사용하는 이유는 뭘까?

  1. 일단 SEO(검색엔진 최적화)에 유리해진다.
    검색 엔진이 태그의 목적에 맡도록 설계된 사이트는 효율적으로 정보를검색하여 검색 노출이 유리해진다.
  2. 웹 접근성에 효율적이다.
    웹 접근성이 필요로 하는 시각장애인인이나 스크린리더같은 환경에서 용이하다.
  3. 유지보수성의 장점
    div만 남발하는 경우 class나 내부의 설계된 내용을 확인해야 하지만, 시멘틱 태그를 확인하므로서 해당 태그의 기능이 어떤걸 뜻하는지 확인이 용이해진다.

아래에서 시멘틱 태그별 내용을 정리하고자 한다.

⭐header

웹 사이트의 상단 부분을 맡거나 머리글의 요소이다.
로고나 제목 등이 위치하며 보통은 바디부분의 최상단이지만 불가피한 경우 레이아웃 구조상 메인의 상단으로 작성되는 경우도 있다.

  • 일반적으로 header에 포함된 내용이다.
    • 하나 이상의 제목 요소(h1 ~ h6)
    • 로고 또는 아이콘
    • 배포 정보

⭐nav

네이게이션의 줄임말로 목차나 리스트와 같은 항목이 들어가는 요소이다.
다른 페이지로 이동시키는 링크의 덩어리가 위치하다보니 메뉴로 구성되는 경우가 많다.
모든 링크관련 요소가 nav에 있어야 하는것은 아니다.
일반적으로 ul, li, a태그의 조합으로 구성한다.

⭐main

웹 사이트의 주요 내용이 담기는 요소이다.
사이드바, 저작권 정보, 사이트 로고, 검색 양식과 같은 반복되는 컨텐츠를 포함시키지 않는다.

⭐section

주제, 카테고리, 영역 별로 그룹을 구분하는 용도로 사용하는 요소이다.

⭐article

문서내에서 독립적인 컨텐츠를 지정하는 요소이다.
article의 내용 자체로 독립적인 내용이어야 한다.

  • 포럼 게시물
  • 블로그 게시글
  • 뉴스

⭐aside

좌측이나 우측의 사이드에 붙는 요소이다.
컨텐츠와 별개의 일부 컨텐츠를 작성할 때 사용된다. 주변 컨텐츠와 간접적인 내용이 들어가거나 본문과 부수적인 내용이 들어간다.
구글 광고와 같은 부수적인 내용이 대부분이 aside에 들어간다.

⭐footer

페이지 내 바닥글을 정의하는 요소이다.

  • footer에 포함되는 내용은 아래와 같다.
    • 저작권 정보
    • 발행 정보
    • 연락처
    • 사이트맵
    • 상단으로 이동시키는 링크
    • 관련 문서 정보
반응형
반응형

Redux에 대해 알아보기

그동안 전역 상태관리를 위해 recoil로 충분히 사용이 가능했지만, redux 또한 놓칠 수 없는 개념이기에 학습하고 정리하는 내용을 작성해본다.
먼저 관련된 키워드와 내용에 대해 알아보고, 사용법을 알아보고자 한다.

🔹Action

액션은 Reducer와 소통하는 존재이다.
변화가 일어나면 액션이라는 것이 발생하는데, 구분자 형태로 사용된다.

액션 작성 규칙
-액션은 객체(Object)형태로 작성해야한다.
-객체 내부에는 type이라는 필드를 필수적으로 가지고 있어야 한다.

🔹ActionCreator

액션 객체를 만들어주는 함수이다.
해당 함수는 액션을 만들어주다보니 당연히 Object형태를 반환하고 type 필드를 가진다.
그 외에 동작할 명세를 작성한다.

const changeInput = (text:string) => ({
  type: "CHANGE_INPUT",
  text
});

🔹Reducer

변화를 일으키는 함수다.
현재 상태와 전달받은 액션 객체 파라미터를 받는다.
두 개의 값을 비교하여 새로운 상태를 반환한다.
구분자할 값이 많아지면 비교적 if문보단 switch문이 가독성이 좋아서 switch를 사용한다.

const initialState = {
    counter: 1
}
function reducer(state=initialState, action){
    switch(action.type){
        case "INCREASE":
        return state.counter+1
    }
}

🔹Store

프로젝트에 store는 한 개를 가진다.
(여러개를 가질 수도 있지만 복잡해질 수 있음)
스토어에는 현재 어플리케이션의 상태와 리듀서가 들어간다.
그 외에 중요한 내장 함수들로 구성된다.

🔹Dispatch

리듀서에서 stateaction 두 개의 파라미터를 받는데, 여기서 reducer에게 action을 보내는 행위를 dispatch가 처리한다.
dispatch는 리듀서를 불러와서 current state와 내가 작성한 action을 처리한다.

🔹Subscribe

store에 등록된 값의 변화를 감지하다가 변화가 발생하면 콜백 함수가 동작한다.

🔹Vanilla-Redux

npm install redux
const { createStore } = require("redux");

const divToggle = document.querySelector(".toggle");
const counter = document.querySelector("h1");
const btnIncrease = document.querySelector("#increase");
const btnDecrease = document.querySelector("#decrease");

const TOGGLE_SWITCH = "TOGGLE_SWITCH";
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";

/**
 * 액션을 가지고 액션 생성 함수를 작성한다.
 * type은 필수값으로 가지고 있어야하며 원하는 형태로 커스텀 할 수 있다.
 */
const toggleSwitch = () => ({ type: TOGGLE_SWITCH });
const increase = (difference) => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });

// 초기값 설정
const initialState = {
    toggle: false,
    counter: 0,
};

/**
 * reducer defined.
 * 변화를 일으키는 함수다. state, action 두 개의 값을 받는다.
 */
function reducer(state = initialState, action) {
    switch (action.type) {
        case TOGGLE_SWITCH:
            return {
                // 불변성 유지를 위해 새로운 객체를 return
                ...state,
                toggle: !state.toggle,
            };
        case INCREASE:
            return {
                ...state,
                counter: state.counter + action.difference,
            };
        case DECREASE:
            return {
                ...state,
                counter: state.counter - 1,
            };
        default:
            return state;
    }
}

// store작성 리듀서 함수를 넣어준다.
const store = createStore(reducer);

/**
 * render함수 작성
 */
const render = () => {
    const state = store.getState();
    if (state.toggle) {
        divToggle.classList.add("active");
    } else {
        divToggle.classList.remove("active");
    }
    counter.innerHTML = state.counter;
};

render();
store.subscribe(render);

// 구독하기
// const listener = () => {
//     console.log("상태가 업데이트됨.");
// };
// const unsubscribe = store.subscribe(listener);
// unsubscribe();

divToggle.onclick = () => {
    store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
    store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
    store.dispatch(decrease());
};
반응형
반응형

💎useMemo 파헤치기

🤔그래서 useMemo가 뭐지?

리액트에서 useMemo를 사용하게 되면 불필요한 렌더링을 막고 앱을 최적화 시킬 수 있다.
여기서 Memo는 Memoization을 뜻하는데, 메모이제이션이란 간단하게 프로그램적으로 반복 행위가 있는 것을 메모리에 저장하여 가져다 사용해서 실행속도를 빠르게 처리하는 기술을 뜻한다.
즉, 한번 동작자체가 부하가 심한 행위가 있는 기능이면서 페이지의 렌더링이 심한 컴포넌트에서 적용하기에 안성맞춤 기술이다.
아직 텍스트만으론 잘 이해가 되질 않는다. 더욱 자세히 알아보겠다.

⚙️useMemo의 구성

function fn(){...}
useMemo(()=> fn(), []);

useMemo는 콜백함수와 의존성 배열 2개의 매개변수를 받는다.
첫번째는 저장(캐싱) 할 함수값을 넣는다.
두번째 의존성 배열부분에는 업데이트 되었을때 저장한 함수가 동작될 요소를 넣어준다.
즉, 배열에 저장된 요소가 업데이트 되면 그때 저장된 함수가 동작된다.
빈 배열을 넣게되면 페이지가 처음 로드되었을때 한번만 캐싱되고 이후부터 사용할 때만 캐싱된 함수를 호출해서 사용한다.

🙃그렇다면 무조건 쓰면 좋을까?

그렇다고 무조건 모든 객체나 원시타입들을 useMemo에 저장해놓고 쓰는게 좋을까?
대답은 No이다.
메모이제이션이기 때문에 결국 useMemo에 등록된 요소들은 결국 어딘가에 저장이 되는데, 개발을 하면서 상황에 맞지 않는 모든 것을 다 저장한다면 오히려 페이지에 부하가 발생하고 느려지는 상황을 초래할 수 도 있다.

😤그럼 언제 써야합니까?!

예시가 제일 빠른 확인이 가능할 것 같다.

import { useState } from "react";

function slow(ms: number) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) {}
}

function calculator(arr: number[]) {
    console.log("calculator --- start");
    slow(100);
    if (arr.length === 0) return 0;
    return Math.floor(arr.reduce((a, b) => a + b, 0) / arr.length);
}

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [number, setNumber] = useState("");
    const [inputNumber, setInputNumber] = useState<number[]>([]);
    return (
        <div>
            <div>
                <input
                    type="number"
                    name="input"
                    value={number}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setNumber(e.target.value);
                    }}
                />
                <button
                    onClick={() => {
                        setInputNumber((prev) => [...prev, +number]);
                        setNumber("");
                    }}
                >
                    추가
                </button>

                <div>
                    <b>지금까지 등록된 값</b>
                    <ul>
                        {inputNumber.map((x) => (
                            <li>{x}</li>
                        ))}
                    </ul>
                </div>

                <div>
                    <b>등록된 평균값:</b>
                    {calculator(inputNumber)}
                </div>
            </div>
        </div>
    );
}

일단 부하가 심한 기능이라는 가정을 위해 일부러 slow라는 함수를 정의하여 calculator()내부에서 호출하여 강제로 동기화 기능을 처리하였다.
해당 부분때문에, 약 0.1초 정도의 부하만 줬는데도 부드럽지 않고 끊기는 느낌이 있다.

해당 예제는 input에 입력한 숫자를 추가하면 평균값을 보여주는 간단한 예제인데, 벌써부터 최악인 모습으로 input에 입력할때마다 onChange에서 변경된 state값에 의해 매번 페이지가 재렌더링이 일어나면서 느려터진 slow함수덕에 엄청 버벅거리는 모습을 경험할 수 있다.

실제로도 input에 "1234" 4번만 입력했는데 매번 calculator가 호출되고 있고 있다.

이럴때 바로 useMemo를 사용하면 추가버튼을 누를때만 해당 함수가 호출되어 최적화를 할 수 있다.

최적화하기

import { useState, useMemo } from "react";

function slow(ms: number) {
    const wakeUpTime = Date.now() + ms;
    while (Date.now() < wakeUpTime) {}
}

function calculator(arr: number[]) {
    console.log("calculator --- start");
    slow(100);
    if (arr.length === 0) return 0;
    return Math.floor(arr.reduce((a, b) => a + b, 0) / arr.length);
}

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [number, setNumber] = useState("");
    const [inputNumber, setInputNumber] = useState<number[]>([]);

    /*
     * useMemo를 통해 최적화를 한다.
     * 두번째 매개변수 의존 배열에는 해당 값이 바뀔때만 호출될 수 있도록 설정한다
     * 첫번째 매개변수에는 콜백함수를 넣는데 두번째에 넣은 값이 바뀔때마다 해당 콜백이 호출된다.
     */
    const avg = useMemo(() => calculator(inputNumber), [inputNumber]);
    return (
        <div>
            <div>
                <input
                    type="number"
                    name="input"
                    value={number}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setNumber(e.target.value);
                    }}
                />
                <button
                    onClick={() => {
                        setInputNumber((prev) => [...prev, +number]);
                        setNumber("");
                    }}
                >
                    추가
                </button>

                <div>
                    <b>지금까지 등록된 값</b>
                    <ul>
                        {inputNumber.map((x) => (
                            <li>{x}</li>
                        ))}
                    </ul>
                </div>

                <div>
                    <b>등록된 평균값:</b>
                    {avg}
                </div>
            </div>
        </div>
    );
}

이제 초기 렌더링될 때 한번 호출되고 input에 입력해보면 전혀 부하가 발생하지 않는 걸 볼 수 있다.
숫자를 입력하고 추가버튼을 누르면 그때 콜백함수가 호출되면서 calculaotr가 호출된다.

⚠️유의사항

어느정도 useMemo를 언제 사용해야할지와 사용법에 대해 알아보았는데, 조심해야할 부분이 있다.
자바스크립트에는 원시타입과 객체타입의 개념이 있다.
원시타입의 경우 특정 정수나 문자열이 있을 때, 해당 값을 주소값에 넣어두고 같은 값이라면 어떤 변수든 같은 주소값을 바라보기에 아래와 같이 비교 연산이 가능하다.

const test = "hello";
const test2 = "hello";
test === test2 // true 같은 주소값을 바라본다.

하지만 객체타입인 배열이나 객체의 종류의경우 비교연산시 문제가 발생한다.

const obj = {name: "mike"}; // 0x01 에 할당
const obj2 = {name: "mike"}; // 0x02에 할당
obj === obj2 // false 주소값이 서로 다르다.

shallow copy, deep copy의 개념이 필요한 부분인데 이부분에 대해서 더 자세히 알아보면 좋을 것이다.

객체타입의 오류

아래 예제를 보면 위에서 왜 객체 타입에 대한 개념을 언급했는지 알 수 있다.

import { useState, useEffect } from "react";

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [name, setName] = useState("");
    const [regitName, setRegitName] = useState("");
    const obj = { name: regitName };

    useEffect(() => {
        console.log("regitName에 추가될 때만 호출되어야 합니다!");
    }, [obj]);

    return (
        <div>
            <div>
                <input
                    type="text"
                    name="input"
                    value={name}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setName(e.target.value);
                    }}
                />
                <button onClick={() => setRegitName(name)}>변경</button>
                <div>
                    <b>입력한 이름</b>
                    <ul>{obj.name}</ul>
                </div>
            </div>
        </div>
    );
}

obj라는 객체를 useState에서 받아온 regitName값으로 정의하는 경우 해당 경우에도 매번 재 렌더링이 발생한다.
새로운 객체를 계속해서 새로 넣는다고 판단되기 때문인데, 이런 경우에도 useMemo를 통해 처리가 가능하다.

객체 개선하기

import { useState, useEffect, useMemo } from "react";

// useMemo 테스트 컴포넌트
export default function TestUseMemo() {
    const [name, setName] = useState("");
    const [regitName, setRegitName] = useState("");
    // const obj = { name: regitName };
    const obj = useMemo(() => {
        return { name: regitName };
    }, [regitName]);

    useEffect(() => {
        console.log("regitName에 추가될 때만 호출되어야 합니다!");
    }, [obj]);

    return (
        <div>
            <div>
                <input
                    type="text"
                    name="input"
                    value={name}
                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
                        setName(e.target.value);
                    }}
                />
                <button onClick={() => setRegitName(name)}>변경</button>
                <div>
                    <b>입력한 이름</b>
                    <ul>{obj.name}</ul>
                </div>
            </div>
        </div>
    );
}

위 처럼 obj객체를 useMemo로 감싸서 처리하면 추가 버튼을 누르는 경우에만 regitName이 동작하면서 useEffect의 콘솔값이 그때 찍히는걸 볼 수 있다.

반응형
반응형

변환하면서 자주 만나는 에러 종류 정리

React에서 typescript로 작성하면서 자주 만나는 에러들을 정리해보려고 한다.

No overload matches this call.

export default function S_useMemo(){
    const [number, setNumber] = useState("");
    const [list, setList] = useState([]);

    const onInsert = (e: React.MouseEvent<HTMLButtonElement>) => {
        setList((prev) => prev.concat(parseInt(number))); //error No overload matches this call.
        setNumber("");
    };

    return (...)
}

기존 state의 값인 list에 concat으로 붙여서 기존 배열을 늘려나가는 방식으로 동작하는 insert함수이다.
해당부분에서 state에 담긴 문자열 number를 정수형으로 변환하여 붙여넣는 방식인데, No overload matcheds this call 에러가 발생하고 있다.
기본적으로 typescript이더라도 useState초기값에 따라 값을 추론하여 타입을 지정해주는데, const [list, setList] = useState([]);
이부분에서 list배열은 어떤 타입의 배열을 받는건지 정해진 값이 없어서 발생한 오류이다 제네릭으로 정수형 배열을 받는다고 처리해주면 된다.

해결

export default function S_useMemo(){
    const [number, setNumber] = useState("");
    const [list, setList] = useState<number[]>([]);

    const onInsert = (e: React.MouseEvent<HTMLButtonElement>) => {
        setList((prev) => prev.concat(parseInt(number))); //error No overload matches this call.
        setNumber("");
    };

    return (...)
}
반응형
반응형

Node.js 교과서를 학습하고 각 단원별 내용을 정리하는 포스팅.

 

Node.js

노드는 자바스크립트 실행기로 자바스크립트 문법을 익히면 노드를 활용 할 수 있다.
기존의 자바스크립트는 웹 브라우저에서 그저 약간의 동적 처리를 위해서 존재했다.
하지만 2008년 구글에서 V8엔진을 사용해 크롬을 출시하고, 오픈소스로 코드를 공개했는데, 기존의 다른 엔진보다 속도가 월등히 빨랐다.
이전에도 자바스크립트를 브라우저 외에서 사용하고자 하는 시도가 있었으나 속도 문제로 적용을 못하던 이슈를 해결하게 되었고, 이로 인해 2009년에 노드가 탄생하게 됨.

 

Node.js: 크롬 V8 자바스크립트 엔진으로 빌드된 자바스크립트 runtime. 노드를 통해 서버를 구성하거나 자바스크립트 프로그램을 실행하는 런타임으로 사용할 수 있다.
runtime: 특정 언어로 만든 프로그램을 실행할 수 있는 환경
server: 네트워크를 통해 클라이언트로 정보, 서비스를 제공하는 컴퓨터나 프로그램들을 총칭한다.
client: 요청을 보내는 주체를 말한다. 브라우저(웹), 앱, 프로그램이나 또 다른 별도의 서버들도 클라이언트가 될 수 있다.

 

Node.js의 구조

노드는 V8, libuv(라이브러리)를 사용하는데, C, C++ 로 구현되었고, 해당 언어는 몰라도 된다.
libuv는 노드의 특성인 이벤트 기반, 논블로킹 I/O 모델을 구현한다.

 

V8: 구글 크롬 브라우저, 안드로이드 브라우저에 탑재된 자바스크립트 엔진
libuv: 비동기I/O에 중점을 두고 있는 라이브러리로 Node.js용으로 만들어졌으나 현재는 크로스 플랫폼을 지원한다. 스레드 풀을 사용하여 비동기I/O를 작업하지만 네트워크 I/O는 OS에서 처리한다.

이벤트 기반

이벤트가 발생하면 미리 지정한 작업을 수행하는 방식들로 addEventListener를 통해 등록한 클릭이나 키 이벤트 행위나 네트워크 요청들을 말한다.
이벤트가 발생하면 어떤 행위를 할지 미리 등록해야하는데, 이런 행위를 이벤트 리스너에 콜백 함수를 등록한다고 표현한다.
이벤트 기반 모델에서는 이벤트 루프라는 개념이 있는데, 동시에 이벤트가 일어나면 어떤 행위를 순차적으로 호출할지 판단해야하는데 이벤트 루프가 해당 역할을 합니다.
자바스크립트는 기본적으로 맨 위에서 아래로 실행되며 한줄씩 실행되고 호출 해야할 함수를 만나면 call stack영역에 담아두고 실행되는 동안 존재하다가 실행되면 사라진다.

 

📄call stack example1.js

function one(){
    two();
      console.log("one");
}
function two(){
      three();
    console.log("two");
}
function three(){
      console.log("three");
}
one();
// call stack
// 1. one() 함수가 call stack에 쌓임
// 2. two() 함수가 call stack에 쌓임
// 3. three() 함수가 call stack에 쌓임

// run
// 1. three 출력
// 2. two출력
// 3. one출력
// 각 함수별 처리때문에 실행은 call stack에 쌓인것과 반대로 출력된다.

 

📄call stack example2.js

function run(n){
    console.log("3초 뒤 실행");
}
console.log("start!");
setTimeout(run, 3000);
console.log("end!");

start!, end!, 3초 뒤 실행이 찍히는 걸 볼 수 있는데, 실제 호출 스택은 아래와 같이 동작한다.

  1. 호출스택에 console.log("start!")가 쌓이고 테스크 큐에 의해 바로 실행된다.
  2. 호출스택에 setTimeout() 메소드가 쌓이고 백그라운드로 넘긴다.
  3. 호출스택에 console.log("end!")가 쌓이고 테스크 큐에 의해 바로 실행된다.
  4. 백그라운드에서 3초가 지나면 테스크 큐로 run메소드를 넘긴다.
  5. 테스크 큐에서 run을 실행하면서 console.log("3초 뒤 실행")이 출력된다.

event loop: 이벤트가 발생하면 호출될 콜백 함수들을 관리하고 실행순서를 결정해주는 역할을 한다.
background: setTimeout같은 타이머, 이벤트 리스너들이 대기하는 곳으로 여러 작업이 동시에 일어날 수 있다.(setTimeout시간이 되어 실행되면서 클릭도 동시에 일어날 수 있음)
task queue: 이벤트가 발생하고 백그라운드에서 테스크 큐로 타이머나 이벤트의 콜백이 넘어오는 곳 정해진 순서대로 실행되지만 특정한 경우에는 순서가 바뀔 수도 있다.

논블로킹 I/O

작업 종류는 두종류가 있는데, 동시에 실행 가능한 작업과 동시에 실행이 불가능한 작업이 존재하는데, 일반적으로 우리가 작성한 코드들은 동시에 실행될 수 없다.
여기서 동시에 실행가능한 것은 무엇일까? 바로 I/O작업을 말하는데, 노드에서 논블로킹 방식으로 제공한다.
Input/Output: 입출력을 말하며, 파일 시스템에 접근(파일을 읽거나 쓰기)하거나 네트워크 작업 등
Blocking: 이전 작업이 있으면 끝날때까지 기다리다가 이전 작업이 끝나야 비로소 현재 작업이 수행 됨.
Non Blocking: 이전 작업을 신경쓰지않고 동시에 작업들을 수행한다.

한 작업당 1초가 걸리는데, 10개의 작업이 있다면 블로킹으로 처리시 10초가 걸린다.
하지만 논블로킹이라면? 10초보다는 훨씬 줄어들어서 완료가 된다.
setTimeout(callback, 0)형태로 논블로킹을 간접 체험 할 수 있는데, 아무리 논블로킹 형태로 작성하더라도 직접 작성하면 전체 소요시간이 짧아 지진 않는다. 직접 작성한 코드들은 동시에 실행되지 않기 때문에, I/O작업에서 효율을 볼 수 있다.

싱글 스레드

싱글 스레드란 말그대로 스레드가 한개라는 뜻이다. 우리가 작성한 코드들이 동시에 실행되지 않는 이유가 노드는 싱글 스레드이기 때문이다.
(하지만 내부를 자세히 살펴보면 노드를 실행하면 하나의 프로세스가 생성되고, 그 프로세스 안에는 여러개의 스레드가 존재하는데, 우리가 직접 제어하는 스레드가 한개뿐이다.)

 

멀티 스레드라는 말도 있는데, 이건 반대다 여러개의 작업을 동시에 실행할 수 있다.(ex) java)
그렇다면 멀티 스레드를 사용하는게 무조건 좋을까? 그건 또 아니다.
아래는 김밥천국을 예시로 들어본다.

싱글, 멀티스레드별 블로킹, 논블로킹

-싱글스레드&블로킹({직원 1명, 고객 3명}, {직원:thread, 고객: response})
고객 3명이 직원 1명에게 들어온 순서대로 주문을 요청하고 첫번째 음식이 나오면 다음 손님 주문을 받고 음식이 나오면 제공한다. 지연시간이 길어진다.

 

-싱글스레드&논블로킹({직원 1명, 고객 3명}, {직원:thread, 고객: response})
고객 3명이 직원 1명에게 동시에 주문을 요청하고 음식이 나오면 직원 1명이 고객 3명에게 주문에 알맞은 음식을 제공한다. 지연시간이 대폭 줄어든다.

 

-멀티스레드&블로킹({직원 3명, 고객 3명}, {직원:thread, 고객: response})
직원이 3명이므로 고객 1명당 직원이 1명씩 각각 붙어서 각 메뉴를 주문받고 완료된 요리를 제공한다. 빠르지만 효율적이지 않다.

 

언뜻 보기엔 좋아보이지만 손님에 맞춰 직원이 들어나다보니 비용이 많이(자원 낭비) 듭니다.
손님이 자리를 떠나면 놀고 있는 직원들이 많아질 수 있습니다.
즉, 멀티스레드이면서 논블로킹으로 작성하면 효율적으로 할 수 있지만 이런 방식의 프로그래밍은 어려운 방법으므로 멀티 프로세싱 방식을 사용한다.

서버로서의 Node.js

노드는 기본적으로 싱글 스레드&Non blocking 모델을 사용한다.(JS 특성)
서버이기 때문에 자연스럽게 클라이언트의 I/O의 요청 작업이 많은데, libuv라이브러리를 사용해 I/O 작업을 논블로킹 방식으로 처리한다.
따라서 스레드 하나가 많은 수의 I/O작업을 감당한다.
하지만 CPU부하가 많은 작업을 하게되면 서버가 죽어버릴 수 있기 때문에 위험하다.
그렇기때문에 이미지, 비디오처리, 대규모 데이터 처리와 같은 CPU에 부하 작업이 많은 곳에선 서버로 권장하지 않는다.
이 외에도 싱글 스레드로 동작하다보니 에러를 제대로 처리하지 못하는 경우에도 하나뿐인 스레드가 죽으면서 서버가 죽고 전체 서비스가 동작하지 않는 현상도 일어날 수 있다.

 

장점 단점
적은 자원으로 서버를 구성 싱글 스레드라 부담이 크다. CPU 코어를 하나만 씀
I/O작업이 많은 서버에 좋음 CPU작업에 부하가 간다면 부적합
멀티 스레드보다 쉬움 하나뿐인 스레드 관리 필요
웹 서버가 내장 규모가 커지면 nginx와 연결해야함
JS를 사용함(비교적 쉽다) 성능이 애매함
JSON과 호환된다 -

서버외의 노드

노드의 사용범위가 늘어나면서 웹, 모바일 앱, 데스크톱 앱 개발에도 사용되기 시작했다.
대표적으로 리액트, 앵귤러, 뷰가 존재한다. 모바일 앱에는 리액트 네이티브가 있고 데스크톱 앱을 개발할땐 일렉트론을 사용한다.
웹 개발을 하면서 많이 사용하는 도구인 Atom, vs code도 일렉트론으로 만들어졌다.

NVM

노드를 사용하려면 먼저 Node.js를 공식홈페이지에서 다운로드를 받아서 사용하면 된다.
이렇게 되면 하나의 노드 버전을 사용해서 개발을 하는데, nvm을 설치해서 사용하게 되면 여러 버전의 노드를 사용할 수 있다.
(기존의 노드를 삭제하지 않고 약간의 명령어를 통해 사용하려는 노드버전을 설치하고 바꾼다.)

반응형
반응형

Utility types ?
유틸리티 타입은 전역으로 사용 가능하며, 공통 타입 변환을 용이하게 하기 위해 유틸리티 타입을 제공한다.

🔹 Partial

Partial<T> 프로퍼티 목록 중 선택해서 사용할 수 있는 유형을 반환한다.

//example
interface Todo {
    title: string;
    description: string;
}

// Partial Utility types
function updateTodo(obj: Partial<Todo>) {
    return obj;
}

const result = updateTodo({
    title: "title",
});

console.log(result); // { title: 'title' } 출력

🔹 Readonly

Readonly<T> 프로퍼티를 읽기 전용으로 설정하고 호출만 가능하다. (수정 불가)

//example
interface Todo {
    title: string;
}

const todo: Readonly<Todo> = {
    title: "Dlete inaxtive users",
};

todo.title = "Hello"; // (Error) readonly에서는 값을 변경할 수 없다.

🔹 Record

Record<K, T> 프로퍼티의 집합 K로 타입을 구성하며 K이외에 다른 타입으로 지정이 불가능하다.T는 프로퍼티를 다른 타입에 매핑시키는 데 사용된다.

//example
interface PageInfo {
    title: string;
}

type Page = "home" | "about" | "contact";

const x: Record<Page, PageInfo> = {
    about: { title: "about!" },
    contact: { title: "contact." },
    home: { subtitle: "home" }, // (Error) subtitle은 정의된 타입이 아니기 때문에 오류 발생
    main: { title: "main" }, // (Error) main은 K에 정의된 집합이 아니기 때문에 key값으로 사용 불가능
};

🔹 Pick

Pick<T, K> 프로퍼티 중 K의 집합을 선택해 원하는 타입만 지정할 수 있다.(Omit의 반대)

//example
interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

// Pick을 이용해 title만 프로퍼티로 지정
type TodoPreview = Pick<Todo, "title">;

const todo: TodoPreview = {
    title: "Clean room",
};

🔹 Omit

Omit<T, K> 모든 프로퍼티 중 특정 프로퍼티(K)만 제거 후 사용할 수 있다.(pick의 반대)

//example
interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

// Omit을 이용해 description 프로퍼티를 제외
type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
    title: "Clean room",
    completed: false,
};

🔹 Exclude

Exclude<T, U> T에서 U를 제외한 타입을 구성한다.(Extract의 반대)

//example
type T0 = Exclude<"a" | "b" | "c", "a">;
// "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// "c"

type T2 = Exclude<string | number | (() => void), Function>;
// string | number

🔹 Extract

Extract<T, U> T에서 U만 추출한다.(Exclude의 반대)

//example
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// "a"

type T1 = Extract<string | number | (() => void), Function>;
// () => void

🔹 NonNullabble

NonNullabble<T> 타입 중 null과 undefined를 제외한다.

//example
type T0 = NonNullabble<string | number | undefined>;
// string | number

type T1 = NonNullabble<string[] | null | undefined>;
// string[]

🔹 Parameters

Parameters<T> T의 값(매개변수)들이 튜플(타입이 정확히 지정된 배열의 타입) 타입으로 구성된다.

//example
declare function f1(arg: { a: number; b: string }): void;
type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [string]
type T2 = Parameters<<T>(arg: T) => T>; // [unknown]
type T3 = Parameters<typeof f1>; // [{a: number, b:string}]
type T4 = Parameters<any>; // unknown[]
type T5 = Parameters<never>; // never
// (Error) Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T6 = Parameters<string>;
// (Error) Type 'Function' does not satisfy the constraint '(...args: any) => any'.
type T7 = Parameters<Function>;

🔹 ConstructorParameters

ConstructorParameters<T> 생성자 함수 유형의 유형에서 튜플 또는 배열 유형을 생성합니다. 모든 매개변수 유형(또는 함수가 아닌 never경우 유형)이 포함된 튜플 유형을 생성합니다 . --> ts 공식문서 인용

//example
type T0 = ConstructorParameters<ErrorConstructor>; // [message?: string]
type T1 = ConstructorParameters<FunctionConstructor>; // string[]
type T2 = ConstructorParameters<RegExpConstructor>; // [pattern: string | RegExp, flags?: string]

interface Inew {
    new (arg: string): Function;
}
type T3 = ConstructorParameters<Inew>; // [string]

function f1(a: T3) {
    a[0], a[1]; // (Error) Tuple type '[arg: string]' of length '1' has no element at index '1'
}

🔹 ReturnType

ReturnType<T> 함수 T의 반환 타입으로 구성된 타입을 생성한다.

//example
declare function f1(): { a: number; b: string };
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<typeof f1>; // {a: number;b: string;}
type T5 = ReturnType<any>; // any
type T6 = ReturnType<never>; // never
// (Error) Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T7 = ReturnType<string>;
/* (Error) Type 'Function' does not satisfy the constraint '(...args: any) => any'. 
Type 'Function' provides no match for the signature '(...args: any): any'. */
type T8 = ReturnType<Function>;

🔹 Required

Required<T> 기존 정의된 프로퍼티가 옵셔널(?)로 필수값이 아니더라도 T의 모든 프로퍼티가 필수로 사용하도록 설정된다.

//example
interface Props {
    a?: number;
    b?: string;
}

const obj: Props = { a: 5 };

// (Error) Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.
const obj2: Required<Props> = { a: 5 };
반응형
반응형

지금까지 작성한 결과물을 동작을 확인해보고 회고록을 작성한다.
 

👀 결과물(캡쳐)

로그인 화면

로그인 페이지 동작

초기 로그인된 적이 없기 때문에 로그인 페이지가 렌더링 된다.

(recoil의 상태값 loginInfo로 체크)

이후 db에 저장된 정보로 로그인을 처리하고 서버쪽으로 응답을 받는다.

여기서 일치한다면 서버쪽에선 세션에 정보를 저장한다.

이후 정상적으로 응답을 받았다는 메시지를 통해 recoil쪽에 로그인된 유저라는 흔적용으로 로그인ID를 등록한다.

loginInfo에 값이 생겼기 때문에 MyInfo페이지로 렌더링된다.
 

마이페이지

변경된 MyPage 컴포넌트에선 useEffect에 의해 rest api로 현재 세션에 담긴 유저의 정보를 달라고 요청한다.

세션이 아직 살아있는 상태라면 세션에 등록된 id정보를 통해 유저 테이블에서 조회 후 적절한 데이터를 json으로 제공한다.

다만, React에서 recoil의 상태값은 새로고침하는 순간 날라간다.

이점을 임시적으로 처리하기 위해 로그인 페이지 useEffect에서 세션에 로그인된 정보가 있다면 recoil에 강제로 값을 처리하고 MyPage로 넘어가도록 처리해주었다.

강제로 새로고침을 많이 하게되면 서버와의 통신도 많아지고 효율적이지 못한 동작이 될 수 있다.
 

로그아웃

로그아웃을 요청하는 rest api가 처리되면 서버측에서 세션에 담긴 로그인 정보를 destory처리한다.

이후 응답에 따라 recoil상태값을 비워서 다시 로그인페이지로 렌더링 될 수 있게 하였다.
 

🤔 작업 후기

가볍게 노드서버도 익혀보고 리액트도 붙여서 동작을 해보려고 만만하게 보고 시작했는데, github에 소스도 올리고 블로그에 지금까지 사용된 라이브러리나 소스코드를 정리하다 보니 시간이 많이 걸렸다.

의외로 서버쪽 처리하는 부분을 설정하는게 시간을 많이 잡아먹었는데, 노드에 대해서 하나도 모르고 접근하다보니 시간 낭비하는 작업이 좀 많았던 것 같다.

일단 로그인 세션처리야 서버측에서 알아서 해주니 어느정도 작업을 마치고부턴 큰 문제가 없었는데, recoil로 유저의 정보를 담다보니 효율적이 못하다는 생각이 들었다. redux나 recoil에 localstorage를 붙여보는 방법 등등을 연구해보고 추가 글을 작성해보아야 할 듯 하다.
 

아래는 작업 해 본 내용을 정리한 글이다.

https://myhappyman.tistory.com/301

https://myhappyman.tistory.com/302

https://myhappyman.tistory.com/303
 

아래는 github에 지금까지 작성한 소스 부분을 올려놨다.

중간중간 가볍게 유효성검사나 로직등을 수정할 수도 있다.

https://github.com/myhappyman/NodeJsAndReactLoginModules

반응형