no image
백준 1753 최단경로
유명한 문제. 우선순위 큐, 인접 리스트, 너비 우선 탐색, 다익스트라 등 공부한 것이 많은 문제였다. 그래프의 최단거리를 탐색하면 되는데 간선의 가중치가 존재한다. 다익스트라 과정 그림 못그림 위의 그림은 단방향 그래프이다 다익스트라는 거리에 대한 초기값을 무한대로 초기화 해놓는다. 1번을 탐색 시작 정점이라고 했을 때, 1번 정점을 방문처리를 해준다. 일반적인 bfs는 큐에 넣을 때 방문처리를 하는 경우가 많은데 다익스트라를 사용할 때는 큐에서 꺼낼 때 방문처리를 해줘야 한다(물론 visited 없이도 풀 수 있다) 기존 거리보다 짧은 최단거리가 나온다면 계속 거리를 갱신해줘야 하는데 큐에 넣을 때 방문 처리 해주면 갱신하지 못하는 경우가 발생한다. 1번에서 간선이 존재하는(자신 포함) 정점의 거리를..
2024.02.18
no image
백준 1074 Z C언어
#include int main(){ int a; scanf("%d", &a); int n = 2; for(int i = 1; i = 1){ if (y >= n/2 && x >= n/2) answer += n*n - (n/2)*(n/2); else if (y >= n/2 && x = n/2) answer += n*n - 3*(n/2)*(n/2); else if (y < n/2 && x < n/2) answer += n*n - 4*(n/2)*(n..
2024.02.18
no image
백준 숫자카드 2 java
6 3 2 10 10 10 -10 -10 7 3 10 9 -5 2 3 4 5 -10 위에는 가진 카드, 밑에는 랜덤한 정수 - 카드더미가 주어진다. 카드더미에 안에 카드 중 내가 몇장을 가지고 있는지 출력하는 문제이다. 위에 예시로 보았을 때 나는10카드 3장, 3과 -10이 2장 3한장을 가지고 있는 것이고 3 0 0 1 2 0 0 2 이렇게 출력이 되야 한다. lower bound = 하한선 = 정렬되어있는 배열에서 처음으로 key값 이상의 값이 나오는 index upper bound = 상한선 = key값이 초과한 값의 index upper bound - lower bound를 하면 중복된 값이 몇개 있는지 알 수 있다. 출처 : https://st-lab.tistory.com/267 만약 arra..
2024.02.18
no image
[C] 백준 9663번 N-Queen : 네이버 블로그
이전에 비슷한, 어쩌면 동일하다고도 할 수 있는 문제를 풀어본 적이 있어서 쉽게 접근할 수 있었다. 퀸을 N x N 체스판에서 서로 공격할 수 없게 배치하는 경우의 수를 출력하면 된다. 먼저 떠올렸던 것은 N x N이니까, 직관적으로 2차원 배열을 0으로 초기화 시킨 후 퀸이 있는 곳을 1로 바꾸는 방법이었는데, 좀 복잡하기도 하고, 해당 문제는 오히려 1차원 배열을 통한 풀이법이 정석처럼 여겨져서 1차원으로 했다. 2차원 배열처럼 직관적이진 않지만 확실히 간단해지긴 한다. 배열의 인덱스가 세로, 즉 체스판의 행으로 보고 배열의 값을 퀸이 있는 열로 보면 된다. 예를 들어 'arr[4] = 2' 라면 퀸이 4행의 2열에 있다는 의미이다. 그러므로 malloc을 통해 입력받은 N만큼 배열 크기를 할당해주면..
2024.02.18
no image
ORM JPA Hibernate Spring Data JPA
이전글 : https://sumjo.tistory.com/6 스프링 부트 Controller, DTO Servlet은 간단히 말하자면 CGI를 개선해서 동적 페이지를 생성해주기 위한 서버 프로그램, 자바 클래스이다. 추후에 다룰 예정이다. 클라이언트에서 요청은 Dispatcher Servlet이 먼저 처리한다. 프론 sumjo.tistory.com ORM(Object-Relational Mapping) 우리가 일반 적으로 알고 있는 애플리케이션 Class와 RDB(Relational DataBase)의 테이블을 매핑(연결)한다는 뜻이며, 기술적으로는 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 것이라고 보면된다. 라고 한다. 간단히 말하자면 객체(클래스)와 데이터베이스를 맵핑시켜준다는 뜻..
2024.01.30
no image
Zustand 상태 관리 라이브러리
리액트에는 상태(state) 라는 개념이 있다. 상태에 대해서는 나중에 다룰 예정인데, 일단은 state는 컴포넌트 내부에서 변경되는 값을 다루는 객체이자, 변화하면 리랜더링 되고 컴포넌트에 반영되는 변수 정도로 이해해두자. 리액트는 state 값을 추적하고 있다. state의 변화를 감지해서 리랜더링을 진행한다. 추후에 서술하겠지만, 그냥 state 를 변경한다고 해서 변화를 감지하는 것이 아니고, setState함수를 통해 주소값이 변경돼야 상태가 변한 것으로 인지한다. 나중에 상태 관련해서 글 쓸 예정 그리고 한 컴포넌트가 아니라 전역적으로 관리해야 하는 상태를 위해 위해 상태 관리 라이브러리를 사용한다. useContext()도 있지만 불필요한 리랜더링을 야기할 수 있다. 이러한 상태 관리를 위해..
2024.01.29
no image
React Router, Outlet
과거의 웹사이트는 페이지가 바뀔때마다 새로운 페이지를 로드해야 했다. 직관적으로 생각해봐도 사용자 경험을 저하시킬 수 밖에 없다. 싱글 페이지 애플리케이션(spa)는 이를 개선하기 위해 만들어졌다. 전체 페이지를 갱신하는 것이 아니라 필요한 부분만 랜더링하여 갱신해준다. 라우터는 사용자가 입력한 URL에 따라 필요한 데이터를 보여주는 역할을 한다. 근데, 앞서 말했듯이 경로마다 새로운 페이지를 가져오는 것이 사용자 경험을 저하시킨다면, 싱글 페이지 애플리케이션의 특성을 살려서, 모든 요소를 랜더링으로 바꾸면 라우터를 쓸 필요도 없지 않나? 맞는 말이지만, 그러면 URL이 하나이기 새로고침 하면 초기 페이지로 이동하고, 뒤로가기도 사용하기 힘들다. 때문에 (프로젝트 관리와 유지를 위해서라도) 경로를 나누어..
2024.01.29
[블로그 프로젝트] forwardRef를 사용해서 Input 컴포넌트 만들기
함수형 컴포넌트는 레퍼런스가 없기 때문에 ref를 사용할 수 없다. 때문에 함수형 컴포넌트에 ref를 통해 데이터를 전달하려면 forwardRef() 를 사용해야 한다. const InputBox = forwardRef((props: Props, ref) => { // state:properties // const { label, type, value, placeholder, error, icon, message} = props; const { onChange , onButtonClick, onKeyDown} = props; // event handler: 키 이벤트 처리 함수 // const onKeyDownHandler = (event: KeyboardEvent) => { if(!onKeyDown) ..
2024.01.27
유명한 문제.
우선순위 큐, 인접 리스트, 너비 우선 탐색, 다익스트라 등 공부한 것이 많은 문제였다.
그래프의 최단거리를 탐색하면 되는데 간선의 가중치가 존재한다.
다익스트라 과정
그림 못그림
위의 그림은 단방향 그래프이다
다익스트라는 거리에 대한 초기값을 무한대로 초기화 해놓는다.
1번을 탐색 시작 정점이라고 했을 때, 1번 정점을 방문처리를 해준다.
일반적인 bfs는 큐에 넣을 때 방문처리를 하는 경우가 많은데 다익스트라를 사용할 때는 큐에서 꺼낼 때 방문처리를 해줘야 한다(물론 visited 없이도 풀 수 있다)
기존 거리보다 짧은 최단거리가 나온다면 계속 거리를 갱신해줘야 하는데 큐에 넣을 때 방문 처리 해주면 갱신하지 못하는 경우가 발생한다.
1번에서 간선이 존재하는(자신 포함) 정점의 거리를 초기화 해주면서 우선순위 queue에 넣어줘서 다음 탐색에 방문할 후보로 넣어준다.
우선순위 큐는 node클래스의 거리를 비교하여 더 작은 거리를 가진 node가 나올 수 있게 했다.
방문노드 1
 
정점
1
2
3
4
5
거리
0
1
3
INF
INF
 
1에서 갈 수 있는 정점은 2와 3이고 그 둘의 거리가 1, 3으로 바꼈다. 거리 갱신은 현재 노드 + 간선
그리고 다음 탐색 노드를 정하는데, 탐색 시점 기준 거리가 가장 짧은 노드를 선택한다.
표를 기준으로는 2번 노드가 거리 1로 가장 짧기 때문에 2를 방문노드로 선정한다.
우선순위 큐를 사용한 이유인데, 그냥 꺼내면 거리가 가장 짧은 2번 node가 나오게 된다.
방문 노드 2
 
정점
1
2
3
4
5
거리
0
1
2
INF
5
 
2번 노드를 방문하고 앞선 과정을 반복해본다.
2번 노드를 현재 노드로 설정하고 방문처리 해준다.
2번에서 갈 수 있는 노드중에서 방문하지 않은 노드에 대해서 최소값을 비교해본다.
1은 2에서 갈 수 있지만 이미 방문했으니 초기화 되지 않는다.
1 -> 3으로 연결된 3의 비용을 지닌 간선보다 1->2의 거리인 1과 2->3의 거리인 1을 더한 2의 비용을 가진 루트가 더 짧은 것을 알게 된다.
그리고 2->4가 가능하니 4번 노드에 대한 거리가 2번노드 거리 1과 5번 노드 거리 4를 더한 5로 초기화 된다.
거리 배열은 아래와 같이 갱신된다.
방문 가능한 모든 노드를 큐에 넣어준다.
3번 노드도 다시 한번 큐에 들어가는데, 갱신된 거리와 함께 들어간다.
그러므로 지금 큐에 있는 노드를 (노드, 거리)로 작성하면
(3,2) (3, 3) (5, 5) 가 들어가 있을 것이다.
우선순위 큐를 통하면 2의 거리를 가진 3번 노드가 다음 방문 노드로 선정될것이다.
3번 노드를 방문 처리 해주고 탐색을 진행한다.
방문노드 3
 
정점
1
2
3
4
5
거리
0
1
2
6
3
 
같은 과정이니 짧게 설명하자면,
3에서 5번 노드로 가는 길이 이전 거리보다 짧기 때문에 갱신이 될 것이고, 4번 노드의 거리가 첫번째로 갱신이 된다.
큐에는 4번 5번 노드가 들어간다.
큐에는 이전에 들어가있던 (3, 3) (4, 5)와
(4, 6) (5, 3) 이 들어가서
(3, 3), (5, 3), (4, 5) (4, 6)이 들어가있다.
큐에서 값을 꺼내면 3의 거리를 가진 3번노드가 나올텐데 3번 노드는 이미 방문처리가 되어 있으므로 방문하지 않고
반복문을 통해 다시 값을 꺼내면 5번 노드가 나오게 된다.
5번노드는 방문할 수 있는 노드가 없으므로 다시 위의 과정을 반복하여 4번 노드가 나오고
4번 노드도 5번과 동일하게 조건문을 타지 않아서 결국 q.isEmpty = true가 되어 탐색이 끝나게 된다.
최종
 
정점
1
2
3
4
5
거리
0
1
2
6
3
 
내가 계속 실수했던 부분은, 갱신된 거리값을 가진 노드 객체를 큐에 넣어줘야 하는데, 간선값만을 지닌 객체를 넣어준 것이다.
그래서 오래걸렸다.

 

import java.util.*;
import java.io.*;


public class Main {
    static int nodeNum;
    static int[] distance;
    public static void main(String[] args) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        nodeNum = Integer.parseInt(st.nextToken());
        int n = Integer.parseInt(st.nextToken());

        int startNode = Integer.parseInt(br.readLine());

        PriorityQueue<Node> q = new PriorityQueue<>();
        ArrayList<ArrayList<Node>> edge = new ArrayList<>();
        boolean[] visited = new boolean [nodeNum+1];
        distance = new int[nodeNum+1];

        for(int i = 1; i <= nodeNum; i++)
            distance[i] = Integer.MAX_VALUE;
        for(int i = 0; i <= nodeNum; i++)
            edge.add(new ArrayList<>());

        for(int i = 0; i < n; i++){
            st = new StringTokenizer(br.readLine());
            int w = Integer.parseInt(st.nextToken());
            int d = Integer.parseInt(st.nextToken());
            int v = Integer.parseInt(st.nextToken());
            edge.get(w).add(new Node(d, v));
        }
        q.add(new Node(startNode, 0));
        distance[startNode] = 0;
        Dijkstra(edge, distance, visited, q);
        for(int i = 1; i <= nodeNum; i++){
            if(!visited[i])
                System.out.println("INF");
            else
                System.out.println(distance[i]);
        }
    }

static void Dijkstra(ArrayList<ArrayList<Node>> edge, int[]distance, boolean[] visited, PriorityQueue<Node> q){

        while(!q.isEmpty()){        
            int node = q.poll().vertex;
            if(!visited[node]){
                visited[node] = true;
                for(int j = 1; j <= edge.get(node).size(); j++){
                    if(distance[node] + edge.get(node).get(j-1).value < distance[edge.get(node).get(j-1).vertex])
                        distance[edge.get(node).get(j-1).vertex] = distance[node] + edge.get(node).get(j-1).value;
                    q.add(new Node(edge.get(node).get(j-1).vertex, distance[edge.get(node).get(j-1).vertex]));
                }
            }
        }
    }
    
}

class Node implements Comparable<Node> {
    int vertex;
    int value;

    public Node(int d, int v) {
        this.vertex = d;
        this.value = v;
    }

    @Override
    public int compareTo(Node p) {
        return this.value - p.value;
    }
}
 

 

#include <stdio.h>

int main(){

    int a;
    scanf("%d", &a);

    int n = 2;

    for(int i = 1; i < a; i++)
        n *= 2;

    int y, x;
    scanf("%d", &y);
    scanf("%d", &x);

    int answer = 0;
    while(n >= 1){
        if (y >= n/2 && x >= n/2)
            answer += n*n - (n/2)*(n/2);
        else if (y >= n/2 && x < n/2)
            answer += n*n - 2*(n/2)*(n/2);
        else if (y < n/2 && x >= n/2)
            answer += n*n - 3*(n/2)*(n/2);
        else if (y < n/2 && x < n/2)
            answer += n*n - 4*(n/2)*(n/2);
        if (n == 1)
            break;
        y = y % (n/2);
        x = x % (n/2);
        n /= 2;
    }
    printf("%d", answer - 1);


}
간만에 C로 풀었다.
이거 어렵다.
발상이 정말 어렵다. 5시간 걸렸다.
핵심은 '분할'이다. 가장 큰 사각형부터 2X2 사각형까지 Z자로 연산이 계속된다는 점을 파악한 후,
사각형을 계속 4등분 하면서 적절한 연산을 해주면 되는데,
2차원 배열의 인덱스가 n/2의 값을 가지는 4등분 된 사각형 안에서 다시 어떤 인덱스를 가지는 지 생각하는 게 정말 어려웠다.
해답은 나머지 연산.
좌표에 대해서 n으로 나머지 연산을 시행하면
사각형을 분할했을 때 그 사각형을 기준으로 상대적인 좌표의 위치를 알 수 있다.
 
 
 
6 3 2 10 10 10 -10 -10 7 3
 
10 9 -5 2 3 4 5 -10
위에는 가진 카드, 밑에는 랜덤한 정수 - 카드더미가 주어진다.
카드더미에 안에 카드 중 내가 몇장을 가지고 있는지 출력하는 문제이다.
위에 예시로 보았을 때 나는10카드 3장, 3과 -10이 2장 3한장을 가지고 있는 것이고
3 0 0 1 2 0 0 2
이렇게 출력이 되야 한다.
lower bound = 하한선 = 정렬되어있는 배열에서 처음으로 key값 이상의 값이 나오는 index
upper bound = 상한선 = key값이 초과한 값의 index
upper bound - lower bound를 하면 중복된 값이 몇개 있는지 알 수 있다.
 
만약 array에 없는 값, 사진 기준으로 5라고 가정해보면
lower bound = 5
upper bound = 5
가 나오므로 둘의 차는 0이 되어 없다는 판정을 할 수 있다.
라고 했는데 이후에 무려 2시간을 더 써서 풀었다
추후 서술
이전에 비슷한, 어쩌면 동일하다고도 할 수 있는 문제를 풀어본 적이 있어서 쉽게 접근할 수 있었다.
퀸을 N x N 체스판에서 서로 공격할 수 없게 배치하는 경우의 수를 출력하면 된다.
먼저 떠올렸던 것은 N x N이니까, 직관적으로 2차원 배열을 0으로 초기화 시킨 후 퀸이 있는 곳을 1로 바꾸는 방법이었는데, 좀 복잡하기도 하고, 해당 문제는 오히려 1차원 배열을 통한 풀이법이 정석처럼 여겨져서 1차원으로 했다.
2차원 배열처럼 직관적이진 않지만 확실히 간단해지긴 한다. 배열의 인덱스가 세로, 즉 체스판의 행으로 보고 배열의 값을 퀸이 있는 열로 보면 된다. 예를 들어 'arr[4] = 2' 라면 퀸이 4행의 2열에 있다는 의미이다. 그러므로 malloc을 통해 입력받은 N만큼 배열 크기를 할당해주면 N x N의 정보를 모두 담을 수 있는 1차원 배열을 만들 수 있는 것이다.
문제 핵심은 백트래킹이다. 나는 아직 개념이 충분하진 않지만 해당 문제가 백트래킹의 대표적인 예제라고 한다. 내 이해를 말하자면 백트래킹은 모든 경우의 수를 탐색하여 답을 찾는 것이다. 조건에 따라 후보군을 탐색하며 유망(promising)한지 검사하고, 유망하다면 하위 깊이에서 또 탐색을 하고, 그렇지 않다면 더 하위로 가지 않고 가지치기(Pruning)를 하고 뒤로 돌아가서(Backtrack) 다음 후보군을 탐색한다. 그 과정에서 구조는 재귀를 활용하는데, 목표 깊이에 도달했을 때(배열이 다 채워졌을 때) 카운트를 ++해준 뒤 탈출조건을 설정해놓고 재귀를 돌리면 된다.
이제 서로 공격하지 못하게 배치하려면 퀸이 공격할 수 있는 루트를 생각해봐야 한다. 퀸은 가로와 세로, 대각선은 모두 움직일 수 있다. 때문에, 체스판을 2차원 배열로 보면 같은 열과 행은 당연히 위치할 수 없고, 대각선에 위치해서도 안된다.
1차원 배열에서는 인덱스 = 행 값 = 열 이기 때문에 일단 인덱스를 증가시키면서 반복문을 돌면 같은 행에는 위치할 수 없다. 그렇다면 같은 열에 있을때는 유망하지 않다는 조건을 써야하는데, 반복문을 돌면서 이전 1차원 배열에 저장되어 있는 값들과 비교하여 같은 값이 있다면 조건문을 false로 설정하면 된다.
대각선 조건은 수학적으로 조금 생각해보면 쉽게 풀린다. 정사각형 판에서 대각선의 의미는, 두 퀸을 직선으로 연결한 직선 즉, 1차원 그래프의 기울기가 1 (좌즉 대각선은 -1) 임을 의미한다. 때문에, y증가량 / x증가량 == 1, y증가량 == x증가량 이라면 대각선에 위치한다고 해석할 수 있는 것이다. 좌측 우측 모두 편하게 계산하기 위해 절대값을 씌운 후 동일한 지 조건문을 써줬다.
유망한 지 판별하는 조건
int is_Queen_valid(int nd, int i, int *arr) { 
int j = 0; 
while (j < nd) { 
	if (i == arr[j]) 
		return 0; 
	if(abs(nd - j) == abs(i - arr[j])) 
		return 0; j++; } return 1; 
}
소스코드
#include <stdio.h>
#include <stdlib.h>

int is_Queen_valid(int nd, int i, int *arr);
int abs(int a);
void queen(int *arr, int now_depth, int final_depth, int *cnt);


int abs(int a)
{
    if (a < 0)
        return (-1 * a);
    else
        return (a);
}

int is_Queen_valid(int nd, int i, int *arr)
{
    int j = 0;
        while (j < nd)
        {
            if (i == arr[j])
                return 0;
            if(abs(nd - j) == abs(i - arr[j]))
                return 0;
            j++;
        }
    return 1;
}

void queen(int *arr, int now_depth, int final_depth, int *cnt)
{
    int i = 0;
    int size = final_depth;
    if (now_depth == final_depth)
        {
            *cnt += 1;
            return ;
        }
    
    while (i < size)
        {
            if (is_Queen_valid(now_depth, i, arr))
            {
                arr[now_depth] = i;
                queen(arr, now_depth + 1, final_depth, cnt);
            }
        i++;
        }
}

int main()
{
    int a;
    scanf("%d", &a);
    int *arr = malloc(sizeof(int) * a);
    int cnt = 0;
    queen(arr, 0, a, &cnt);
    printf("%d", cnt);
    free(arr);
}
queen에서 파라미터로 받은 함수만 잘 살펴보면 된다.
arr = 체스판을 형상화한 1차원 배열
now_depth = 현재 탐색 깊이
final_depth = 목표 탐색 깊이
cnt = 방법의 수

'알고리즘-문제' 카테고리의 다른 글

구름톤 챌린지 4주 차 문제 17 학습 일기  (0) 2024.02.18
백준 1436 영화감독 숌  (0) 2024.02.18
백준 1753 최단경로  (0) 2024.02.18
백준 1074 Z C언어  (0) 2024.02.18
백준 숫자카드 2 java  (0) 2024.02.18

이전글 : https://sumjo.tistory.com/6

 

스프링 부트 Controller, DTO

Servlet은 간단히 말하자면 CGI를 개선해서 동적 페이지를 생성해주기 위한 서버 프로그램, 자바 클래스이다. 추후에 다룰 예정이다. 클라이언트에서 요청은 Dispatcher Servlet이 먼저 처리한다. 프론

sumjo.tistory.com

 

ORM(Object-Relational Mapping)

우리가 일반 적으로 알고 있는 애플리케이션 Class와 RDB(Relational DataBase)의 테이블을 매핑(연결)한다는 뜻이며, 기술적으로는 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 것이라고 보면된다.

라고 한다.

간단히 말하자면 객체(클래스)와 데이터베이스를 맵핑시켜준다는 뜻이다. 예시를 들어보겠다.

데이터베이스를 맵핑시켜주기 위해서는 데이터 베이스 칼럼과 대응되는 Entity 객체가 필요하다.

@Entity(name="user")
@Table(name="user")
public class UserEntity {
        @Id
        private String email;
        private String password;
        private String nickname;
        private String telNumber;
        private String address;
        private String addressDetail;
        private String profileImage;
        private boolean agreedPersonal;
    }

user table과 대응되는 Entity 객체의 일부이다.

데이터베이스의 칼럼을 다 가지고 있는 객체이다. 즉 한 테이블에 대응되는 객체인 셈이다.

변수명을 바탕으로 대응시키는데, telNumber -> tel_number로 변환돼서 대응이 되기 때문에 데이터베이스 칼럼명을 옳바르게 작성해야 한다.

 

JPA

기본적으로 ORM 표준 명세이다.

JPA는 라이브러리가 아니고 관계형 데이터베이스의 사용 방식을 정의한 인터페이스 모음이다.

때문에 실제로 구현한 구현체가 필요하고 Spring에서 Hibernate가 JPA구현체의 표준처럼 사용된다.

 

Hibernate

JPA가 인터페이스, Hibernate가 인터페이스를 구현한 Class 의 관계라고 볼 수 있겠다.

위에 말했듯이 JPA구현체의 표준처럼 사용되고 있다.

 

Spring Data Jpa

공식 문서에 따르면 JPA 기반 API를 더 사용하기 쉽게 만들어주는 기술이라고 나와있다.

즉 Spring Data JPA 또한 인터페이스 이고, JPA 사용을 더 쉽게 한번 더 감싸 준 것이다. Repository를 제공하여 데이터베이스 접근을 편하게 만들어준다.

예시를 보자.

 

 

 

데이터베이스 접근 순서도이다.

Spring Data JPA 에서 제공하는 Repository를 통해 CRUD메소드를 호출하면

메소드를 구현한 Hibernate가 데이터베이스와 통신한다.

JDBC는 DB CONNECTION과 같은 기본적인 API를 제공하는 인터페이스다.

 

 

 

리액트에는 상태(state) 라는 개념이 있다.

상태에 대해서는 나중에 다룰 예정인데,

일단은 state는 컴포넌트 내부에서 변경되는 값을 다루는 객체이자, 변화하면 리랜더링 되고 컴포넌트에 반영되는 변수 정도로 이해해두자.

리액트는 state 값을 추적하고 있다. state의 변화를 감지해서 리랜더링을 진행한다.

추후에 서술하겠지만, 그냥 state 를 변경한다고 해서 변화를 감지하는 것이 아니고, setState함수를 통해 주소값이 변경돼야 상태가 변한 것으로 인지한다. 나중에 상태 관련해서 글 쓸 예정

그리고 한 컴포넌트가 아니라 전역적으로 관리해야 하는 상태를 위해 위해 상태 관리 라이브러리를 사용한다.

useContext()도 있지만 불필요한 리랜더링을 야기할 수 있다.

 

이러한 상태 관리를 위해 여러가지 라이브러리가 있는데, Redux, Recoil 등이 유명한 것으로 안다.

내가 써볼 라이브러리는 Zustand 이다.

사용은 어렵지 않다.

npm install zustand

받아준다 늘 그렇듯이.

 

참고 : https://github.com/pmndrs/zustand

 

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.

github.com

zustand 예시 및 설명이 잘 돼있다.

ts 사용 예시이다.

import { User } from "types/interface";
import { create } from "zustand";

interface LoginUserStore {
	loginUser: User | null;
	setLoginUser: (loginUser: User) => void;
	resetLoginUser: () => void;
};

const useLoginUserStore = create<LoginUserStore>((set) => ({
	loginUser: null,
	setLoginUser: (loginUser) => set(state => ({...state, loginUser })),
	resetLoginUser: () => set(state => ({...state, loginUser: null})),
}));

export default useLoginUserStore;

creat 를 통해 store 를 만들어 준다.

내부적으로 set 함수를 통해 상태를 변경해준다.

state를{...state}로 넣어준 이유는

요약하자면

리액트와 똑같이 state는 불변으로 유지해야 한다.

즉 초기에 할당한 값 자체를 바꾸면 안되고, 변경을 위해서 항상 새로운 할당을 진행한다.

immutable 유지를 위해 전개 연산자를 통해 state를 복사해준 뒤 상태를 변경한다.

하지만 밑에까지 보면 상태를 병합해주기 때문에 skip할 수 있다는 것도 쓰여있는데, 자세히는 모르겠다.

 

import { useLoginUserStore } from 'stores'

const  { setLoginUser, resetLoginUser } = useLoginUserStore();

const { loginUser } = useLoginUserStore();

 

이렇게 import 해온 뒤

다른 파일에서 loginUser의 상태를 참조하거나,

set함수를 통해 상태를 변경할 수 있다.

'React' 카테고리의 다른 글

state, useState  (0) 2024.03.06
React Router, Outlet  (0) 2024.01.29
[블로그 프로젝트] forwardRef를 사용해서 Input 컴포넌트 만들기  (0) 2024.01.27
리액트 Ref, state와의 차이  (0) 2024.01.27
[블로그 프로젝트] DTO 정의  (0) 2024.01.14

React Router, Outlet

sumjo
|2024. 1. 29. 08:13

과거의 웹사이트는 페이지가 바뀔때마다 새로운 페이지를 로드해야 했다.

직관적으로 생각해봐도 사용자 경험을 저하시킬 수 밖에 없다.

싱글 페이지 애플리케이션(spa)는 이를 개선하기 위해 만들어졌다.

전체 페이지를 갱신하는 것이 아니라 필요한 부분만 랜더링하여 갱신해준다.

 

라우터는 사용자가 입력한 URL에 따라 필요한 데이터를 보여주는 역할을 한다.

근데, 앞서 말했듯이 경로마다 새로운 페이지를 가져오는 것이 사용자 경험을 저하시킨다면,

싱글 페이지 애플리케이션의 특성을 살려서, 모든 요소를 랜더링으로 바꾸면 라우터를 쓸 필요도 없지 않나?

맞는 말이지만, 그러면 URL이 하나이기 새로고침 하면 초기 페이지로 이동하고, 뒤로가기도 사용하기 힘들다.

때문에 (프로젝트 관리와 유지를 위해서라도) 경로를 나누어 웹페이지를 구성할 필요가 있다.

그리고 이를 라우팅 이라고 한다. 

덧붙이자면 우리가 사용할 라우터 또한 사실 페이지 경로에 따라 필요한 컴포넌트만 랜더링 해서 보여주는 것이다.

 

리액트는 기본적으로 html파일이 하나만 있는 SPA 라이브러리이고 내장된 route 기능이 없다.

그래서 라우팅을 위해서는 다른 라이브러리가 필요한데, react-router-DOM 라이브러리를 사용해볼것이다.

 npm install react-router-dom

react-router-dom 을 설치해준다.

 

먼저 최상단을 <BrowserRouter> 태그로 감싸주어야 한다.

index.tsx

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

 

 

App.tsx

import { Route, Routes } from 'react-router-dom';

return (
    <Routes>
      <Route element={<Container />}>
        <Route path={MAIN_PATH()} element={<Main />} / >
        <Route path={AUTH_PATH()} element={<Authentication />} / >
        <Route path={SEARCH_PATH(':searchWord')} element={<Search />} / >
        <Route path={USER_PATH(':userEmail')} element={<UserP />} / >
        <Route path={BOARD_PATH()}>
          <Route path={BOARD_WRITE_PATH()} element={<BoardWrite/>}/>
          <Route path={BOARD_DETAIL_PATH(':boardNumber')} element={<BoardDetail/>}/>
          <Route path={BOARD_UPDATE_PATH('boardNumber')} element={<BoardUpdate/>} />
        </Route> 
      <Route path="*" element={<h1>Not Found</h1>} />
      </Route>
    </Routes>
  );
}

