MancityBallboy 흔적 남기기

SSR과 CSR 그리고 SPA

SSR

전통적인 웹 어플리케이션 방식으로 요청시 마다 서버에서 처리하여 새로고침으로 페이지 응답

웹에서 기능과 데이터가 많아지면서 SPA가 생김

SSR은 처음 클라이언트가 접속했을때 브라우저에게 자바스크립트 코드를 다운받아 해설할때까지 기다리지 않고 서버에서 사용자에게 보여질 HTML을 모두 구성하여서 미리 준비해 사용자에게 페이지를 보여주는 방식이다.

사용자가 웹 페이지에 접속했을때 서버에서는 사용자에게 렌더링 될 HTML파일을 응답하여 사용자에게 웹페이지가 바로 렌더링 될 수 있도록 해준다.

그 후 브라우저는 자바스크립트 파일을 다운받아 해석하고 실행하는 절차를 가진다.

SSR을 사용하면 모든 데이터가 매핑된 서비스 페이지를 클라이언트에게 바로 보여줄 수 있다. 서버를 이용해서 페이지를 구성하기 때문에 클라이언트에서 구성하는 CSR보다는 페이지를 구성하는 속도는 다소 늦어질 수 있지만, 전체적으로 사용자에게 보여주는 콘텐츠 구성이 완료되는 시점은 빨라진다는 장점이 있다. 더불어 또한 SEO또한 쉽게 구성할 수 있다.

과정

  1. 서버가 렌더링될 준비가 된 HTML을 브라우저로 보낸다.
  2. 브라우저는 페이지를 렌더링하며, 페이지가 보이게 된다. 그리고 브라우저는 JS를 다운로드한다.
  3. 브라우저는 리액트를 실행한다.
  4. 페이지는 이제 작동가능하다.

SEO란?

검색자의 의도를 이해하고 이에 충실히 맞춰 웹 페이지의 콘텐츠를 제작하고, 이 페이지가 검색결과 페이지에서 잘 노출 되도록 웹 페이지의 태그와 링크 구조를 개선하여 자연 유입 트래픽을 늘리는 시책이라고 할 수 있습니다.

CSR

CSR은 자바스크립트 파일을 브라우저에서 해석해 렌더링 하는 방식이다.

클라이언트가 자바스크립트 파일을 브라우저에서 해석한 후 HTML을 렌더링한다.

반면 CSR은 SSr보다 초기 전송되는 페이지의 속도는 빠르지만 서비스에서 필요한 데이터를 클라이넡으에서 추가로 요청하여 재구성해야하기 때문에 전체적인 페이지 완료 시점은 SSR보다 느려진다.

과정

  1. 서버가 브라우저에 응답을 보낸다.
  2. 브라우저는 JS를 다운로드한다.
  3. 브라우저는 리액트를 실행한다.
  4. 페이지는 이제 보이고 작동가능하다.

SPA, MPA와 CSR, SSR과의 관계

SPA

서버로부터 처음에만 페이지를 받아오고 이후는 동적으로 페이지를 구성하여 새로운 페이지를 받아오지 않는 웹 애플리케이션을 의미

페이지가 한번 로딩된 이후 데이터를 수정하거나 조회할 때, 페이지가 새로고침 되지않고 다른 페이지로 넘어가지 않습니다.

즉 하나의 페이지에서 사람들에게 보이는 부분만 변경되는 원리입니다.

MPA

서버로부터 첫 페이지를 받아오고 이후에 데이터를 수정하거나 조회할대 다른 완전한 페이지로 이동합니다.

그렇다면 SPAMPACSR 그리고 SSR과 어떤 관계를 가지게 될까요?

SPA는 처음으로 페이지를 서버로 부터 받아온 후에 서버로 부터 받는 데이터를 통해 동적으로 DOM을 구성한 후에 렌더링 되는 화면으로 바뀌게 합니다. 즉, 서버로 부터 HTML을 받아오는 것이 아니라 브라우저 내에서 HTML구성 즉 DOM을 구성해야 한다는 의미입니다. CSR방식을 이용하게 됩니다.

반면에 MPA는 동적이지 않는 페이지를 상황에 맞게 클라이언트에 뿌려줘야하지 때문에 SSR방식을 채택하게 됩니다.

참고사이트

  • https://velog.io/@rhftnqls/CSR-SSR
  • https://poiemaweb.com/js-spa

Map과 Set

