728x90

단순하게 Spring Boot Security 5에 대해 코드만 작성하는 글이 아니라, 이해를 위한 글을 작성하기 노력하고 있습니다.

Oauth2.0을 알아보기 전에 기본인  HTTP Basic 로그인에 대해 알아보겠습니다.

1. 종속성 추가하기

먼저 종속성을 추가해줍니다. 기존 프로젝트에 시큐리티만 추가하면 됩니다.
lombok은 코드를 간편하게 쓰기 위해 추가했습니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

1-2. 시큐리티 추가만으로 생기는 보안

이렇게 라이브러리를 단순히 추가함으로 왼쪽(추가 전)에 되던 통신이 오른쪽처럼 401로 막히게 됩니다.
정확하지는 않지만, 실제 코드 실행 시 막히는 현상을 파악했습니다.

TIP ^V^

401은 사용자가 로그인이 되어 있지 않은 상태 => 인증 실패 (개발자는 앱 설계 시 자격 증명이 누락되거나 잘못되었을 때 )
403은 사용자가 권한이 없는 상태 => 권한 부여 실패, 호출자를 식별하였지만, 이용 권리 없음을 나타냄

2. 스프링 시큐리티 인증 절차 설명

조금 복잡하게 되어있지만 하이라이트만 확인해보면 좋겠습니다. Oauth2.0을 사용하면 위에 정의된 아키텍쳐에 들어가는 구성 요소의 이름이 조금 바뀝니다.

1. 인증 필터 (Authentication Filter): 인증 요청을 인증 관리자에게 위임하고 응답을 바탕으로 보안 컨텍스트를 구성함
2. 인증 관리자 (Authentication Manager): 인증 공급자를 이용하여 인증을 처리한다.
3. 인증  공급자 (Authentication Provider):
     -  사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용한다.
     -  암호 관리를 구현하는 암호 인코더를 인증 논리에 이용한다.
4. 유저 세부 정보 서비스 (User Details Service)
5. 보안 컨텍스트(Security Context): 인증 프로세스 후 인증 데이터를 유지한다.

3. 자동으로 구성되는 빈

1. UserDetailsService
2. PasswordEncoder
인증 공급자 (Authentication Provider)는 위 두 개의 빈을 이용하여 1) 사용차를 찾고, 2) 암호를 확인합니다.

사용자 정보 관리하는 UserDetailsService

스프링 시큐리티에서 기본적으로 제공하는 기본 구현을 이용했지만, 우리는 입맛에 맞게 바꿔서 사용한다.

1차 정리

1. 기본적으로 UserDetailsService와 PasswordEncoder가 Basic 인증 흐름에 꼭 필요하다는 사실을 인지해야 한다.
2. Basic 인증에서는 클라이언트가 사용자 이름과 암호를 HTTP Authorization 헤더를 통해 보내기만 하면 된다.
=> 헤더 값에 접두사 Basic을 붙이고 그 뒤에 :으로 구분된 사용자 이름과 암호가 포함된 문자열을 Base64 인코딩하여 붙인다.

4. 문제🙄: 기존 websecurityconfigureradapter deprecated

기존에 사용하던 WebSecurityConfigureradapter가 Deprecated 되었다. 거의 대부분 인터넷이나 서적을 찾아보면 이를 사용하는 코드가 적혀있지만, 현재 기준으로 Deprecated가 되어 다른 코드를 사용해야 한다. 자세한 내용은 아래 하단 링크를 통해 확인하면 된다.

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

 

Spring Security without the WebSecurityConfigurerAdapter

<p>In Spring Security 5.7.0-M2 we <a href="https://github.com/spring-projects/spring-security/issues/10822">deprecated</a> the <code>WebSecurityConfigurerAdapter</code>, as we encourage users to move towards a component-based security configuration.</p> <p

spring.io

- 고민해야하는 것, Bean? 메소드 재정의?

 시큐리티를 이용할 때 두 가지 방법을 사용할 수 있다. 둘 중 하나만으로 구현해야지 일관성이 있고, 코드가 헷갈리지 않는다. 

@EnableSecrurity 어노테이션

EnableWebSecurity Interface

클래스 밖에 Configuration 어노테이션을 붙일 필요가 없다. 이미 EnableWebSecurity 인터페이스 안에 우리가 사용하고자 하는 어노테이션이 있으니 Configuration 어노테이션을 붙일 필요없다.

6. HttpBasic 활성화

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.httpBasic();
        http.authorizeRequests()
            .anyRequest().authenticated();

        return http.build();
    }

https://docs.spring.io/spring-security/site/docs/current/api/

 

Overview (spring-security-docs 5.7.3 API)

Authentication processing mechanisms, which respond to the submission of authentication credentials using various protocols (eg BASIC, CAS, form login etc).

docs.spring.io

7. User Entity 생성

로그인 구현하기 위해 DB상에 user 테이블을 생성합니다. 생성하는 이유는 유저의 정보를 DB 상에 저장하기 위함입니다.
만약에 이를 이용하지 않는다면 메모리에 저장해두면 됩니다. 이 방법은 따로 이 글에선 알아보지 않겠습니다.

import javax.persistence.*;
import java.util.List;

@Entity
public class User {

    @Id
    @GeneratedValue
    private Integer id;

    private String username;
    private String password;

    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Authority> authorities;

    public List<Authority> getAuthorities() {
        return authorities;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

}

8.UserRepository 생성

FindUserByUsername 메소드를 하나 작성해줍니다. 작성 이유는 Username으로 값을 조회하기 위함입니다.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    Optional<User> findUserByUsername(String username);
}

9. CustomUserDetail 생성

이 클래스를 생성하는 이유는 Security Context에 저장되는 인증 객체를 만들기 위함입니다. 
이 클래스는 추후 컨트롤러에서 파라미터로 받아오는 객체의 클래스입니다.