<Routes> 태그는 모든 <Route>의 상위 경로에 존재해야 한다.

<Route> 태그의 인자

path에는 경로를 전달하고 element로는 경로에 따라 랜더링 해줄 엘리멘트(화면)을 전달한다.

경로를 변수로 바꿔놓은 상태라 알아보기 힘들 수 있는데,

예시를 들자면

MAIN_PATH = http://localhost:3000/

BOARD_PATH() = http://localhost:3000/board

BOARD_WRITE_PATH(:boardNumber) = http://localhost:3000/:boardNumber <- 쿼리스트링

 

이런식으로 경로에 따른 화면을 랜더링 해준다.

 

Outlet

Outlet 태그는 부모 엘리먼트에서 사용해 자식 엘리먼트를 랜더링 하고 싶을 때 사용한다.

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* This element will render either <DashboardMessages> when the URL is
          "/messages", <DashboardTasks> at "/tasks", or null if it is "/"
      */}
      <Outlet />
    </div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route
          path="messages"
          element={<DashboardMessages />}
        />
        <Route path="tasks" element={<DashboardTasks />} />
      </Route>
    </Routes>
  );
}

App에 DashboardMessages와 DashboardTasks를 <Route path="/" element={<Dashboard>} 로 감싸고 있는 것을 볼 수있다.