map과 set은 ES6에서 새로 도입한 데이터 구조이며 맵은 keyvalue를 연결한다는 점에서 객체와 유사하고 셋은 중복을 허용하지 않는 다는 점에서 배열과 비슷합니다.

Map

ES6이전에는 키와 값을 연결하려고 하면 객체를 이용해야 했습니다. 하지만 이렇게 한다면 많은 단점이 존재했습니다. 하지만 Map객체는 이러한 단점들을 모두 해결했고 키와 값을 연결하려는 목적이라면 객체보다 나은 선택입니다.

const u1 = { name: 'Cynthia'}
const u2 = { name: 'Jackson'}
const u2 = { name: 'Olive'}
const u2 = { name: 'James'}

const userRoles = new Map()

userRoles.set(u1, 'User')
userRoles.set(u2, 'User')
userRoles.set(u3, 'Admin')

// 이런식으로도 가능합니다.
userRoles
	.set(u1, 'User')
	.set(u1, 'User')
	.set(u1, 'Admin')

// 이와같은 형태도 가능합니다.
const userRoles = new Map([
    [u1, 'User'],
    [u2, 'User'],
    [u3, 'Admin'],
])

// u2의 역할을 가져오는 get 함수도 있습니다.
userRoles.get(u2) // 'User'

// u2가 역할이 있는지를 판단하는 has함수도 있습니다.
userRoles.has(u2) // true
userRoles.has(u4) // false

// userRoles의 크기를 반환하는 size 프로퍼티도 있습니다.
userRoles.size // 3

// 맵의 요소를 지우기 위해서는 delete를 이용하면됩니다.
userRoles.delete(u2)

for(let r of userRoles.values()) // 이와같이 values 프로퍼티를 이용하여 이터러블 객체를 만들 수 있습니다.

[...userRoles.values()] // 이와 같이 사용하면 ['User', 'User', 'Admin']와 같은 배열을 생성할수도 있습니다.

Set

셋은 중복을 허용치 않는 데이터 집합입니다. 이전 예제를 함께 다시 사용하면 관리자는 ‘User’와 ‘Admin’이라는 역할을 한꺼번에 가질 수 있지만 똑같은 역할을 여러번 가진다는 것은 상식적이지 않습니다. 셋은 이런 데이터 구조를 다루어야 할때 이상적입니다.

const roles = new Set()

// 역할을 추가할때는 add프로퍼티를 이용하면 됩니다.
roles.add("User")
roles.add("Admin")
roles.add("User")

// Map과 마찬가지로 size프로퍼티가 존재합니다.
roles.size // 2 똑같은 역할은 추가되지 않았기 때문에 size는 2 입니다.

// Map와 마찬가지로 delete 프로퍼티가 존재합니다.
roles.delete('Admin') // ["User"]

(leet code) max profit

DP

Best Time to Buy and Sell Stock with Transaction Fee

Medium

You are given an array prices where prices[i] is the price of a given stock on the ith day, and an integer fee representing a transaction fee.

Find the maximum profit you can achieve. You may complete as many transactions as you like, but you need to pay the transaction fee for each transaction.

Note: You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).

Example 1:

Input: prices = [1,3,2,8,4,9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by:
- Buying at prices[0] = 1
- Selling at prices[3] = 8
- Buying at prices[4] = 4
- Selling at prices[5] = 9
The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

간단히 설명하면 주식의 주가가 prices라는 배열로 표현되고 주식을 사고파는 행위를 통해 얻을 수 있는 최대의 이익을 도출해내는 문제입니다.

당연히, 주식은 사야만 팔 수 있습니다.

Solution

def maxProfit(prices, fee):
    profit = 0                ## 각 idx에서 얻을 수 있는 최대의 이익을 저장
    buyStock = -prices[0]     ## 주식을 사고 난 후의 버짓을 저장 최초의 지갑은 첫 주식을 산 이후의 버짓으로 저장
    for price in prices:
        profit = max(profit, buyStock + price - fee)   ## 특정한 주가에서 버짓과 주가를 더하고 수수료를 빼면 얻을 수 있는 이득이며 최대값을 
        											   ## 저장하여 가지고 간다.
        buyStock = max(buyStock, profit - price)       ## 현재 profit에서 현재 주가를 빼면 자기는 버짓이 나오며 버짓은 항상 최대값을 가질 수                                                        ## 있게 한다.
   	return profit

이 문제에서 잘 이해해야하는 부분은 버짓과 이익의 상호작용을 잘 이해해야한다.

  1. 주식을 사면 버짓이 정해진다.
  2. 버짓을 가지고 어떠한 시기에 팔면 이익이 정해진다.
  3. 이익을 가지고 어떤 주식을 또 산다.

이런 알고리즘을 반복하며 이익의 최댓값을 찾게 됩니다. 조금 더 상세하게 알고리즘을 설명하자면

  1. 주가마다 버짓의 최대값을 찾는다.

  2. 버짓의 최대값을 정하면 그 버짓을 통해 이익의 최대값을 찾는다.

  3. 이익을 봤지만 주식을 사지 않는다면 버짓은 주식을 팔지 않은 상태로 유지된다.

    어떤 시점에서 주식을 판걸로 이익을 산정하였지만 그 후에 팔지 않음으로써 더 큰 이득을 볼 수도 있습니다. 하지만 가지고 있는 이익보다 더 값싼 주식이 생긴다면 언제든지 사는 것이 이득일 것입니다. 왜냐하면 이미 주식 거래를 통해 이득을 봤고, 더 큰 잠재적인 이득을 기대할 수 있기 때문입니다.

비동기적 프로그래밍

비동기적 프로그래밍의 필요성은 여러가지의 이유로 필요합니다. 예를 들면 사용자의 입력과 같은 것들은 대부분이 비동기적 실행이 필요합니다. 하지만 자바스크립트에서는 자바 스크립트의 본성 때문에 더 필요하다고 볼 수 있습니다. 자바 스크립트 어플리케이션은 단일 스레드에서 작동합니다. 즉, 한번에 한가지의 동작만을 할 수 있다는 것을 의미합니다.

자바스크립트의 비동기적 프로그래밍에는 뚜렷한 구분이 있는 3가지의 페러다임이 있습니다.

  • 콜백
  • 프라미스
  • 제너레이터

사용자 입력 외에도 비동기적 처리를 할 경우가 몇 가지 있습니다.

  • Ajax 요청을 비롯한 네트워크 요청
  • 파일을 읽고 쓰는 등의 파일 시스템 작업
  • 의도적으로 시간 지연을 사용하는 기능

콜백

콜백을 간단히 설명하면 호출함수 입니다. 언제 끝날지 모르는 비동기적 실행이 끝났을때를 인지하고 그때 실행되는 함수를 콜백함수라고 합니다.

스코프와 비동기적 실행

함수를 실행하면 스코프도 함께 생성된다는 것은 다들 아는 사실일 것입니다. 비동기적 실행에서도 스코프 내 어디서 변수가 선언 되는냐에 따라 실행결과가 많이 달라집니다.

function countdown() {
    let i
    console.log("count down!!")
    for (i = 5; i >= 0; i--) {
        setTimeout(function () {
            console.log(i === 0 ? "go!" : i)
        }, (5 - i) * 1000)
        
    }
}

// count down!
// -1 
// -1 
// -1
// -1
// -1
// -1

우리가 원하는 것은 5로 시작하는 카운트 다운 후에 go가 출력되는 것입니다. 하지만 -1 만 6번 반복되서 출력될 뿐입니다. 이 이유에 대해서 먼저 얘기하면 for문이 실행 될때마다 같은 i를 공유하기 때문입니다. 사실상 비동기 함수인 setTimeout이 실행되는것은 for문이 끝난 후에 실행 되기때문에 그 때는 공유하고 있는 변수인 i 가 -1이 되었기 때문에 i가 6번 반복합니다.

이를 해결할 수 있는 방법은 iife와 반복문 내에 i를 선언하는 2가지의 방법이 있습니다.

function countdown() {
    console.log("count down!!")
    for (let i = 5; i >= 0; i--) {
        setTimeout(function () {
            console.log(i === 0 ? "go!" : i)
        }, (5 - i) * 1000)     
    }
}

// count down!!
// 5
// 4
// 3
// 2
// 1
// go!

function countdown() {
    let i
    for (i = 5; i >= 0; i--){
        (function (x) {
            setTimeout(function() {
                console.log(x === 0 ? "go" : x)
            }, (5-x)*1000)
        })(i)
    }
}      // IIFE로 처리하면 다음과 같이 처리 할 수있다. 하지만 ES6로 들어오면서 블록스코프가 생겼기때문에 IIFE의 활용도가 조금 떨어지긴합니다.

i 변수를 for문 내에 선언 함으로써 for문이 한번 반복 될때마다 다른 i를 가지기 때문에 원하는 결과를 얻을 수 있게 됩니다.

Union Find 알고리즘

union - find 알고리즘은 집합을 이용하여 문제를 풀 때 유용하게 이용할 수 있는 알고리즘 입니다.

예제 문제

https://www.acmicpc.net/problem/1717

집합의 표현

초기에 {0}, {1}, {2}, … {n} 이 각각 n+1개의 집합을 이루고 있다. 여기에 합집합 연산과, 두 원소가 같은 집합에 포함되어 있는지를 확인하는 연산을 수행하려고 한다.

집합을 표현하는 프로그램을 작성하시오.

입력

첫째 줄에 n(1 ≤ n ≤ 1,000,000), m(1 ≤ m ≤ 100,000)이 주어진다. m은 입력으로 주어지는 연산의 개수이다. 다음 m개의 줄에는 각각의 연산이 주어진다. 합집합은 0 a b의 형태로 입력이 주어진다. 이는 a가 포함되어 있는 집합과, b가 포함되어 있는 집합을 합친다는 의미이다. 두 원소가 같은 집합에 포함되어 있는지를 확인하는 연산은 1 a b의 형태로 입력이 주어진다. 이는 a와 b가 같은 집합에 포함되어 있는지를 확인하는 연산이다. a와 b는 n 이하의 자연수 또는 0이며 같을 수도 있다.

문제 풀이

def find(root, num):
    if root[num] == num:
        return num
    else:
        y = find(root, root[num])
        root[num] = y
        return root[num]

def union(root, num1, num2):
    if find(root, num1) == find(root, num2):
        return
    else:
        num1 = find(root, num1)
        num2 = find(root, num2)

        root[num1] = num2


def unionExpression():
    N, M = list(map(int, input().split()))
    inputArr = [list(map(int, input().split())) for _ in range(M)]

    root = [x for x in range(0, N + 1)]

    for operator, num1, num2 in inputArr:
        if num1 > num2:
            num1, num2 = num2, num1
        if operator == 1:
            if find(root, num1) == find(root, num2):
                print("YES")
            else:
                print("NO")
        elif operator == 0:
            union(root, num1, num2)

unionExpression()

root 배열에 자신의 부모를 저장하는 방식으로 집합을 만들어 나가는 방식입니다.

[0, 1, 2, 3, 4, 5, 6] 이라는 배열이 있을때

0 1 2 라는 input을 만나면 1, 2를 같은 집합으로 취급해라는 의미 입니다. 그럼

[0, 2, 2, 3, 4, 5, 6] 으로 root배열이 바뀌게 됩니다.

0 2 3을 만나면

[0, 2, 3, 3, 4, 5, 6]으로 root배열이 바뀌게 됩니다. 이로써 1 2 3 은 같은 집합에 속하게 되는 것입니다.

find함수는 원하는 숫자의 가장 부모를 찾아가는 함수입니다.

union함수는 두 수의 부모를 비교하여 부모가 다를때 같은 부모로 만들어서 같은 집합으로 인지 할 수 있게 합니다.

Union - Find 함수의 최적화

find 함수의 최적화

def find(root, num):
    if root[num] == num:
        return num
    else:
        y = find(root, root[num])
        root[num] = y
        return root[num]

find함수를 이용해서 부모를 찾을 때 지나가는 모든 노드의 값을 부모의 값으로 지정해 준다. 그럼으로써 다음에 더 빠르게 부모를 찾아 갈 수 있게 될것이다.

union 함수의 최적화

def union(num1, num2):
    if find(num1) == find(num2):
        return
    else:
        num1 = find(num1)
        num2 = find(num2)

        if rank[num1] > rank[num2]:
            root[num2] = num1
        else:
            root[num1] = num2
            if rank[num1] == rank[num2]:
                rank[num1] += 1

rank라는 배열을 만들어서 rank가 낮은 집합을 높은 집합에 붙히는 식으로 union함수를 최적화 할 수 있다.

이를 union-by-rank라고 칭한다.

위와 같이 rank를 이용하면 트리의 높이를 최소한으로 할 수 있으며 find함수의 재귀 깊이도 또 한 최소화 되기 때문에 실행 시간을 줄이는데에 많은 도움을 줄 수 있다.

참고 사이트

https://www.geeksforgeeks.org/union-find-algorithm-set-2-union-by-rank/

https://zoomkoding.github.io/algorithm/2019/05/19/Union-Find-2.html

https://ko.wikipedia.org/wiki/%EC%84%9C%EB%A1%9C%EC%86%8C_%EC%A7%91%ED%95%A9%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0

https://gmlwjd9405.github.io/2018/08/31/algorithm-union-find.html