먼저  코드를 살펴보면 UserDetails를 구현합니다. 코드를 확인해봅시다.

import com.example.demo.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.stream.Collectors;

public class CustomUserDetails implements UserDetails {

    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities().stream().map(a -> new SimpleGrantedAuthority(a.getName())).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public final User getUser() {
        return user;
    }

}

실제 코드를 확인해보면 위와 같은 주석이 있습니다.

파파고 번역

Authentication 객체에 캡슐화가 된다는 부분이 중요합니다. 왜냐하면 나중에 Security Context에 넣을 때 Authentication 객체만 들어가기 때문입니다. 그래서 우리는 User 객체를 가지고 있지만 Authentication으로 캡슐화가 될 수 있는 UserDetails를 구현합니다.

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/UserDetails.html

권한, 비밀번호, 아이디 등에 관한 메소드를 정의하고 있습니다.

authentication 인터페이스는 아래 링크를 통해 확인할 수 있습니다.

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/authentication/package-summary.html

10. JpaUserService 생성

Jpa라고 붙은 이유는 Data Jpa 라이브러리를 통해서 디비 상에 값이 존재하는지 확인하기 때문입니다.
또한 이 클래스의 loadUserByUsername메소드는 위에서 정의한 CustomUserDeatils 객체를 반환합니다.

import com.example.demo.User;
import com.example.demo.UserRepository;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.function.Supplier;

@Service
public class JpaUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public JpaUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Supplier<UsernameNotFoundException> s = () -> new UsernameNotFoundException("Problem during authentication!");

        User user = userRepository.findUserByUsername(username).orElseThrow(s);

        return new CustomUserDetails(user);
    }

}

11. AuthenticationProviderService 생성

상단 부분을 다시 읽어보면, Provider가 무슨 일을 하는 지 알 수 있습니다. 다음과 같습니다.

3. 인증  공급자 (Authentication Provider):
     -  사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용한다.
     -  암호 관리를 구현하는 암호 인코더를 인증 논리에 이용한다.