Dashboard를 보면 div태그 안에 <Outlet /> 을 확인할 수 있다.

해당 요소는 URL이 "/messages"일 때 <DashboardMessages>를 렌더링하고, "/tasks"일 때는 <DashboardTasks>를 렌더링합니다. 만약 URL이 "/"일 경우에는 null을 렌더링합니다.

설명처럼, Outlet 태그 자리에 자식 엘리먼트가 랜더링이 된다고 알아두면 될 듯 하다. 이를 활용해서 Header와 Footer를 랜더링 할 것이다.

 

간단한 원리

깊게 공부한 건 아니지만 뇌피셜을 곁들인 원리를 써보자면

 

1. BrowserRouter는 HistoryAPI를 사용해서 사용자가 방문한 모든 경로를 객체로 만들어 하위 <Routes>에 전달한다.

 

2. HistoryAPI는 stack구조이기 때문에 경로가 바뀌면 stack의 최상단이 변경되면서 <Routes>가 참조하던 객체도 새로운 

것으로 변경된다.

 

3. 이로 인해 Routes 컴포넌트가 리랜더링 되고 하위 라우팅 관련 컴포넌트들도 리랜더링 된다.

 

 

 

함수형 컴포넌트는 레퍼런스가 없기 때문에 ref를 사용할 수 없다.

때문에 함수형 컴포넌트에 ref를 통해 데이터를 전달하려면 forwardRef() 를 사용해야 한다.

 

const InputBox = forwardRef<HTMLInputElement, Props>((props: Props, ref) => {

	//         state:properties 			//
	const { label, type, value,  placeholder, error, icon, message} = props;
	const { onChange , onButtonClick, onKeyDown} = props;

	//  event handler: 키 이벤트 처리 함수 			//
	const onKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
		if(!onKeyDown) return;
		onKeyDown(event);
	}

	//       render: Input Box 컴포넌트 			//
	return (
		<div className='inputbox'>
			<div className='inputbox-label'>{label}</div>
			<div className={error ? 'inputbox-container-error' : 'inputbox-container'}>
				<input  ref={ref} type = {type} className='input' placeholder={placeholder} value={value} onChange={onChange} onKeyDown={onKeyDownHandler}/>
				{onButtonClick !== undefined && (
				<div className='icon-button' onClick={onButtonClick}>
					{icon !== undefined && (
					<div className={`icon ${icon}`}></div>
					)}
				</div>
				)}
			</div>
			{message !== undefined && <div className='inputbox-message'>{message}</div>}
		</div>
	)
})

export default InputBox

 

위와 같이 forwardRef를 통해 인자로 props와 함께 ref를 받아서 처리할 수 있다.