결국 인증한다는 사실만 기억하세요!

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationProviderService implements AuthenticationProvider {

    private final JpaUserDetailsService userDetailsService;

    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public AuthenticationProviderService(JpaUserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        CustomUserDetails user = userDetailsService.loadUserByUsername(username);

        return checkPassword(user, password, bCryptPasswordEncoder);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public Authentication checkPassword(CustomUserDetails user, String rawPassword, PasswordEncoder encoder) {
        if(encoder.matches(rawPassword, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
        } else {
            throw new BadCredentialsException("Bad credentials");
        }
    }
}

12. 설정

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class ProjectConfig {

    private final AuthenticationProviderService authenticationProvider;

    public ProjectConfig(AuthenticationProviderService authenticationProvider) {
        this.authenticationProvider = authenticationProvider;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.httpBasic();

        http.authorizeRequests()
            .anyRequest().authenticated();

        http.authenticationProvider(authenticationProvider);

        http.formLogin()
                .defaultSuccessUrl("/main", true);

        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

AuthenticationProvder도 설정해주시면 됩니다.

이정도면 HTTP BASIC은 성공입니다.

다음 글에서는 Oauth2.0 로그인을 알아보겠습니다.

반응형
728x90

 

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

 

2668번: 숫자고르기

세로 두 줄, 가로로 N개의 칸으로 이루어진 표가 있다. 첫째 줄의 각 칸에는 정수 1, 2, …, N이 차례대로 들어 있고 둘째 줄의 각 칸에는 1이상 N이하인 정수가 들어 있다. 첫째 줄에서 숫자를 적절

www.acmicpc.net

 

이 글을 읽는다면 문제는 이미 다 알고 있다고 생각합니다.

구해야하는 정답은 첫 번째 줄에서 뽑은 수와  두 번째에서 뽑은 수가 같은 집합의 최대 크기이다.

두번째 줄에서 주어진 수에서 인덱스번호가 없는 2와 7은 첫번째 줄에서 선택할 필요가 없다.
-> 왜냐하면 2와 7을 뽑아봤자 2번째 줄에 2와 7이 없기 때문에 같은 수를 뽑을 수가 없기 때문이다.

일단 이해하기 쉽도록 그래프를 그려줍니다.
-> 그래프를 그릴 때 각 노드들은 인덱스 번호이며 연결되어있는 값들은 2번째 줄에서 준 값들의 인덱스 번호입니다.

그럼으로 2와 7을 기준으로  각 노드들에 연결되어 있는 2와 7를 제거합니다.

## 먼저 2를 제거합니다

2가 연결된 곳은 1 번 노드였고 1번 노드에 자식 노드가 있음으로 제거할 대상이 아닙니다. 이후 2가 연결된 곳이 없음으로 7를 제거합니다.

## 7 제거

7이 연결된 곳은 6이었고, 지우니, 자식 노드가 없습니다. 이로 인해 6도 사용하지 못합니다. 그럼으로 6도 지워줍니다.

## 6 제거

6이 연결된 곳은 4였고 지우니, 자식 노드가 없으므로 쓰지 못합니다.

## 4 제거

4가 연결된 곳은 5였고 제거하니 자식 노드가 남아있음으로 제거를 멈춥니다.

다 제거하고 나니  사용가능한 수는 총 3개입니다.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int inputs[101];
vector<int> adj[101];

int main() {
    int n;
    cin >> n;

    for(int i=1; i<=n; i++) {
        cin >> inputs[i];
        adj[inputs[i]].push_back(i);
    }

    int answer = n;
    for(int i=1; i<=n; i++) {
        if(adj[i].size() == 0) {
            
            int k = i;
            int index = inputs[i];
            
            while(true) {
                // 제거
                for(int j=0; j<adj[index].size(); j++) {
                    if(adj[index][j] == k) {
                        adj[index].erase(adj[index].begin()+ j);
                        answer--;
                        break;
                    }
                }
                
                if(adj[index].size() == 0) {
                    k = index;
                    index = inputs[index];
                    
                } else {
                    break;
                }
            }
        }
    }

    vector<int> answers;
    for(int i=1; i<=n; i++) {
        if(adj[i].size() != 0 ) {
            for(int j=0; j<adj[i].size(); j++) {
                answers.push_back(adj[i][j]);
            }
        }
    }
    sort(answers.begin(), answers.end());

    cout << answer << "\n";
    for(int i=0; i<answers.size(); i++){
        cout << answers[i] << "\n";
    }
}

## 재귀로 작성

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int inputs[101];
vector<int> adj[101];
int answer;
int n;

void remove(vector<int>& temp, int target){
    for(int j=0; j<temp.size(); j++) {
        if(temp[j] == target) {
            temp.erase(temp.begin()+ j);
            answer--;
            break;
        }
    }
}

void recrucive(int target, int index) {
    
    remove(adj[index], target);
            
    if(adj[index].size() == 0) {
        recrucive(index, inputs[index]);
    } else {
        return;
    }

}

int main() {
    cin >> n;
    answer = n;
    for(int i=1; i<=n; i++) {
        cin >> inputs[i];
        adj[inputs[i]].push_back(i);
    }

    for(int i=1; i<=n; i++) {
        if(adj[i].size() == 0) {
            recrucive(i, inputs[i]);
        }
    }

    vector<int> answers;
    for(int i=1; i<=n; i++) {
        if(adj[i].size() != 0 ) {
            for(int j=0; j<adj[i].size(); j++) {
                answers.push_back(adj[i][j]);
            }
        }
    }
    sort(answers.begin(), answers.end());

    cout << answer << "\n";
    for(int i=0; i<answers.size(); i++){
        cout << answers[i] << "\n";
    }
}

 

반응형

'코딩 관련 > c++' 카테고리의 다른 글

공원 산책 프로그래머스  (0) 2023.06.21
프로그래머스 할인행사  (0) 2022.12.17
백준 1, 2, 3 더하기 5 문제 분석  (0) 2022.08.09
이집트인의 곱셈  (0) 2021.08.24
스택 응용 분석해보기 1편  (0) 2021.08.24
728x90

일단 문제를 확인해보면 1부터 3까지 총 3 가지 수로 주어진 N의 더하기 경우의 수를 구하는 문제임을 알 수 있다.

조건이 있는데 연속된 수를 사용하면 안 된다란 조건이 있다. => 곧 힌트란 의미.

먼저 이를 어떻게 구하는 지 고민을 해보면 완전탐색을 생각할 수 있다.

처음 태블릿으로 적었는데 진짜 못 그렸다... 아이패드 돌려줘;;

각 경우의 수가 총 2개이다.

1 깊이에서 3개, 2 깊이에서 2개씩 총 6개, 3 깊이에서도 2개씩 총 6*2 12개이다. 4 깊이에서는 24개

결국 깊이에서의 경우의 수가 3, 6, 12, 24 순으로 늘어난다. 무슨 수열인지는 모르겠지만 자신의 앞의 수 * 2씩 늘어난다. 딱 봐도 엄청 느리다. 

그래도 수식을 구해야하니까 수열에 공통적으로 3이 곱해져 있으니, 앞으로 빼보니 3*2^n 이다. 문제에서는 n이 100,000보다 작거나 같다고 하니 3*2^n은 계산기도 오버플로우가 난다. 2^100 만 해도 1,267,650,600,228,229,401,496,703,205,376가 나오니 시도조차 하면 안 되는 방법이다.

어차피 완전탐색을 그리다보면 같은 경우의 수를 탐색하며, 똑같은 상태를 매번 계산한다는 점이 느껴진다. 그럼으로 DP로 풀 수 있다고 느껴진다고 해야한다.( 사실 문제의 유형을 보고 풀어서 ... )

이 문제는 조건이 있었다. 만약 없다면 어떻게 풀까?

조건없이 푸는 경우

5를 구한다면 2, 3, 4의 수를 더하면 된다.

근데 이 문제는 연속된 경우를 피해야 한다. 어떻게 피할 수 있을까? 일단 일차원 배열의 경우, 안에 중첩된 경우를 분간하지 못한다.

# 느낌

3차원으로 dp 그리기

# 1의 경우

모양이 똑같다.

# 2의 경우

# 3의 경우

# 4의 경우

근데 표를 돌려본다면?

테이블이 트리 모양이랑 똑같다.

신기하다

 

반응형

'코딩 관련 > c++' 카테고리의 다른 글

프로그래머스 할인행사  (0) 2022.12.17
백준 C++ 숫자고르기 골5 문제 풀이  (0) 2022.08.15
이집트인의 곱셈  (0) 2021.08.24
스택 응용 분석해보기 1편  (0) 2021.08.24
자료구조 Stack 구현하기  (0) 2021.08.23
728x90

 Oauth2.0에 대해 작년에 아무런 지식없이 인터넷을 방황하며 코드를 작성했었다. 당시 Nestjs로 작성했었는데 이제는 다른 프레임워크로 작성해야했다. 이번에는 스프링 부트로 oauth를 이용하려고 하고 라이브러리를 찾아본 결과 시큐리티 5에 있는 oauth2.0-client를 이용했다.

어려운 부분은 크게 2가지였다.

1. 트위치 Oauth에 적용하기
2. Oauth2.0에 대한 지식

해결했던 방법

1. 트위치 Oauth2.o에 적용을 해결했던 방법

 카카오와 네이버도 스프링 Oauth2.0 라이브러리에서 제공하는 기본 제공자가 아니다. 이 두개의 적용 방법과 트위치 개발자 페이지와 검색을 통해서 해결했다.

spring.security.oauth2.client.registration.twitch.client-id = 
spring.security.oauth2.client.registration.twitch.client-secret = 
spring.security.oauth2.client.registration.twitch.client-name=Twitch
spring.security.oauth2.client.registration.twitch.authorization-grant-type = authorization_code
spring.security.oauth2.client.registration.twitch.client-authentication-method=post
spring.security.oauth2.client.registration.twitch.redirect-uri = http://localhost:8080/login/oauth2/code/twitch
spring.security.oauth2.client.registration.twitch.response_type=token
spring.security.oauth2.client.registration.twitch.scope=user:read:email

# Twitch Provider 등록
spring.security.oauth2.client.provider.twitch.authorization-uri=https://id.twitch.tv/oauth2/authorize
spring.security.oauth2.client.provider.twitch.token-uri=https://id.twitch.tv/oauth2/token
spring.security.oauth2.client.provider.twitch.user-info-uri=https://id.twitch.tv/oauth2/userinfo
spring.security.oauth2.client.provider.twitch.user-name-attribute=token

 2. 기본 지식의 부족

 code를 받아서 token를 받아오는 방식이 있다. => 권한 코드

브라우저 기반 클라이언트 사이드 앱 암묵적 허가 => 액세스 토큰 발급

총 4가지 방식 중에서 2가지를 헷갈렸었다.

권한 코드는 백엔드에서 처리하는 방식에 적합하고 위 설정 파일에서spring.security.oauth2.client.registration.twitch.authorization-grant-type = authorization_code 라고 적은 부분도 어떤 방식을 이용하겠다고 설정한 것이다.

방법의 종류와 방법의 진행 방식을 책을 통해서 학습 후 진행하니까 수월해졌다.
 

반응형
728x90

Dropzone이란 라이브러리가 존재하고, 이를 React hook방식으로 사용하도록 만든 것이 react-dropzone이다.

설치

npm install --save react-dropzone
yarn add react-dropzone
### 목적
프리뷰가 있고, Drop한 이미지를 삭제할 수 있어야한다.

먼저 react-dropzone의 Preview 코드를 가져온다.

import React, {useEffect, useState} from 'react';
import {useDropzone} from 'react-dropzone';

const thumbsContainer = {
  display: 'flex',
  flexDirection: 'row',
  flexWrap: 'wrap',
  marginTop: 16
};

const thumb = {
  display: 'inline-flex',
  borderRadius: 2,
  border: '1px solid #eaeaea',
  marginBottom: 8,
  marginRight: 8,
  width: 100,
  height: 100,
  padding: 4,
  boxSizing: 'border-box'
};

const thumbInner = {
  display: 'flex',
  minWidth: 0,
  overflow: 'hidden'
};

const img = {
  display: 'block',
  width: 'auto',
  height: '100%'
};


function Previews(props) {
  const [files, setFiles] = useState([]);
  const {getRootProps, getInputProps} = useDropzone({
    accept: {
      'image/*': []
    },
    onDrop: acceptedFiles => {
      setFiles(acceptedFiles.map(file => Object.assign(file, {
        preview: URL.createObjectURL(file)
      })));
    }
  });
  
  const thumbs = files.map(file => (
    <div style={thumb} key={file.name}>
      <div style={thumbInner}>
        <img
          src={file.preview}
          style={img}
          // Revoke data uri after image is loaded
          onLoad={() => { URL.revokeObjectURL(file.preview) }}
        />
      </div>
    </div>
  ));

  useEffect(() => {
    // Make sure to revoke the data uris to avoid memory leaks, will run on unmount
    return () => files.forEach(file => URL.revokeObjectURL(file.preview));
  }, []);

  return (
    <section className="container">
      <div {...getRootProps({className: 'dropzone'})}>
        <input {...getInputProps()} />
        <p>Drag 'n' drop some files here, or click to select files</p>
      </div>
      <aside style={thumbsContainer}>
        {thumbs}
      </aside>
    </section>
  );
}

<Previews />

하나의 파일만 받아오려면 옵션을 넣어야 한다.

const {
        getRootProps,
        getInputProps,
        isFocused,
        isDragAccept,
        isDragReject,
    } = useDropzone({
        onDrop: acceptedFiles => {
            setFiles(acceptedFiles.map(file => Object.assign(file, {
                preview: URL.createObjectURL(file)
            })))
        }, accept: { 'image/*': [], multiple: false }
    });

mutiple: false란 옵션을 이용하면 된다.

### 올린 파일 삭제

제일 어려웠던 부분은 올린 파일을 삭제하는 방법이었다.

파일을 어떻게 삭제할까?

<aside style={thumbsContainer} onClick={() => setFiles([])} >
                    {files.map(file => (
                        <div>
                            <div style={thumb} key={file.name}>
                            <div style={thumbInner}>
                                <img
                                    src={file.preview}
                                    style={img}
                                    // Revoke data uri after image is loaded
                                    onLoad={() => { URL.revokeObjectURL(file.preview) }}
                                    alt="preview"
                                />
                            </div>
                        </div>
                            <div>{file.path}</div>
                        </div>
                    ))
              }
</aside>

파일의 내용을 보여주는 코드에 onClick을 달아 state를 초기화하는 방법이다.

또는 여러개의 파일을 사용한다면, setFiles( files.filter( f => ) )방법으로 하면 된다.

반응형
728x90

https://susuhan.notion.site/Spring-DI-6113d9eefba446c99413d1323abe9276

- 이쁘게 보기 -

 

Spring DI 방법론

글의 목적 필드 의존성 주입과 setter 의존성 주입, 생성자 의존성 주입에 관한 차이를 알아가기

susuhan.notion.site

Spring DI 방법론

A. 문제의 발단

코드 컨밴션이 존재하지 않아, 프로젝트 내에 코드의 일관성이 일치하지 않는 현상이 발생하였다.
그 중 하나로 의존성 주입하는 방식이 거론되어, 의존성 주입 방식의 차이와 실제 오류가 발생하는지를 알아보게 되었다.

의존성 주입 방식에는 크게 3가지가 존재한다.

  1. 필드 의존성
  2. 생성자 의존성
  3. setter 의존성

1. 필드 의존성 주입 (Field Injection)

public class A {
    @Autowired
    private AService aService;
}

2. setter 의존성 주입 (Setter based Injection)

public class A {
    private AService aService;

    @Autowired
    public void setAService(AService aService){
        this.aService = aService;
    }
}

3. 생성자 의존성 주입 (Constructor based Injection)

// 의존성 주입 대상이 한 개일 경우, 생성자에 @Autowired 어노테이션을 붙이지 않아도 된다.
public class A {
    private final AService aService;

    public A(AService aService){
        this.aService = aService;
    }
}

// 의존성 주입 대상이 2개이상인 경우, 생성자에 @Autowired 어노테이션을 붙여야 한다.
public class A {
    private final AService aService;
    private final BService bService;

    @Autowired
    public A(AService aService, BSerivce bService){
        this.aService = aService;
        this.bService = vService;
    }
}

B. 차이점: Bean 주입 순서

1. 필드 의존성 주입

  1. 주입 받으려는 빈의 생성자를 호출하여, 빈을 찾거나 빈 팩토리에 등록한다.
  2. 생성자 인자를 사용하는 빈을 찾거나 만든다.
  3. 필드에 주입한다.

2. Setter 의존성 주입

  1. 주입받으려는 빈의 생성자를 호출하여, 빈을찾거나 빈 팩토리에 등록한다.
  2. 생성자의 인자를 사용하는 빈을 찾거나 만든다.
  3. 주입하려는 빈 객체의 수정자(setter)를 호출하여 주입한다.

🟥 필드, Setter 의존성 방식은 런타임에서 의존성을 주입하여, 의존성을 주입하지 않아도 객체가 생성될 수 있다.

3. 생성자 의존성 주입

  1. 생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리에서 만든다.
  2. 찾은 파라미터 빈으로 주입하려는 생성자를 호출한다.

🟥 객체가 생성되는 시점에 빈을 주입하여, 의존성 주입되지 않아 발생할 수 있는 NullPointerException을 방지한다.

C. 순환 참조

A에서 B를 호출하고, B에서 A를 호출하는 관계를 순환 참조라 한다. 이런 경우가 발생하는 일은 한 프로젝트 내에 관리해야하는 클래스가 많아지는 경우 실수로 발생하게 된다.

@Service
public class Aservice {
    @Autowired
    private Bservice bservice;
}

@Service
public class Bservice {
    @Autowired
    private Aservice aservice;
}
  1. 필드와 Setter 방식에서는 빈이 생성된 후에 참조를 하기 때문에 애플리케이션이 아무런 오류나 경고없이 구동된다. 실제 코드가 호출될 때까지 알 수 없다는 의미이다.
  2. 생성자를 통해 의존성 주입할 경우 BeanCurrentlyInCreationException이 발생한다. 순환참조뿐만 아니라 의존 관계에 내용을 외부로 노출 시킴으로 애플리케이션이 실행하는 시점에서 오류를 확인할 수 있다.

D. 코드의 깔끔함

1. 필드 의존성 주입

@Service
public class Aservice {
    @Autowired
    private Bservice bservice;
}

장점: 한 필드에 어노테이션을 붙임으로써 생성자를 만들지 않아 코드의 수가 줄어든다.

단점: 필드가 많아질 경우 한 줄씩 어노테이션이 추가적으로 붙게 된다.

2.생성자 주입과 Loombok

매번 생성자를 만들어서 주입하는 것이 코드의 깔끔함을 저해시킨다. 하지만 Lombok을 이용한다면 생성자를 자동으로 만들어준다.

@RequiredArgsConstructor
@Service
public class Aservice {
    private final Bservice bservice;
}

RequiredArgsConstructor는 final로 선언된 필드를 가지고 생성자를 만들어준다.

E. final 사용

필드나 setter 방식을 이용시에 final 키워드를 사용할 수 없다.

final 키워드 사용 시 런타임 상에서 의존성이 변경되는 가능성을 제거해준다.

F. 실제 코드 실행 시 발생 상황 관찰하기

@Service
public class A {

    @Autowired
    B b;

}

@Service
public class B {

    @Autowired
    A a;
}

@Service
public class A {

    private final B b;

    public A(B b) {
        this.b = b;
    }
}
@Service
public class B {

    private final A a;

    public B(A a) {
        this.a = a;
    }
}

오류가 발생하지 않았다.

앞에서 한 말이 거짓말일까?

2.6.0 버전 특징

기본적으로 순환참조가 발생하지 않는다.

spring.main.allow-circular-references=true

필드 주입 = 속성 추가후 발생 시 순환참조 오류가 발생하지 않고 그대로 동작한다.

결론

  1. 2.6.0 버전 이후부터는 순환참조 오류를 미리 잡아주게 되었다. 순환참조 상의 오류때문에 필드, set방식을 안 쓸 이유는 없다.
  2. 생성자 의존성 주입 방식의 이점은 런타임 상에서 bean 객체가 변경되지 않는 것을 선호한다는 이유만 남은 것 같다. 하지만 런타임 상에서 bean 객체를 의도적으로 변경하는 사람이 존재할까? 하지만 이런 생각은 이전에 순환참조를 안 만들면 된다는 생각과 동일하기 때문에, 간단하게 막을 수 있다면 막아두는 것이 좋다고 생각한다.
  3. 코드의 깔끔함에 있어서 사람들의 견해가 다르다고 생각한다.
    누군가는 어노테이션을 사용함에 있어 깔끔하다고 생각하고, 누군가는 어노테이션이 없는 것에 깔끔함을 느낄 수 있다.
    만약에 생성자 주입이 직접 설정해야하는 번거로움이 걱정된다면 final이 있는 필드만 생성자를 만들어주는 @RequiredArgsConstructor를 사용하면 될 것 같다.
  4. set방식도 구현하는 방식만 다르지 필드 의존성 주입과의 단점과 동일하여 위에 논한 것으로 충분하다고 생각한다.
반응형
728x90

React Query

상태 관리 라이브러리란?


1. 서버 상태 관리 라이브러리

리액트에는 상태 관리 라이브러리의 큰 종류로 서버 상태, 클라이언트 상태가 존재한다.
클라이언트 상태 관리 라이브러리는 리덕스, 리코일, Zustand 등으로 존재한다.

2. 왜 상태 관리 라이브러리를 사용하나?

리덕스를 사용하는 이유:

<아래 글 참조>

https://koolreview.tistory.com/119

서버 상태 라이브러리를 사용하는 이유:

간단히 서버의 데이터를 가져와 관리해주는 라이브러리라 생각하면 된다.

Client vs Server State


Client state

앱 메모리에 유지되고 이를 액세스 하거나 업데이트하는 것은 동기식입니다.

Server state

원격으로 지속되며 가져오기 또는 업데이트를 위해 비동기식 API가 필요함

리액트 쿼리를 사용하면서 생각한 내용:


  • 클라이언트 상태 관리 라이브러리는 컴포넌트를 타고 타고 내려가는 props의 불편함 때문에 사용한다.
  • 서버 상태 관리 라이브러리는 useEffect와 useState 등을 사용하여 가져온 값을 새로운 값으로 유지하는 불편함을 해소하기 위해 사용한다.

용어 정리


 

fetching: 데이터를 가져오는 중

fresh: 데이터가 신선한(오래되지 않은) 상태

  • 서버와 현재 가지고 있는 데이터가 일치한다는 의미입니다.

stale: fresh의 반대 의미

  • 데이터가 오래되어 서버와의 데이터가 다를 것이다 가정한 상태입니다.

inactive: 사용하지 않는 상태

delete: 가비지 컬렉터에 의해 캐시에서 제거됩니다.

캐시란?

  • fetching 통해 받아온 값들을 메모리 상에 적재하는 곳입니다.

staleTime: fresh 상태에서 stale 상태로 변화하기까지의 시간

cascheTime: 캐시 메모리에 데이터가 유지되는 시간

Query: 질의문정도로 이해하면 됩니다.

QueryKey: 캐시 메모리에서 데이터를 꺼내올 때 사용하는 유일 키입니다.

흐름


상태는 총 5단계로, 그림과 같이 흘러갑니다.

특이한 점:

  • staleTime이 0이라면 바로 데이터가 썩은 상태(데이터가 일치하지 않다)라고 가정합니다.

설치


npm install react-query || yarn add react-query

React-Query 초기 설정


Bold 처리된 코드가 추가된 코드입니다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
**import { QueryClient, QueryClientProvider } from 'react-query';**
import App from './App';
import reportWebVitals from './reportWebVitals';

// Query Client를 생성
**const queryClient = new QueryClient()**
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    // Provider에 prop으로 넘겨준다.
  **<QueryClientProvider client={queryClient}>**
    <React.StrictMode>
      <App />
    </React.StrictMode>
  **</QueryClientProvider>**
);

reportWebVitals();

최상단 파일에서 HTML 태그를 QueryClientPrivoider 태그로 감싸주고, client props에 QueryClient를 넘겨줍니다.

데이터 가져오는 방법


UseQuery

import axios from 'axios'
import { useQuery } from 'react-query'

const fetchDataFunction = () => {
    return axios.get('주소')
}

export const example = () => {

        // 방법 1
    const {isLoading, data} = useQuery('text-unique-key', () => {
        // function
        return axios.get('주소')
    })

      // 방법 2
    const {isLoading, data} = useQuery('text-unique-key', fetchDataFunction);

    if(isLoading){
        return <h2>Loading</h2>
    }

    return <>

    </>
}

useQuery를 통해 값을 읽어옵니다. 첫 번째 파라미터인 text-unique-key는 쿼리를 구분하는 키로 사용됩니다.

  • 방법 1과 2의 차이는 함수를 내부에 적거나, 외부로 빼서 사용하는 차이가 존재합니다.

에러 및 로딩 다루기

const {isLoading, isError, error} = useQuery('text-unique-key', fetchDataFunction);

if(isLoading){
    return <h2>Loading</h2>
}
if(isError){
    return <h2>{error}</h2>
}

Query에서 isLoading과 isError를 넘겨주어 각 상태에 따라 반환하는 값을 달리하여 화면을 상태별로 다룰 수 있다.

DevTools 띄우기

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools'
import App from './App';
import reportWebVitals from './reportWebVitals';

const queryClient = new QueryClient()
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <QueryClientProvider client={queryClient}>
    <React.StrictMode>

    </React.StrictMode>
    **<ReactQueryDevtools initialIsOpen={false} position='bottom-right' />**
  </QueryClientProvider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
`<ReactQueryDevtools initialIsOpen={false} position='bottom-right' />`

이 코드를 작성하여 초기 오픈을 할지 말지, 어느 위치에 둘 것인지 선택 가능하다

오른쪽 하단에 꽃 모양으로 뜨며, 클릭 시 아래처럼 펼쳐진다.

마우스가 가리키는 곳이 유니크 키이며 클릭 시 하단과 같이 패널이 뜬다.

이 패널에서 액션을 실행할 수도 있고, 데이터에 대한 값에 대해 볼 수도 있다.

Query Cache


같은 데이터를 매번 받아 사용하면 데이터를 받아오기 때문에 로딩이 발생한다. 그렇기에 캐싱 기능을 사용하면 한번 받아온 데이터를 저장하여 페이지를 로딩 없이 사용할 수 있다.

기본적으로 useQuery 이용 시 staleTime은 0, cacheTime 5분으로 설정되어 캐싱은 자동으로 작동한다.

Stale Time

데이터가 자주 변경되지 않는 데이터들은 네트워크 요청을 하여 데이터를 수시로 가져올 필요가 없다. 그렇기에 데이터 요청 수를 줄이기 위해 staleTime을 조절한다.

특이한 개념 윈도 포커스 ( 탭 이동? )

브라우저 탭을 이동하다 돌아오면 오래되어버린 데이터, steal 상태의 데이터를 다시 가져온다. 하지만 staleTime 시간 동안에는 다시 받아오지 않는다.

다시 정리하자면, 기본적으로 캐싱은 5분으로 진행된다. 이는 곧 메모리에 저장된다는 의미이고, Query를 실행하는 페이지에 이동되어도 이미 데이터가 있기 때문에 로딩이 발생하지 않는다.

하지만 stealTime은 기본 값이 0 임으로 데이터를 받아온 순간부터 받아온 데이터는 썩어버린 데이터 취급을 받게 되고, 이에 따라 윈도우 포커스가 변하여 다시 원래 페이지를 가리키면 데이터 다시 받아온다.

캐시 메모리에 저장된 데이터가 cachTime이 지난 이후에 delete가 된다는 사실도 기억하자. 그렇기에 데이터가 삭제된 이후에는 loading이 발생한다.

const { isLoading, data, isError, error, isFetching } = useQuery(
    'text-unique-key',
     fetchDataFunction**,{
       cacheTime: 5000,
       staleTime: 0
     }**);
실제 time 명시는 위와 같이 한다. 하지만 기본 값과 동일함으로 적지 않아도 된다.

useQuery 옵션들

enabled: boolean

쿼리를 자동으로 실행되지 않도록 해주는 옵션

const { data } = useQuery(['todo', id], () => fetch(),{
    enabled: false
})

refetchOnMount: boolean | “always”

데이터가 stale 상태 시 마운트 할 때마다 refetch를 실행하는 옵션
default → true이며, always로 설정 시 마운트 시마다 매번 refetch를 실행한다.

const { data } = useQuery(['todo', id], () => fetch(),{
    refetchOnMount: true | false | "always"
})

refetchOnWindowFocus ⇒ boolean | “always”

데이터가 stale상태일 경우 윈도우 포커싱 될 때마 refetch가 실행된다.
default true, always는 항상 윈도우 포커싱마다 실행된다는 의미이다.

주기적으로 다시 받아오기

const { data } = useQuery(['todo', id], () => fetch(),{
    refetchInterval: 2000, // 2초마다 다시 받아옵니다.
    refetchIntervalInBackground: true, // 포커스를 잃은 경우, 백그라운드에서도 계속 동작하도록
})

OnClick마다 받아오도록 코드 작성

const { data, refetch } = useQuery(['todo', id], () => fetch(),{
    enabled: false,
})

enabled로 false로 만든 후 button의 onClick에 refetch functnion을 넣어서 클릭 시마다 호출하도록 할 수 있다.

Side Effect 처리

  • 일단 화면에 데이터를 보여 주는 용도로 사용
  • 사용자가 경험 측면에서 의미가 있음
export const RQSuperHeroesPage = () => {
    const onSuccess = () => {
        console.log('Perform side effect after data fetching')
    }

    const onError = () => {
        console.log('Perform side effect after encountering error')
    }

    const { isLodading, data isError, error, isFetching, refetch } = useQuery(
                    'super-heroes',fetchSuperHerores,{
                    onSuccess,
                    onError,
                })

onSuccess, onError 옵션에 따라 직접 만든 함수를 실행할 수 있다.

  • 위 onSuccess와 onError가 동일한 이름이기 때문에 추가적으로 작성하는 코드가 없다.(es6문법)

필터링

const { isLoading, data, isError, error, isFetching, refetch } = useQuery(
    'super-heroes', fetchSuperHerores, {
        onSuccess,
        onError,
        select: (data) => {
            const superHeroNames = data.data.map((hero)=> hero.name)
            return superHeroNames
        },
    }
)

select로 데이터를 선택(필터링)해서 사용 가능하다

  • 직접 사용하는 모습

커스텀 훅

따로 파일로 빼두어 사용하는 것을 커스텀 훅이라 한다.

동적 쿼리

id값을 넘겨서 주소 넣어 값을 받아온다.

멀티 쿼리

data의 이름이 겹쳐, :를 이용하여 이름을 바꿔 사용한다.

동적 병렬 쿼리

useQueries를 통해 배열에 map을 이용하여 쿼리 키와 쿼리 함수를 넘겨 사용한다.

쿼리 의존 상태

channelId가 의존 상태(user를 호출받은 다음 id값이 생겨서 의존 관계)이므로 있는 경우에 가져오려면, enabled 옵션을 사용한다.
enabled로 처음에는 false였다 true로 바뀌면서 값을 가져온다.

목록 → 아이템 쿼리 수 줄이기

상품 목록을 가져오고, 상품 상세 페이지 시 두 번의 쿼리 요청이 발생함.

같은 값인데 동일하게 가져오니 비효율적이며, 네트워크가 느린 곳에서는 좋은 성능을 발휘하지 못한다.

그러므로 useQueryClient를 이용하여 QueryClient에서 유일 키를 getQueryData에 넣어 값을 호출하여 사용한다.

Pagenation Query

keepPreviousData 옵션을 이용하여 새로운 데이터 요청 동안 마지막 data 값을 유지한다. 그렇기에 목록이 사라지는 깜빡임 현상을 방지할 수 있다.

무한 쿼리 페이지

외국 사이트의 쇼핑몰 사이트의 more버튼을 계속 누를 때 사용되는 query로 무한으로 요청함.

mutation (업데이트)

성공 시 invalidateQueryies로 값을 버려버린다.(무효화시킨다)

업데이트 시 가져온 데이터 관리

업데이트 쿼리 최적화

각 상태별로 로직을 따로 처리한다.

Axios 편리하게 사용하기

반응형
728x90

왜 우리는 상태 관리를 할까?


아무런 이유 없이 나는 강의에서 사용해서 사용했다 😥


 저는 초반 웹 개발을 배우면서 Vuex(상태 관리 라이브러리)를 사용하니까 사용했습니다. 그래서 상태 관리 라이브러리가 무엇인지 제대로 고민해보지 못한 것 같습니다. 개발을 점점 진행하다 보니 상태 관리가 무엇인지 서버에서 가져온 값은 무엇인지 고민하게 되어 이 글을 작성합니다.

상태 관리란?

상태 관리를 한국어로 할 때 더 직감적으로 이해하는 데 있어 방해가 된다고 생각합니다. 영어로 적을 시 State Management입니다. 여기서 State를 볼 수 있는데 State는 리액트의 useState 훅에서 가리키는 State를 말합니다. 한마디로 페이지나 컴포넌트에서 사용되는 State를 관리하는 라이브러리입니다.

왜 상태를 관리해야 할까?

 리액트나 뷰나 하나의 페이지를 컴포넌트 단위로 나누어 만들고 있습니다. 상위 컴포넌트에서 하위 컴포넌트로 데이터를 넘겨줍니다. 또 한번 더 컴포넌트에서 컴포넌트로 데이터를 넘겨주는 경우도 존재합니다. 그렇기에 점점 컴포넌트 깊이가 길어지면 상태를 관리하는 데 있어 불편함이 존재합니다. 아래 영상을 보면 쉽게 이해할 수 있습니다.

 예를 들어 매번 props로 데이터를 넘겨줘야한다거나, 상위 컴포넌트에서 어떤 값을 넘겨주었는지 기억을 해야한다거나, 원하는 데이터가 형제 컴포넌트에 있어서 상위 컴포넌트에서 값을 두개로 넘겨줘야한다거나 등의 불편을 느낄 수 있습니다. 그래서 나온 것이 리덕스입니다.

https://miro.medium.com/max/700/1*Rwq-0CxITh6DCwW8J9p9pw.gif

왜 리덕스?

 Recoil, Vuex, Ngrx 등 모두 리덕스 개념에서 시작되었으며, 리덕스는 리액트에만 사용하라고 만든 라이브러리가 아닙니다. 위 Gif와 같이 Store란 친구로 Props 대신해 데이터를 뿌려줍니다. 실제 사용하면 Store가 프론트의 DB처럼 느껴집니다.

클라이언트? 서버? 상태 관리?

 우아한형제들에서 발표한 React-Query 소개 영상을 통해 서버 상태 관리 단어를 처음 봤습니다. 그리고 리덕스와 비슷한 라이브러리들이 클라이언트 상태 관리란 것도 처음 들었습니다. 클라이언트 상태 관리? 서버 상태 관리? 이게 무슨 차이일까요?

서버 상태 관리 라이브러리 React-query/Swr

 SWR 라이브러리를 사용해보지 못해 모르지만 react-query는 사용해봐 알고 있습니다. 주식을 생각해보면 주식의 데이터는 값이 계속 변동이 되어야 합니다. 바로 돈과 직결되는 문제가 발생하기 때문입니다. 그렇기에 이 데이터를 서버의 데이터베이스의 상태와 동일하게 유지되어 좋습니다. 그렇기에 주기적으로 서버 데이터를 프론트로 가져와야합니다. 이 과정의 코드 구현을 해소해주는 라이브러리가 서버 상태 관리입니다.

클라이언트 상태 관리 라이브러리란?

 그럼 클라이언트 상태 관리 라이브러리는 무엇일까요? 기존 리덕스를 사용하는 프로젝트들을 보면 action에 axios를 사용하여 값을 업데이트했습니다. 이런 식으로 화면에 보여주는 값들을 action을 통해 가져와 관리하는 라이브러리입니다

왜? 서버 상태 관리 라이브러리가 존재해?

 서버에서 가져온 데이터는 서버에 있는 데이터와 동일할까요? 그럴 수도 있고 아닐 수도 있습니다. 그렇기때문에 주기적으로 데이터를 가져와 서버 상태와 동일하게 유지해야합니다. 그런데 리덕스에서는 이렇게 주기적으로 호출하여 가져오려면 따로 코드를 작성해야합니다. 그렇기 때문에 이를 편리하게 해주는 라이브러리가 나왔습니다.

 우리는 서버에서 가져온 값을 Props로 많이 넘기고 있기 때문에  UI에 사용되는 State보다 서버에서 가져온 State를 많이 사용하고 있다고 생각합니다.

혼용해도 될까? 하나만 써야할까?

 처음에는 리액트 쿼리와 리덕스를 같이 사용하려고 했습니다. 하지만 생각해보니 동일한 데이터를 같이 캐시에 담아두면 메모리 낭비가 발생한다고 생각했습니다. 리액트 쿼리를 사용하는 게 서버에서 값을 가져와 관리하기 편하다고 느낍니다. 그래서 리액트쿼리를 사용하고, 클라이언트 간에 UI State 관리가 힘들어진다면 그때 리덕스를 사용할 예정입니다.(사실 리덕스보다 Zustand를 사용할 생각입니다.)

참고


이미지 참조: How to Pass Data between React Class Components

반응형

'코딩 관련 > 자바스크립트' 카테고리의 다른 글

페이지 재로딩 시 상단 이동  (0) 2023.10.19
Console.log 그렇게 쓰지 마요..  (0) 2023.10.19
React-Dropzone 모듈 만들기  (0) 2022.07.08
군 계산기 만들기  (0) 2018.09.26
Moment js 너란 넘은 정말..  (0) 2018.09.26

+ Recent posts