props의 각 변수와 함수를 input 요소에 맵핑 시켜준다.

onKeyDownHandler를 통해 keyEvent 처리 함수도 만들어준다.

 

//				event handler: 이메일 인풋 키 다운 이벤트 처리				//
const onEmailKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
	if (event.key !== 'Enter') return;
	if (!passwordRef.current) return;
	passwordRef.current.focus();
}

//				event handler: 패스워드 키 다운 이벤트 처리				//
const onPassworKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
	if (event.key !== 'Enter') return;
	onSignInButtonClickHandler();
}



return (

..
..

</div>
	<InputBox ref={emailRef} label = '이메일주소' type = 'text' placeholder='이메일 주소를 입력해주세요.' error={error} value={email} onChange={onEmailChangeHandler} onKeyDown={onEmailKeyDownHandler}/>
	<InputBox ref={passwordRef} label = '패스워드' type = {passwordType} placeholder='비밀번호를 입력해주세요.' error={error} value={password} onChange={onPasswordChangeHandler} icon={passwordButtonIcon} onButtonClick={onPasswordButtonClickHandler} onKeyDown={onPassworKeyDownHandler}/>
</div>
)

 

로그인 카드 컴포넌트의 일부이다.

InputBox에 props와 함께 ref를 전달하고 이메일 입력에서 keyDown(Enter)이벤트 발생 시 비밀번호 입력창으로 포커스를 옮겨주고

비밀번호 입력후 다시 Enter를 누를 시 로그인 버튼 클릭 이벤트 함수를 호출해서 로그인 동작이 일어난다.