<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ysk.log</title>
        <link>https://velog.io/</link>
        <description>꿈을 계속 간직하고 있으면 반드시 실현할 때가 온다. - 괴테.</description>
        <lastBuildDate>Sun, 26 Jan 2025 18:38:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. ysk.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sukeun_youn" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Venus] Team Project - 인증/인가 구현 (1)]]></title>
            <link>https://velog.io/@sukeun_youn/Venus-Team-Project-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@sukeun_youn/Venus-Team-Project-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 26 Jan 2025 18:38:18 GMT</pubDate>
            <description><![CDATA[<h2 id="백엔드-spring-boot--spring-security">백엔드 (Spring Boot &amp; Spring Security)</h2>
<h3 id="개발-환경">개발 환경</h3>
<p><strong>Frontend</strong> : Next.js 15 (React 19, React-dom 19), Next-Auth
<strong>Backend</strong> : Spring Boot 3.x.x, Spring Security</p>
<h3 id="의존성-추가-buildgradle">의존성 추가 (build.gradle)</h3>
<h4 id="buildgradlekts">build.gradle.kts</h4>
<pre><code class="language-kotlin">dependencies {
    // Spring Security
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;

    // Spring Web
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;

    // JPA (데이터베이스 연동 - 필요에 따라)
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;

    // OAuth 2.0 Client
    implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-client&#39;

    // Lombok (선택 사항, 코드 간결화)
    compileOnly &#39;org.projectlombok:lombok&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;

    // JSON Web Token (JWT)
    implementation &#39;io.jsonwebtoken:jjwt-api:0.11.5&#39;
    implementation &#39;io.jsonwebtoken:jjwt-impl:0.11.5&#39;
    implementation &#39;io.jsonwebtoken:jjwt-jackson:0.11.5&#39;

    // Google Client Libraries
    implementation &#39;com.google.api-client:google-api-client:1.32.1&#39;
    implementation &#39;com.google.oauth-client:google-oauth-client:1.34.1&#39;

    // 데이터베이스 드라이버 (예: H2, MySQL, PostgreSQL)
    runtimeOnly &#39;com.h2database:h2&#39; // 예시로 H2 데이터베이스 사용
}</code></pre>
<hr>
<h3 id="user-엔티티-생성">User 엔티티 생성</h3>
<pre><code class="language-java">package com.example.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

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

@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = &quot;users&quot;)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;
    private String provider; // 인증 제공자 (google, local 등)
    private String providerId; // provider 에서 제공하는 고유 id
    @ElementCollection(fetch = FetchType.EAGER)
    private List&lt;String&gt; roles; // 사용자 역할
}</code></pre>
<hr>
<h3 id="userrepository-생성">UserRepository 생성</h3>
<pre><code class="language-java">package com.example.auth.repository;

import com.example.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
    Optional&lt;User&gt; findByEmail(String email);
    Optional&lt;User&gt; findByProviderAndProviderId(String provider, String providerId);

}</code></pre>
<hr>
<h3 id="securityconfig-설정">SecurityConfig 설정</h3>
<pre><code class="language-java">// SecurityConfig.java
package com.example.auth.config;

import com.example.auth.security.JwtAuthenticationFilter;
import com.example.auth.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCryptPasswordEncoder 사용
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -&gt; cors.configurationSource(corsConfigurationSource()))
            // CSRF 비활성화 (JWT 사용 시 불필요)
            .csrf((csrf) -&gt; csrf.disable())
            // 세션 사용 안함
            .sessionManagement(session -&gt; session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests((auth) -&gt; auth
                    .requestMatchers(&quot;/auth/register&quot;, &quot;/auth/login&quot;, &quot;/auth/google&quot;, &quot;/auth/google/callback&quot;).permitAll()  // 모든 사용자 허용
                    .anyRequest().authenticated()  // 나머지 요청은 인증 필요
            )
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        // CORS 설정
        config.setAllowedOrigins(List.of(&quot;http://localhost:3000&quot;)); // 허용할 오리진
        config.setAllowedMethods(List.of(&quot;GET&quot;,&quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;PATCH&quot;, &quot;OPTIONS&quot;)); // 허용할 HTTP 메서드
        config.setAllowedHeaders(List.of(&quot;*&quot;)); // 허용할 헤더
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, config);
        return source;
    }
}</code></pre>
<hr>
<h3 id="jwttokenprovider-클래스">JwtTokenProvider 클래스</h3>
<pre><code class="language-java">package com.example.auth.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Date;
import java.util.List;

@Component
public class JwtTokenProvider {
    @Value(&quot;${jwt.secret}&quot;)
    private String secretKey;

    @Value(&quot;${jwt.expiration}&quot;)
    private long tokenValidTime;

    private SecretKey key;

    @PostConstruct
    public void init(){
        this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    // JWT 토큰 생성
    public String createToken(String email, List&lt;String&gt; roles) {
        Claims claims = Jwts.claims().setSubject(email);
        claims.put(&quot;roles&quot;, roles);

        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + tokenValidTime))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parserBuilder()
                            .setSigningKey(key)
                            .build()
                            .parseClaimsJws(token)
                            .getBody();
        String email = claims.getSubject();
        List&lt;String&gt; roles = (List&lt;String&gt;)claims.get(&quot;roles&quot;);
        List&lt;SimpleGrantedAuthority&gt; authorities = roles.stream().map(SimpleGrantedAuthority::new).toList();

        return new UsernamePasswordAuthenticationToken(email, &quot;&quot;, authorities);
    }


    // 토큰에서 이메일 추출
    public String getEmailFromToken(String token) {
         return Jwts.parserBuilder()
                 .setSigningKey(key)
                 .build()
                 .parseClaimsJws(token)
                 .getBody()
                 .getSubject();
    }


    // JWT 토큰 유효성 확인
    public boolean validateToken(String token) {
        try {
            Jws&lt;Claims&gt; claims = Jwts.parserBuilder()
                                        .setSigningKey(key)
                                        .build()
                                        .parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}</code></pre>
<hr>
<h3 id="jwtauthenticationfilter-클래스">JwtAuthenticationFilter 클래스</h3>
<pre><code class="language-java">package com.example.auth.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);

        if (token != null &amp;&amp; jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    // 헤더에서 토큰 추출
    private String resolveToken(HttpServletRequest request) {
         String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if(StringUtils.hasText(bearerToken) &amp;&amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}</code></pre>
<hr>
<h3 id="authcontroller">AuthController</h3>
<pre><code class="language-java">package com.example.auth.controller;

import com.example.auth.dto.LoginDto;
import com.example.auth.dto.RegisterDto;
import com.example.auth.entity.User;
import com.example.auth.security.JwtTokenProvider;
import com.example.auth.service.UserService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/auth&quot;)
public class AuthenticationController {

    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider jwtTokenProvider;

    @Value(&quot;${google.client-id}&quot;)
    private String googleClientId;
    @Value(&quot;${google.client-secret}&quot;)
    private String googleClientSecret;
    @Value(&quot;${google.redirect-uri}&quot;)
    private String googleRedirectUri;

    @PostMapping(&quot;/register&quot;)
    public ResponseEntity&lt;String&gt; register(@RequestBody RegisterDto registerDto) {
        User user = User.builder()
                .email(registerDto.getEmail())
                .password(passwordEncoder.encode(registerDto.getPassword()))
                .roles(List.of(&quot;ROLE_USER&quot;))
                .provider(&quot;local&quot;)
                .build();
        userService.register(user);
        return new ResponseEntity&lt;&gt;(&quot;User registered successfully&quot;, HttpStatus.CREATED);
    }

    @PostMapping(&quot;/login&quot;)
    public ResponseEntity&lt;String&gt; login(@RequestBody LoginDto loginDto) {
        User user = userService.getUserByEmail(loginDto.getEmail()).orElseThrow(()-&gt;new IllegalArgumentException(&quot;등록되지 않은 유저입니다.&quot;));
        if (!passwordEncoder.matches(loginDto.getPassword(), user.getPassword())) {
            throw new IllegalArgumentException(&quot;비밀번호가 일치하지 않습니다.&quot;);
        }
        String token = jwtTokenProvider.createToken(user.getEmail(), user.getRoles());
        return new ResponseEntity&lt;&gt;(token, HttpStatus.OK);
    }

    @GetMapping(&quot;/google&quot;)
    public void googleLogin(HttpServletResponse response) throws IOException {

        String reqUrl = UriComponentsBuilder.fromUriString(&quot;https://accounts.google.com/o/oauth2/v2/auth&quot;)
            .queryParam(&quot;client_id&quot;, googleClientId)
            .queryParam(&quot;redirect_uri&quot;, googleRedirectUri)
            .queryParam(&quot;response_type&quot;, &quot;code&quot;)
            .queryParam(&quot;scope&quot;, &quot;email profile&quot;)
            .build()
            .toUriString();

        response.sendRedirect(reqUrl);
    }

    @GetMapping(&quot;/google/callback&quot;)
    public ResponseEntity&lt;String&gt; googleCallback(@RequestParam String code) throws IOException {
        RestTemplate restTemplate = new RestTemplate();
        String tokenUrl = UriComponentsBuilder.fromUriString(&quot;https://oauth2.googleapis.com/token&quot;)
            .queryParam(&quot;code&quot;, code)
            .queryParam(&quot;client_id&quot;, googleClientId)
            .queryParam(&quot;client_secret&quot;, googleClientSecret)
            .queryParam(&quot;redirect_uri&quot;, googleRedirectUri)
            .queryParam(&quot;grant_type&quot;, &quot;authorization_code&quot;)
            .build()
            .toUriString();

        ResponseEntity&lt;String&gt; tokenResponse = restTemplate.postForEntity(tokenUrl, null, String.class);

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode tokenJson = objectMapper.readTree(tokenResponse.getBody());
        String accessToken = tokenJson.get(&quot;access_token&quot;).asText();

        String userInfoUrl = UriComponentsBuilder.fromUriString(&quot;https://www.googleapis.com/oauth2/v2/userinfo&quot;)
            .queryParam(&quot;access_token&quot;, accessToken)
            .build()
            .toUriString();

        ResponseEntity&lt;String&gt; userInfoResponse = restTemplate.getForEntity(userInfoUrl, String.class);

        JsonNode userInfoJson = objectMapper.readTree(userInfoResponse.getBody());
        String email = userInfoJson.get(&quot;email&quot;).asText();
        String googleId = userInfoJson.get(&quot;id&quot;).asText();

        User user = userService.getUserByProviderAndProviderId(&quot;google&quot;, googleId).orElse(null);

        if(user == null) {
            User newUser = User.builder()
                    .email(email)
                    .provider(&quot;google&quot;)
                    .providerId(googleId)
                    .roles(List.of(&quot;ROLE_USER&quot;))
                    .build();
            userService.register(newUser);
             return new ResponseEntity&lt;&gt;(jwtTokenProvider.createToken(email, newUser.getRoles()), HttpStatus.OK);
        }
         return new ResponseEntity&lt;&gt;(jwtTokenProvider.createToken(user.getEmail(), user.getRoles()), HttpStatus.OK);
    }
}</code></pre>
<hr>
<h3 id="userservice-인터페이스-및-구현">UserService 인터페이스 및 구현</h3>
<h4 id="userservicejava">UserService.java</h4>
<pre><code class="language-java">package com.example.auth.service;

import com.example.auth.entity.User;

import java.util.Optional;

public interface UserService {
    void register(User user);
    Optional&lt;User&gt; getUserByEmail(String email);
    Optional&lt;User&gt; getUserByProviderAndProviderId(String provider, String providerId);

}</code></pre>
<h4 id="userserviceimpljava">UserServiceImpl.java</h4>
<pre><code class="language-java">// UserServiceImpl.java
package com.example.auth.service;

import com.example.auth.entity.User;
import com.example.auth.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
    private final UserRepository userRepository;
    @Override
    public void register(User user) {
        userRepository.save(user);
    }

    @Override
    public Optional&lt;User&gt; getUserByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    @Override
    public Optional&lt;User&gt; getUserByProviderAndProviderId(String provider, String providerId) {
        return userRepository.findByProviderAndProviderId(provider,providerId);
    }
}</code></pre>
<hr>
<h3 id="dto">DTO</h3>
<h4 id="signupdtojava">SignupDto.java</h4>
<pre><code class="language-java">package com.example.demo.dto;

import lombok.Data;

@Data
public class SignupDto {
    private String email;
    private String password;
}</code></pre>
<h4 id="signindtojava">SigninDto.java</h4>
<pre><code class="language-java">package com.example.demo.dto;

import lombok.Data;

@Data
public class SigninDto {
    private String email;
    private String password;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native Bootsplash]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-Bootsplash</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-Bootsplash</guid>
            <pubDate>Thu, 02 May 2024 14:27:56 GMT</pubDate>
            <description><![CDATA[<h2 id="react-native-bootsplash">React Native Bootsplash</h2>
<blockquote>
<p><a href="https://github.com/zoontek/react-native-bootsplash">[ react-native-bootsplash 공식문서 ]</a></p>
<p>모바일 애플리케이션에 스플래시 화면과 앱 아이콘을 추가하기 위한 훌륭한 옵션입니다. </p>
<p>그것은 즉시 스플래시 화면과 앱 아이콘을 생성하는 데 활용할 수 있는 CLI와 함께 제공됩니다. </p>
<p>자세히 살펴봅시다.</p>
</blockquote>
<h2 id="sw--vs-react-native-splash-screen-">S.W. ( vs. react-native-splash-screen )</h2>
<h3 id="strong">Strong</h3>
<ul>
<li>CLI assets generator 와 함께 제공된다.</li>
<li>assets generator를 사용하여 다양한 크기의 앱 아이콘을 생성할 수 있다.</li>
<li>assets generator를 사용하여 적절한 크기의 splash screen을 생성하고, CLI의 명령으로 splash screen을 사용자 정의할 수 있다.</li>
<li>CLI는 자동으로 Android Drawable XML 파일을 자동으로 생성한다.</li>
<li>사용자가 splash screen의 화면 지속 시간을 지정할 수 있다.</li>
<li>API를 사용하면 Jest와 같은 라이브러리를 사용하여 쉽게 테스트 할 수 있다.</li>
<li>132kB의 비교적 작은 번들 크기를 가지고 있다.</li>
</ul>
<hr>
<h3 id="weak">Weak</h3>
<ul>
<li>라이브러리가 단독 개발자에 의해 개발되고 기여자가 상대적으로 적다.</li>
<li>사용자는 Android 및 iOS용 splash screen을 구성해야 한다.</li>
<li>구성이 길고 iOS 용 Xcode를 사용해야 한다.</li>
<li>.gif 확장자 파일과 애니메이션을 허용하지 않는다.</li>
<li>Expo CLI 환경은 지원하지 않는다.</li>
</ul>
<hr>
<h2 id="supplies">Supplies</h2>
<p>Splash Screen에 사용할 <strong>4096px x 4096px 로고 이미지 ( .png, .svg ) 파일</strong></p>
<hr>
<h2 id="setup">Setup</h2>
<h3 id="installation">Installation</h3>
<pre><code class="language-javascript">$ npm install react-native-bootsplash

+ cd ios &amp;&amp; pod install

# --- or ---

$ yarn add react-native-bootsplash

+ cd ios &amp;&amp; pod install</code></pre>
<hr>
<h3 id="assets-generation">Assets Generation</h3>
<pre><code class="language-javascript">$ npx react-native generate-bootsplash [Logo_path] \ [options]

+ cd ios &amp;&amp; pod install

# --- or ---

$ yarn react-native generate-bootsplash [Logo_path] \ [options]

+ cd ios &amp;&amp; pod install</code></pre>
<h4 id="logo_path-">Logo_path :</h4>
<p>Project Root 로부터 <strong>Logo 파일 위치</strong></p>
<p>예시 ) <strong>. / assets / images / logo_ss.png</strong></p>
<h4 id="options-">Options :</h4>
<pre><code class="language-javascript">--platforms &lt;list&gt;          Platforms to generate for, separated by a comma (default: &quot;android,ios,web&quot;)
--background &lt;string&gt;       Background color (in hexadecimal format) (default: &quot;#fff&quot;)
--logo-width &lt;number&gt;       Logo width at @1x (in dp - we recommend approximately ~100) (default: 100)
--assets-output &lt;string&gt;    Assets output directory path
--flavor &lt;string&gt;           Android flavor build variant (where your resource directory is) (default: &quot;main&quot;)
--html &lt;string&gt;             HTML template file path (your web app entry point) (default: &quot;index.html&quot;)
--license-key &lt;string&gt;      License key to enable brand and dark mode assets generation
--brand &lt;string&gt;            Brand file path (PNG or SVG)
--brand-width &lt;number&gt;      Brand width at @1x (in dp - we recommend approximately ~80) (default: 80)
--dark-background &lt;string&gt;  [dark mode] Background color (in hexadecimal format)
--dark-logo &lt;string&gt;        [dark mode] Logo file path (PNG or SVG)
--dark-brand &lt;string&gt;       [dark mode] Brand file path (PNG or SVG)
-h, --help                  display help for command</code></pre>
<hr>
<h4 id="example-">Example :</h4>
<pre><code class="language-javascript">yarn react-native generate-bootsplash svgs/light_logo.svg \
  --platforms=android,ios,web \
  --background=F5FCFF \
  --logo-width=100 \
  --assets-output=assets \
  --flavor=main \
  --html=index.html \
  --license-key=xxxxx \
  --brand=svgs/light_brand.svg \
  --brand-width=80 \
  --dark-background=00090A \
  --dark-logo=svgs/dark_logo.svg \
  --dark-brand=svgs/dark_brand.svg</code></pre>
<pre><code class="language-javascript">&gt;&gt; Android 

android/app/src/main/res/values/colors.xml
android/app/src/main/res/drawable-hdpi/bootsplash_logo.png
android/app/src/main/res/drawable-mdpi/bootsplash_logo.png
android/app/src/main/res/drawable-xhdpi/bootsplash_logo.png
android/app/src/main/res/drawable-xxhdpi/bootsplash_logo.png
android/app/src/main/res/drawable-xxxhdpi/bootsplash_logo.png

&gt;&gt; iOS

ios/RNBootSplashExample/BootSplash.storyboard
ios/RNBootSplashExample.xcodeproj/project.pbxproj
ios/RNBootSplashExample/Info.plist
ios/RNBootSplashExample/Images.xcassets/BootSplashLogo.imageset/Contents.json
ios/RNBootSplashExample/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo-&lt;hash&gt;.png
ios/RNBootSplashExample/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo-&lt;hash&gt;@2x.png
ios/RNBootSplashExample/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo-&lt;hash&gt;@3x.png

&gt;&gt; Web

index.html

&gt;&gt; Assets
assets/bootsplash.manifest.json
assets/bootsplash.logo.png
assets/bootsplash.logo@1.5x.png
assets/bootsplash.logo@2x.png
assets/bootsplash.logo@3x.png
assets/bootsplash.logo@4x.png

&gt;&gt; With licence key
{...}


# Thanks for using react-native-bootsplash
# Done in 2.75s.</code></pre>
<hr>
<h2 id="android">Android</h2>
<h3 id="stylesxml-수정">styles.xml 수정</h3>
<h4 id="androidappsrcmainresvaluesstylesxml">android/app/src/main/res/values/styles.xml</h4>
<pre><code class="language-xml">&lt;resources&gt;

  &lt;style name=&quot;AppTheme&quot; parent=&quot;Theme.AppCompat.DayNight.NoActionBar&quot;&gt;
      &lt;!-- Your base theme customization --&gt;
  &lt;/style&gt;

  &lt;!-- BootTheme should inherit from Theme.BootSplash or Theme.BootSplash.EdgeToEdge --&gt;
  &lt;style name=&quot;BootTheme&quot; parent=&quot;Theme.BootSplash&quot;&gt;
    &lt;item name=&quot;bootSplashBackground&quot;&gt;@color/bootsplash_background&lt;/item&gt;
    &lt;item name=&quot;bootSplashLogo&quot;&gt;@drawable/bootsplash_logo&lt;/item&gt;
    &lt;item name=&quot;bootSplashBrand&quot;&gt;@drawable/bootsplash_brand&lt;/item&gt; &lt;!-- Only if you have a brand image --&gt;
    &lt;item name=&quot;postBootSplashTheme&quot;&gt;@style/AppTheme&lt;/item&gt;
  &lt;/style&gt;

&lt;/resources&gt;</code></pre>
<hr>
<h3 id="androidmanifestxml-수정">AndroidManifest.xml 수정</h3>
<h4 id="androidappsrcmainandroidmanifestxml">android/app/src/main/AndroidManifest.xml</h4>
<pre><code class="language-xml">&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

  &lt;!-- … --&gt;

  &lt;application
    android:name=&quot;.MainApplication&quot;
    android:label=&quot;@string/app_name&quot;
    android:icon=&quot;@mipmap/ic_launcher&quot;
    android:roundIcon=&quot;@mipmap/ic_launcher_round&quot;
    android:allowBackup=&quot;false&quot;
    android:theme=&quot;@style/AppTheme&quot;&gt; &lt;!-- Apply @style/AppTheme on .MainApplication --&gt;
    &lt;activity
      android:name=&quot;.MainActivity&quot;
      android:label=&quot;@string/app_name&quot;
      android:configChanges=&quot;keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode&quot;
      android:launchMode=&quot;singleTask&quot;
      android:windowSoftInputMode=&quot;adjustResize&quot;
      android:exported=&quot;true&quot;
      android:theme=&quot;@style/BootTheme&quot;&gt; &lt;!-- Apply @style/BootTheme on .MainActivity --&gt;
      &lt;!-- … --&gt;
    &lt;/activity&gt;
  &lt;/application&gt;
&lt;/manifest&gt;</code></pre>
<hr>
<h3 id="mainactivityjavakt-수정">MainActivity.{java,kt} 수정</h3>
<h4 id="androidappsrcmainjavacomyourprojectnamemainactivityjavakt">android/app/src/main/java/com/yourprojectname/MainActivity.{java,kt}</h4>
<h4 id="mainactivityjava-react-native--073">MainActivity.java (react-native &lt; 0.73)</h4>
<pre><code class="language-java">// …

// add these required imports:
import android.os.Bundle;
import com.zoontek.rnbootsplash.RNBootSplash;

public class MainActivity extends ReactActivity {

  // …

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    RNBootSplash.init(this, R.style.BootTheme); // ⬅️ initialize the splash screen
    super.onCreate(savedInstanceState); // super.onCreate(null) with react-native-screens
  }
}</code></pre>
<h4 id="mainactivitykt-react-native--073">MainActivity.kt (react-native &gt;= 0.73)</h4>
<pre><code class="language-kotlin">// …

// add these required imports:
import android.os.Bundle
import com.zoontek.rnbootsplash.RNBootSplash

class MainActivity : ReactActivity() {

  // …

  override fun onCreate(savedInstanceState: Bundle?) {
    RNBootSplash.init(this, R.style.BootTheme) // ⬅️ initialize the splash screen
    super.onCreate(savedInstanceState) // super.onCreate(null) with react-native-screens
  }
}</code></pre>
<hr>
<h2 id="ios">iOS</h2>
<h3 id="appdelegatemm-수정">AppDelegate.mm 수정</h3>
<h4 id="ios---projectname---appdelegatemm">ios / [ ProjectName ] / AppDelegate.mm</h4>
<pre><code class="language-swift">#import &quot;AppDelegate.h&quot;
#import &quot;RNBootSplash.h&quot; // ⬅️ add the header import

// …

@implementation AppDelegate

// …

// ⬇️ Add this before file @end (for react-native 0.74+)
- (void)customizeRootView:(RCTRootView *)rootView {
  [RNBootSplash initWithStoryboard:@&quot;BootSplash&quot; rootView:rootView]; // ⬅️ initialize the splash screen
}

// OR

// ⬇️ Add this before file @end (for react-native &lt; 0.74)
- (UIView *)createRootViewWithBridge:(RCTBridge *)bridge
                          moduleName:(NSString *)moduleName
                           initProps:(NSDictionary *)initProps {
  UIView *rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
  [RNBootSplash initWithStoryboard:@&quot;BootSplash&quot; rootView:rootView]; // ⬅️ initialize the splash screen
  return rootView;
}

@end</code></pre>
<h2 id="usage">Usage</h2>
<pre><code class="language-javascript">import { useEffect } from &quot;react&quot;;
import { Text } from &quot;react-native&quot;;
import BootSplash from &quot;react-native-bootsplash&quot;;

type hide = (config?: { fade?: boolean }) =&gt; Promise&lt;void&gt;;

const App = () =&gt; {
  useEffect(() =&gt; {
    const init = async () =&gt; {
      // …do multiple sync or async tasks
    };

    init().finally(async () =&gt; {
      await BootSplash.hide({ fade: true });
      console.log(&quot;BootSplash has been hidden successfully&quot;);
    });
  }, []);

  return &lt;Text&gt;My awesome app&lt;/Text&gt;;
};


------- 사용법 -------


import BootSplash from &quot;react-native-bootsplash&quot;;

type isVisible = () =&gt; Promise&lt;boolean&gt;;

BootSplash.isVisible().then((value) =&gt; console.log(value));</code></pre>
<hr>
<h3 id="with-react-navigation">With React Navigation</h3>
<pre><code class="language-javascript">import { NavigationContainer } from &quot;@react-navigation/native&quot;;
import BootSplash from &quot;react-native-bootsplash&quot;;

const App = () =&gt; (
  &lt;NavigationContainer
    onReady={() =&gt; {
      BootSplash.hide();
    }}
  &gt;
    {/* content */}
  &lt;/NavigationContainer&gt;
);</code></pre>
<hr>
<h3 id="test-with-jest">Test With Jest</h3>
<h4 id="jestsetupjs-or-any-other-file-name">jest/setup.js (or any other file name)</h4>
<pre><code class="language-javascript">jest.mock(&quot;react-native-bootsplash&quot;, () =&gt; {
  return {
    hide: jest.fn().mockResolvedValue(),
    isVisible: jest.fn().mockResolvedValue(false),
    useHideAnimation: jest.fn().mockReturnValue({
      container: {},
      logo: { source: 0 },
      brand: { source: 0 },
    }),
  };
});</code></pre>
<h4 id="jestconfig">jest.config</h4>
<pre><code class="language-javscript">{
  &quot;setupFiles&quot;: [&quot;&lt;rootDir&gt;/jest/setup.js&quot;]
}</code></pre>
<hr>
<h3 id="status-bar-transparent">Status Bar Transparent</h3>
<h4 id="valuesstylesxml">values.styles.xml</h4>
<pre><code class="language-xml">- &lt;resources&gt;
+ &lt;resources xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;

  &lt;style name=&quot;BootTheme&quot; parent=&quot;Theme.BootSplash&quot;&gt;
    &lt;!-- … --&gt;

+   &lt;!-- Apply color + style to the status bar (true = dark-content, false = light-content) --&gt;
+   &lt;item name=&quot;android:statusBarColor&quot; tools:targetApi=&quot;m&quot;&gt;@color/bootsplash_background&lt;/item&gt;
+   &lt;item name=&quot;android:windowLightStatusBar&quot; tools:targetApi=&quot;m&quot;&gt;true&lt;/item&gt;
  &lt;/style&gt;</code></pre>
<h3 id="edge-to-edge-layout">edge-to-edge layout</h3>
<h4 id="valuesstylesxml-1">values.styles.xml</h4>
<pre><code class="language-xml">- &lt;style name=&quot;BootTheme&quot; parent=&quot;Theme.BootSplash&quot;&gt;
+ &lt;style name=&quot;BootTheme&quot; parent=&quot;Theme.BootSplash.EdgeToEdge&quot;&gt;</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB] 소셜로그인 (OAuth2) - Naver]]></title>
            <link>https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Naver</link>
            <guid>https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Naver</guid>
            <pubDate>Wed, 24 Apr 2024 08:16:25 GMT</pubDate>
            <description><![CDATA[<h2 id="before-">Before ...</h2>
<p><a href="https://velog.io/@sukeun_youn/ZANIT-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2">[ SpringBoot OAuth2 소셜로그인에 대해 알아보자. ]</a></p>
<h2 id="others-">Others ...</h2>
<p><a href="https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Google">[ 소셜로그인 (OAuth2) - Google ]</a>
<a href="https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Kakao">[ 소셜로그인 (OAuth2) - Kakao ]</a></p>
<h2 id="naver-developer-center">Naver Developer Center</h2>
<h3 id="애플리케이션-등록">애플리케이션 등록</h3>
<hr>
<ol>
<li>네이버 개발자 센터 접속 후 로그인</li>
<li>Application -&gt; 애플리케이션 등록</li>
<li>애플리케이션 이름 지정 후 사용 API는 네이버 로그인 선택</li>
<li>필요한 정보 선택</li>
<li>로그인 오픈 API 서비스 환경에서 PC 웹 선택 후 URL 지정
 <strong>서비스 URL</strong> : <a href="http://localhost:8080">http://localhost:8080</a>
 <strong>Callback URL</strong> : <a href="http://localhost:8080/login/oauth2/code/naver">http://localhost:8080/login/oauth2/code/naver</a></li>
<li>Client ID와 Client Secret을 저장</li>
</ol>
<hr>
<h3 id="getattributes-전달-객체">GetAttributes 전달 객체</h3>
<pre><code class="language-bash">&lt;naver&gt;
{
    resultcode=00, 
    message=success, 
    response = {
        id=pvdq1FSG3VZlD7Cp3JuWfAFi-3xir6A-WPlP5f8kXIo, email=chb20050@gmail.com, 
        name=안창범
    }
}</code></pre>
<hr>
<blockquote>
<p><strong>google</strong>과 <strong>facebook</strong>과는 달리 ...</p>
<p><strong>kakao</strong> 와 <strong>naver</strong> 에서 받아온 정보는 객체 안에 객체가 있는 형식이라 추출할 때, 다른 처리 방식이 필요함</p>
</blockquote>
<hr>
<h3 id="데이터-변환">데이터 변환</h3>
<blockquote>
<p>위에서 확인한 값들을 우리가 만든 User 엔티티에 맞게끔 변환해서 가입을 시켜줘야 한다.</p>
</blockquote>
<hr>
<ul>
<li>provider = &quot;google&quot;, &quot;kakao&quot;, &quot;naver&quot;, &quot;facebook&quot;</li>
<li>providerId = google의 sub값, kakao와 facebook의 id값, naver의 response의 id 값</li>
<li>loginId = provider_providerId 로 설정</li>
<li>nickname = 각 사이트에 등록한 이름으로 설정</li>
<li>email = google과 facebook의 email값, kakao의 kakao_account의 email값, naver의 response의 email값</li>
</ul>
<hr>
<h2 id="구현-예제">구현 예제</h2>
<h3 id="applicationyml">application.yml</h3>
<pre><code class="language-yml">spring:
  # OAuth 로그인
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 발급받은 클라이언트 ID
            client-secret: 발급받은 클라이언트 보안 비밀번호
            scope:
              - email
              - profile

          kakao:
            client-id: 발급받은 REST API 키
            client-secret: 발급받은 Client Secret 코드
            scope:
              - account_email
              - profile_nickname
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            client-name: Kakao
            client-authentication-method: POST

          naver:
            client-id: 발급받은 Client ID
            client-secret: 발급받은 Client Secret
            scope:
              - name
              - email
            client-name: Naver
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/naver

          facebook:
            client-id: 발급받은 앱 ID
            client-secret: 발급받은 앱 시크릿 코드
            scope:
              - email
              - public_profile

        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id

          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response</code></pre>
<h3 id="oauth2userinfo">OAuth2UserInfo</h3>
<blockquote>
<p>사이트별로 값을 추출하는 방식은 다르지만 추출해야 하는 값은 같기 때문에 통일성을 위해 interface 생성</p>
</blockquote>
<h4 id="oauth2userinfojava">OAuth2UserInfo.java</h4>
<pre><code class="language-java">public interface OAuth2UserInfo {
    String getProviderId();
    String getProvider();
    String getEmail();
    String getName();
}</code></pre>
<h4 id="naveruserinfojava">NaverUserInfo.java</h4>
<pre><code class="language-java">@AllArgsConstructor
public class NaverUserInfo implements OAuth2UserInfo{

    private Map&lt;String, Object&gt; attributes;

    @Override
    public String getProviderId() {
        return (String) attributes.get(&quot;id&quot;);
    }

    @Override
    public String getProvider() {
        return &quot;naver&quot;;
    }

    @Override
    public String getEmail() {
        return (String) attributes.get(&quot;email&quot;);
    }

    @Override
    public String getName() {
        return (String) attributes.get(&quot;name&quot;);
    }
}</code></pre>
<h3 id="oauth2userservice">OAuth2UserService</h3>
<h4 id="oauth2userservicejava">OAuth2UserService.java</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info(&quot;getAttributes : {}&quot;, oAuth2User.getAttributes());

        OAuth2UserInfo oAuth2UserInfo = null;

        String provider = userRequest.getClientRegistration().getRegistrationId();

        if(provider.equals(&quot;google&quot;)) {
            log.info(&quot;구글 로그인 요청&quot;);
            oAuth2UserInfo = new GoogleUserInfo( oAuth2User.getAttributes() );
        } else if(provider.equals(&quot;kakao&quot;)) {
            log.info(&quot;카카오 로그인 요청&quot;);
            oAuth2UserInfo = new KakaoUserInfo( (Map)oAuth2User.getAttributes() );
        } else if(provider.equals(&quot;naver&quot;)) {
            log.info(&quot;네이버 로그인 요청&quot;);
            oAuth2UserInfo = new NaverUserInfo( (Map)oAuth2User.getAttributes().get(&quot;response&quot;) );
        } else if(provider.equals(&quot;facebook&quot;)) {
            log.info(&quot;페이스북 로그인 요청&quot;);
            oAuth2UserInfo = new FacebookUserInfo( oAuth2User.getAttributes() );
        }

        String providerId = oAuth2UserInfo.getProviderId();
        String email = oAuth2UserInfo.getEmail();
        String loginId = provider + &quot;_&quot; + providerId;
        String nickname = oAuth2UserInfo.getName();


        Optional&lt;User&gt; optionalUser = userRepository.findByLoginId(loginId);
        User user = null;

        if(optionalUser.isEmpty()) {
            user = User.builder()
                    .loginId(loginId)
                    .nickname(nickname)
                    .provider(provider)
                    .providerId(providerId)
                    .role(UserRole.USER)
                    .build();
            userRepository.save(user);
        } else {
            user = optionalUser.get();
        }

        return new PrincipalDetails(user, oAuth2User.getAttributes());
    }
}</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/6f0a7005-5cb3-4b7f-99fc-f56b196028b2/image.png" alt=""></p>
<h2 id="참조">참조</h2>
<p><a href="https://chb2005.tistory.com/183">https://chb2005.tistory.com/183</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB] 소셜로그인 (OAuth2) - Kakao]]></title>
            <link>https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Kakao</link>
            <guid>https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Kakao</guid>
            <pubDate>Wed, 24 Apr 2024 08:12:04 GMT</pubDate>
            <description><![CDATA[<h2 id="before-">Before ...</h2>
<p><a href="https://velog.io/@sukeun_youn/ZANIT-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2">[ SpringBoot OAuth2 소셜로그인에 대해 알아보자. ]</a></p>
<h2 id="others-">Others ...</h2>
<p><a href="https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Google">[ 소셜로그인 (OAuth2) - Google ]</a>
<a href="https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Naver">[ 소셜로그인 (OAuth2) - Naver ]</a></p>
<h2 id="kakao-developer-console">Kakao Developer Console</h2>
<h3 id="애플리케이션-등록">애플리케이션 등록</h3>
<hr>
<ol>
<li>카카오 개발자 페이지 접속 후 로그인</li>
<li>내 어플리케이션 클릭</li>
<li>애플리케이션 추가하기</li>
<li>앱 아이콘, 앱 이름, 사업자명 작성</li>
<li>앱 키 -&gt; REST API 키 저장</li>
<li>보안 -&gt; Client Secret 생성 후 코드도 저장 + 활성화</li>
<li>카카오 로그인 -&gt; 활성화 설정 둘 다 ON으로 변경 + Redirect URI 설정
Redirect URI는 <a href="http://localhost:8080/login/oauth2/code/kakao%EB%A1%9C">http://localhost:8080/login/oauth2/code/kakao로</a> 지정</li>
<li>플랫폼 -&gt; Web 플랫폼 등록 -&gt; <a href="http://localhost:8080">http://localhost:8080</a> 설정</li>
<li>동의항목 -&gt; 가져올 정보값 선택 (일부 항목은 검수를 해야 필수 동의 설정 가능)
닉네임, 이메일만 선택하고 진행해 봄</li>
</ol>
<hr>
<h3 id="getattributes-전달-객체">GetAttributes 전달 객체</h3>
<pre><code class="language-bash">&lt;kakao&gt;
{
    id=2632890179, 
    connected_at=2023-01-22T08:17:54Z, 
    properties = {nickname=안창범}, 
    kakao_account = {
        profile_nickname_needs_agreement=false, 
        profile={nickname=안창범}, 
        has_email=true, 
        email_needs_agreement=false, 
        is_email_valid=true, 
        is_email_verified=true, 
        email=chb2005@naver.com
    }
}</code></pre>
<hr>
<blockquote>
<p><strong>google</strong>과 <strong>facebook</strong>과는 달리 ...</p>
<p><strong>kakao</strong> 와 <strong>naver</strong> 에서 받아온 정보는 객체 안에 객체가 있는 형식이라 추출할 때, 다른 처리 방식이 필요함</p>
</blockquote>
<hr>
<h3 id="데이터-변환">데이터 변환</h3>
<blockquote>
<p>위에서 확인한 값들을 우리가 만든 User 엔티티에 맞게끔 변환해서 가입을 시켜줘야 한다.</p>
</blockquote>
<hr>
<ul>
<li>provider = &quot;google&quot;, &quot;kakao&quot;, &quot;naver&quot;, &quot;facebook&quot;</li>
<li>providerId = google의 sub값, kakao와 facebook의 id값, naver의 response의 id 값</li>
<li>loginId = provider_providerId 로 설정</li>
<li>nickname = 각 사이트에 등록한 이름으로 설정</li>
<li>email = google과 facebook의 email값, kakao의 kakao_account의 email값, naver의 response의 email값</li>
</ul>
<hr>
<h2 id="구현-예제">구현 예제</h2>
<h3 id="applicationyml">application.yml</h3>
<pre><code class="language-yml">spring:
  # OAuth 로그인
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 발급받은 클라이언트 ID
            client-secret: 발급받은 클라이언트 보안 비밀번호
            scope:
              - email
              - profile

          kakao:
            client-id: 발급받은 REST API 키
            client-secret: 발급받은 Client Secret 코드
            scope:
              - account_email
              - profile_nickname
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            client-name: Kakao
            client-authentication-method: POST

          naver:
            client-id: 발급받은 Client ID
            client-secret: 발급받은 Client Secret
            scope:
              - name
              - email
            client-name: Naver
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/naver

          facebook:
            client-id: 발급받은 앱 ID
            client-secret: 발급받은 앱 시크릿 코드
            scope:
              - email
              - public_profile

        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id

          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response</code></pre>
<h3 id="oauth2userinfo">OAuth2UserInfo</h3>
<blockquote>
<p>사이트별로 값을 추출하는 방식은 다르지만 추출해야 하는 값은 같기 때문에 통일성을 위해 interface 생성</p>
</blockquote>
<h4 id="oauth2userinfojava">OAuth2UserInfo.java</h4>
<pre><code class="language-java">public interface OAuth2UserInfo {
    String getProviderId();
    String getProvider();
    String getEmail();
    String getName();
}</code></pre>
<h4 id="kakaouserinfojava">KakaoUserInfo.java</h4>
<pre><code class="language-java">@AllArgsConstructor
public class KakaoUserInfo implements OAuth2UserInfo{

    private Map&lt;String, Object&gt; attributes;

    @Override
    public String getProviderId() {
        // Long 타입이기 때문에 toString으로 변호나
        return attributes.get(&quot;id&quot;).toString();
    }

    @Override
    public String getProvider() {
        return &quot;kakao&quot;;
    }

    @Override
    public String getEmail() {
        // kakao_account라는 Map에서 추출
        return (String) ((Map) attributes.get(&quot;kakao_account&quot;)).get(&quot;email&quot;);
    }

    @Override
    public String getName() {
        // kakao_account라는 Map에서 추출
        return (String) ((Map) attributes.get(&quot;properties&quot;)).get(&quot;nickname&quot;);
    }
}</code></pre>
<h3 id="oauth2userservice">OAuth2UserService</h3>
<h4 id="oauth2userservicejava">OAuth2UserService.java</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info(&quot;getAttributes : {}&quot;, oAuth2User.getAttributes());

        OAuth2UserInfo oAuth2UserInfo = null;

        String provider = userRequest.getClientRegistration().getRegistrationId();

        if(provider.equals(&quot;google&quot;)) {
            log.info(&quot;구글 로그인 요청&quot;);
            oAuth2UserInfo = new GoogleUserInfo( oAuth2User.getAttributes() );
        } else if(provider.equals(&quot;kakao&quot;)) {
            log.info(&quot;카카오 로그인 요청&quot;);
            oAuth2UserInfo = new KakaoUserInfo( (Map)oAuth2User.getAttributes() );
        } else if(provider.equals(&quot;naver&quot;)) {
            log.info(&quot;네이버 로그인 요청&quot;);
            oAuth2UserInfo = new NaverUserInfo( (Map)oAuth2User.getAttributes().get(&quot;response&quot;) );
        } else if(provider.equals(&quot;facebook&quot;)) {
            log.info(&quot;페이스북 로그인 요청&quot;);
            oAuth2UserInfo = new FacebookUserInfo( oAuth2User.getAttributes() );
        }

        String providerId = oAuth2UserInfo.getProviderId();
        String email = oAuth2UserInfo.getEmail();
        String loginId = provider + &quot;_&quot; + providerId;
        String nickname = oAuth2UserInfo.getName();


        Optional&lt;User&gt; optionalUser = userRepository.findByLoginId(loginId);
        User user = null;

        if(optionalUser.isEmpty()) {
            user = User.builder()
                    .loginId(loginId)
                    .nickname(nickname)
                    .provider(provider)
                    .providerId(providerId)
                    .role(UserRole.USER)
                    .build();
            userRepository.save(user);
        } else {
            user = optionalUser.get();
        }

        return new PrincipalDetails(user, oAuth2User.getAttributes());
    }
}</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/6f0a7005-5cb3-4b7f-99fc-f56b196028b2/image.png" alt=""></p>
<h2 id="참조">참조</h2>
<p><a href="https://chb2005.tistory.com/183">https://chb2005.tistory.com/183</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB] 소셜로그인 (OAuth2) - Google]]></title>
            <link>https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Google</link>
            <guid>https://velog.io/@sukeun_youn/SB-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2-Google</guid>
            <pubDate>Wed, 24 Apr 2024 07:54:17 GMT</pubDate>
            <description><![CDATA[<h2 id="springboot-소셜-로그인-구글">SpringBoot 소셜 로그인 (구글)</h2>
<h2 id="before-">Before ...</h2>
<p><a href="https://velog.io/@sukeun_youn/ZANIT-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-OAuth2">[ SpringBoot OAuth2 소셜로그인에 대해 알아보자. ]</a></p>
<h2 id="google-console">Google Console</h2>
<h3 id="프로젝트-생성">프로젝트 생성</h3>
<hr>
<ol>
<li>구글 API 콘솔 접속</li>
<li>새 프로젝트 생성</li>
<li>OAuth 동의 화면 탭으로 이동</li>
<li>외부 선택 후 만들기 클릭</li>
<li>앱 이름, 사용자 지원 이메일 등 필수 칸 입력 후 저장</li>
<li>생성 완료</li>
<li>사용자 인증 정보 탭으로 이동</li>
<li>사용자 인증 정보 만들기 클릭</li>
<li>OAuth Client ID 클릭</li>
<li>원하는 애플리케이션 유형 선택 후 이름은 자유롭게 설정</li>
<li>승인된 리다이렉션 URI 입력</li>
</ol>
<p><strong>로컬</strong> : ( <a href="http://localhost:8080/login/oauth2/code/google">http://localhost:8080/login/oauth2/code/google</a> )
<strong>배포</strong> : ( http://<em>Project_URL</em>/login/oauth2/code/google )
12. 생성이 완료되면 다음과 같이 클라이언트 ID와 클라이언트 보안 비밀번호를 다운받을 수 있음.</p>
<hr>
<h2 id="구현-예제">구현 예제</h2>
<h3 id="라이브러리-추가">라이브러리 추가</h3>
<pre><code class="language-java">implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-client&#39;</code></pre>
<h3 id="applicationyml">application.yml</h3>
<pre><code class="language-java">spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 생성된 클라이언트 ID
            client-secret: 생성된 보안 비밀번호
            scope:
              - email
              - profile</code></pre>
<blockquote>
<p>보안을 위해 application.yml에 적어주지 않고 환경변수로 등록하여 사용할 수도 있음.</p>
</blockquote>
<h3 id="memeber-entity에-column-추가">Memeber Entity에 Column 추가</h3>
<h4 id="memberjava">Member.java</h4>
<pre><code class="language-java">private String provider;
private String providerId;</code></pre>
<h3 id="principal-details-수정">Principal Details 수정</h3>
<h4 id="oauth2userdetailsjava">OAuth2UserDetails.java</h4>
<pre><code class="language-java">public class OAuth2UserDetails implements UserDetails, OAuth2User { 

    ... 

    private Map&lt;String, Object&gt; attributes;

    ...

    public PrincipalDetails(User user, Map&lt;String, Object&gt; attributes) {
        this.user = user;
        this.attributes = attributes;
    }

    ...

    @Override
    public Map&lt;String, Object&gt; getAttributes() {
        return attributes;
    }
}</code></pre>
<h4 id="oauth2userdetailsjava-1">OAuth2UserDetails.java</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class Oauth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info(&quot;getAttributes : {}&quot;, oAuth2User.getAttributes());

        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oAuth2User.getAttribute(&quot;sub&quot;);
        String loginId = provider + &quot;_&quot; +providerId;

        Optional&lt;User&gt; optionalUser = userRepository.findByLoginId(loginId);
        User user;

        if(optionalUser.isEmpty()) {
            user = User.builder()
                    .loginId(loginId)
                    .nickname(oAuth2User.getAttribute(&quot;name&quot;))
                    .provider(provider)
                    .providerId(providerId)
                    .role(UserRole.USER)
                    .build();
            userRepository.save(user);
        } else {
            user = optionalUser.get();
        }

        return new PrincipalDetails(user, oAuth2User.getAttributes());
    }
}</code></pre>
<h3 id="securityconfig-수정">SecurityConfig 수정</h3>
<h4 id="securityconfigjava">SecurityConfig.java</h4>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final PrincipalOauth2UserService principalOauth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                // 인증
                .antMatchers(&quot;/security-login/info&quot;).authenticated()
                // 인가
                .antMatchers(&quot;/security-login/admin/**&quot;).hasAuthority(UserRole.ADMIN.name())
                .anyRequest().permitAll()
                .and()
                // Form Login 방식 적용
                .formLogin()
                // 로그인 할 때 사용할 파라미터들
                .usernameParameter(&quot;loginId&quot;)
                .passwordParameter(&quot;password&quot;)
                .loginPage(&quot;/security-login/login&quot;)     // 로그인 페이지 URL
                .defaultSuccessUrl(&quot;/security-login&quot;)   // 로그인 성공 시 이동할 URL
                .failureUrl(&quot;/security-login/login&quot;)    // 로그인 실패 시 이동할 URL
                .and()
                .logout()
                .logoutUrl(&quot;/security-login/logout&quot;)
                .invalidateHttpSession(true).deleteCookies(&quot;JSESSIONID&quot;)
                // OAuth 로그인
                .and()
                .oauth2Login()
                .loginPage(&quot;/security-login/login&quot;)
                .defaultSuccessUrl(&quot;/security-login&quot;)
                .userInfoEndpoint()
                .userService(principalOauth2UserService)
        ;
        http
                .exceptionHandling()
                .authenticationEntryPoint(new MyAuthenticationEntryPoint())
                .accessDeniedHandler(new MyAccessDeniedHandler());
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB] AWS S3 - 파일 업로드]]></title>
            <link>https://velog.io/@sukeun_youn/SB-AWS-S3-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-o7kx6pi0</link>
            <guid>https://velog.io/@sukeun_youn/SB-AWS-S3-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-o7kx6pi0</guid>
            <pubDate>Wed, 24 Apr 2024 06:37:47 GMT</pubDate>
            <description><![CDATA[<h2 id="before-">Before ...</h2>
<p><a href="https://velog.io/@sukeun_youn/SB-AWS-S3-BUCKET-%EC%83%9D%EC%84%B1-%EC%84%A4%EC%A0%95">[ AWS S3 Bucket 생성 및 설정 ]</a> <strong>이 완료되었다면 ...?!</strong></p>
<h2 id="aws-s3---file-upload">AWS S3 - FILE UPLOAD</h2>
<h4 id="buildgradle">Build.gradle</h4>
<pre><code class="language-java">implementation &#39;org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE&#39;</code></pre>
<h4 id="applicationyml">application.yml</h4>
<pre><code class="language-yml">cloud:
  aws:
    s3:
      bucket: &lt;S3 버킷 이름&gt;
    credentials:
      access-key: &lt;저장해놓은 액세스 키&gt;
      secret-key: &lt;저장해놓은 비밀 액세스 키&gt;
    region:
      static: ap-northeast-2
      auto: false
    stack:
      auto: false</code></pre>
</br>

<h3 id="aws-s3-configuration-생성">AWS S3 Configuration 생성</h3>
<h4 id="s3configjava">S3Config.java</h4>
<pre><code class="language-java">@Configuration
public class S3Config {

    @Value(&quot;${cloud.aws.credentials.access-key}&quot;)
    private String accessKey;

    @Value(&quot;${cloud.aws.credentials.secret-key}&quot;)
    private String secretKey;

    @Value(&quot;${cloud.aws.region.static}&quot;)
    private String region;

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return (AmazonS3Client) AmazonS3ClientBuilder
                .standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }
}</code></pre>
</br>

<h3 id="파일-업로드-구현">파일 업로드 구현</h3>
<h4 id="s3servicejava">S3Service.java</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class S3UploadService {

    private final AmazonS3 amazonS3;

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucket;

    public String saveFile(MultipartFile multipartFile) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(multipartFile.getSize());
        metadata.setContentType(multipartFile.getContentType());

        amazonS3.putObject(bucket, originalFilename, multipartFile.getInputStream(), metadata);
        return amazonS3.getUrl(bucket, originalFilename).toString();
    }
}</code></pre>
</br>

<h3 id="파일-다운로드-구현">파일 다운로드 구현</h3>
<h4 id="s3servicejava-1">S3Service.java</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class S3UploadService {

    private final AmazonS3 amazonS3;

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucket;

    ...

    public ResponseEntity&lt;UrlResource&gt; downloadImage(String originalFilename) {
    UrlResource urlResource = new UrlResource(amazonS3.getUrl(bucket, originalFilename));

    String contentDisposition = &quot;attachment; filename=\&quot;&quot; +  originalFilename + &quot;\&quot;&quot;;

    // header에 CONTENT_DISPOSITION 설정을 통해 클릭 시 다운로드 진행
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
            .body(urlResource);

    }
}</code></pre>
</br>

<h3 id="파일-삭제-구현">파일 삭제 구현</h3>
<h4 id="s3servicejava-2">S3Service.java</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class S3UploadService {

    private final AmazonS3 amazonS3;

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucket;

    ...

    public void deleteImage(String originalFilename)  {
        amazonS3.deleteObject(bucket, originalFilename);
    }    
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB] AWS S3 - BUCKET 생성 / 설정]]></title>
            <link>https://velog.io/@sukeun_youn/SB-AWS-S3-BUCKET-%EC%83%9D%EC%84%B1-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sukeun_youn/SB-AWS-S3-BUCKET-%EC%83%9D%EC%84%B1-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 24 Apr 2024 06:30:46 GMT</pubDate>
            <description><![CDATA[<h2 id="aws-s3-file-upload">AWS S3 File Upload</h2>
<h2 id="aws-s3">AWS S3</h2>
<blockquote>
<p><strong>AWS Simple Storage Service</strong>의 줄임말로 파일 서버의 역할을 하는 서비스</p>
</blockquote>
<h4 id="s3의-장점">S3의 장점</h4>
<ul>
<li>무제한 용량 (하나의 파일에 대한 용량은 있지만, 전체 용량은 무제한)</li>
<li>파일 저장에 최적화 (개발자가 따로 용량을 추가하거나 성능을 높이는 작업을 하지 않아도 됨)</li>
<li>높은 내구도 (파일이 유실될 가능성이 낮음)</li>
<li>이 외에도 다소 저렴한 비용, 높은 객체 가용성, 뛰어난 보안성 등의 장점이 있음</li>
</ul>
<h2 id="asw-s3-생성">ASW S3 생성</h2>
<blockquote>
<p><strong>객체(Object)</strong> : 파일과 파일정보로 구성된 저장단위
<strong>버킷(Bucket)</strong> : 저장된 객체 대한 컨테이너</p>
<p>버킷은 최대 100개 생성 가능하며, 버킷에 저장할 수 있는 객체수는 제한 없음.</p>
</blockquote>
<h3 id="bucket-생성">Bucket 생성</h3>
<hr>
<ol>
<li>AWS Console 접속 후 S3 서비스 선택</li>
<li>버킷 만들기 클릭!</li>
<li>원하는 버킷 이름 입력</li>
<li>AWS Region 선택 (ap-northeast-2)</li>
<li>객체 소유권 선택 (ACL 비활성화)</li>
<li>모든 퍼블릭 엑세스 차단 해제</li>
<li>나머지는 Default로 설정</li>
</ol>
<hr>
<h3 id="사용자-생성">사용자 생성</h3>
<hr>
<ol>
<li>AWS Console 접속 후 IAM 서비스 선택</li>
<li>액세스 관리 -&gt; 사용자 -&gt; 사용자 추가</li>
<li>원하는 버킷 이름 입력</li>
<li>직접 정책 연결 -&gt; AmazonS3FullAccess 선택</li>
<li>사용자 생성</li>
</ol>
<hr>
<h3 id="access-key-생성">Access Key 생성</h3>
<hr>
<ol>
<li>방금 생성한 사용자 선택 후 보안 자격 증명 탭 클릭</li>
<li>액세스 키 만들기 클릭</li>
<li>기타 선택 후 다음</li>
<li>원하는 설명 태그 입력 후 액세스 키 만들기</li>
<li>액세스 키를 만들면 아래와 같이 액세스 키와 비밀 액세스 키를 확인할 수 있는데, 이 값들을 저장후 완료</li>
</ol>
<hr>
<h3 id="버킷-정책-변경">버킷 정책 변경</h3>
<hr>
<ol>
<li>AWS Console에서 생성한 버킷으로 이동</li>
<li>권한 -&gt; 버킷 정책 -&gt; 편집</li>
<li>정책이 비어있으면 &#39;+ 새 문 추가&#39; 클릭</li>
<li>정책 내용을 아래와 같이 변경</li>
</ol>
<hr>
<pre><code class="language-bash">{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Sid&quot;: &quot;Statement1&quot;,
            &quot;Principal&quot;: &quot;*&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &quot;s3:*&quot;,
            &quot;Resource&quot;: &quot;arn:aws:s3:::&lt;버킷 이름&gt;/*&quot;
        }
    ]
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native Vector Icons]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-Vector-Icons</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-Vector-Icons</guid>
            <pubDate>Mon, 22 Apr 2024 07:31:24 GMT</pubDate>
            <description><![CDATA[<h2 id="react-native-vector-icons">react-native-vector-icons</h2>
<blockquote>
<p><strong>[공식문서]</strong>
<a href="https://github.com/oblador/react-native-vector-icons">https://github.com/oblador/react-native-vector-icons</a></p>
</blockquote>
<blockquote>
<p><strong>[디렉토리]</strong>
<a href="https://oblador.github.io/react-native-vector-icons/">https://oblador.github.io/react-native-vector-icons/</a></p>
</blockquote>
<h2 id="설치">설치</h2>
<pre><code class="language-javascript">npm install --save react-native-vector-icons

yarn add react-native-vector-icons

--------

&gt;&gt; For TypeScript &lt;&lt;

npm install --save-dev @types/react-native-vector-icons

yarn add -D @types/react-native-vector-icons</code></pre>
<h2 id="android">Android</h2>
<h4 id="androidappbuildgradle">android/app/build.gradle</h4>
<pre><code class="language-java">
apply from: file (&quot;../../node_modules/react-native-vector-icons/fonts.gradle&quot;) // add this line


------- OR -------


To customize the fonts being copied, use:


project.ext.vectoricons = [
    iconFontNames: [ &#39;MaterialIcons.ttf&#39;, &#39;EvilIcons.ttf&#39; ] // 사용하고자하는 폰트
]

apply from: file(&quot;../../node_modules/react-native-vector-icons/fonts.gradle&quot;)
</code></pre>
<hr>
<p><strong>node_modules/react-native-vector-icons/Fonts/</strong> 하위에 원하는 폰트 파일들을 <strong>COPY</strong></p>
<p><strong>android/app/src/main/assets/fonts</strong> 폴더에 <strong>PASTE</strong>. 
<em>(ensure the folder name is lowercase, i.e., fonts).</em></p>
<p>( assets/fonts 폴더가 없다면 폴더를 생성해주자. )</p>
<hr>
<pre><code class="language-javascript">
** 직접 Android Studio를 열어도 무관 **

&lt;&lt; Open Android Studio Command &gt;&gt;

$ open -a /Applications/Android\\ Studio.app ./android 
</code></pre>
<blockquote>
<p>마지막으로 Android Studio를 열고 자동으로 Gradle Sync 되는 것을 기다린다.</p>
</blockquote>
<h2 id="ios">iOS</h2>
<h4 id="iosprojectxcworkspace">ios/[project].xcworkspace</h4>
<p>을 선택해여 Xcode를 실행해준다.</p>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/90fdff86-9544-4107-8cad-ea79decbccf9/image.avif" alt=""></p>
<hr>
<p>Xcode가 실행되면 위와 같이 프로젝트를 우클릭한 후 New Group 메뉴를 선택하고 <strong>Fonts</strong> 그룹을 만들어준다.</p>
<p><strong>node_modules/react-native-vector-icons/Fonts/</strong> 의 원하는 폰트 파일들을 <strong>COPY</strong></p>
<p>위에 만든 Fonts 그룹에 <strong>PASTE</strong> </p>
<hr>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/3046cbb5-4698-4dd7-903f-024b2857b8c3/image.avif" alt=""></p>
<hr>
<p>위와 같은 창이 뜰텐데...</p>
<p><strong>&quot;Copy items if needed&quot;</strong> 가 체크된 상태에서 오른쪽 하단의 <strong>Finish</strong> 버튼을 선택</p>
<hr>
<pre><code class="language-xml">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
    ...
  &lt;key&gt;UIViewControllerBasedStatusBarAppearance&lt;/key&gt;
  &lt;false/&gt;
  &lt;key&gt;UIAppFonts&lt;/key&gt;
  &lt;array&gt;
    &lt;string&gt;AntDesign.ttf&lt;/string&gt;
    &lt;string&gt;Entypo.ttf&lt;/string&gt;
    &lt;string&gt;EvilIcons.ttf&lt;/string&gt;
    &lt;string&gt;Feather.ttf&lt;/string&gt;
    &lt;string&gt;FontAwesome.ttf&lt;/string&gt;
    &lt;string&gt;FontAwesome5_Brands.ttf&lt;/string&gt;
    &lt;string&gt;FontAwesome5_Regular.ttf&lt;/string&gt;
    &lt;string&gt;FontAwesome5_Solid.ttf&lt;/string&gt;
    &lt;string&gt;Foundation.ttf&lt;/string&gt;
    &lt;string&gt;Ionicons.ttf&lt;/string&gt;
    &lt;string&gt;MaterialCommunityIcons.ttf&lt;/string&gt;
    &lt;string&gt;MaterialIcons.ttf&lt;/string&gt;
    &lt;string&gt;Octicons.ttf&lt;/string&gt;
    &lt;string&gt;SimpleLineIcons.ttf&lt;/string&gt;
    &lt;string&gt;Zocial.ttf&lt;/string&gt;
    &lt;string&gt;Fontisto.ttf&lt;/string&gt;
  &lt;/array&gt;
&lt;/dict&gt;
</code></pre>
<hr>
<p><strong>ios/[project]/Info.plist</strong> 파일을 열고 위의 내용을 추가 후</p>
<p>( 물론... 원하는 폰트만 적용할 것. )</p>
<p>마지막으로, <strong>Xcode</strong> 에서 <strong>cmd + shift + k</strong>를 눌러 <strong>Clean Build Folder를 실행</strong>하면 <strong>iOS</strong> 설정 끝...</p>
<hr>
<h2 id="사용법">사용법</h2>
<pre><code class="language-javascript">
import Icon from &#39;react-native-vector-icons/FontAwesome&#39;;

export default class Home extends React.Component&lt;Props, State&gt; {
    render() {
        return (
            &lt;View&gt;
                &lt;Icon name=&quot;home&quot; size={24} color=&quot;#ffffff&quot; /&gt;
            &lt;/View&gt;
        );
    }
}
</code></pre>
</br>

<h2 id="reference">Reference</h2>
<p><a href="https://deku.posstree.com/ko/react-native/react-native-vector-icons/">https://deku.posstree.com/ko/react-native/react-native-vector-icons/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native 권한 설정]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-%EA%B6%8C%ED%95%9C-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-%EA%B6%8C%ED%95%9C-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 19 Apr 2024 11:02:21 GMT</pubDate>
            <description><![CDATA[<h2 id="react-native-permissions">react-native-permissions</h2>
<p>React Native 앱에서 <strong>권한을 요청하고 처리하는 라이브러리</strong>.</p>
<blockquote>
<p><strong>[react-native-permissions 공식문서]</strong>
<a href="https://github.com/zoontek/react-native-permissions">https://github.com/zoontek/react-native-permissions</a></p>
</blockquote>
<hr>
<h4 id="설명">설명</h4>
<ul>
<li><p>사용자가 앱에서 필요로 하는 권한을 요청하면 사용자에게 <strong>해당 권한을 요청하는 대화상자</strong>를 보여준다.</p>
</li>
<li><p>사용자가 권한을 <strong>허용하면 이를 처리</strong>하고, <strong>거부하면 사용자에게 알림</strong>을 보낸다.</p>
</li>
<li><p><strong>iOS</strong> - 사용자가 권한을 승일할 때마다 대화 상자에 표시된다.</p>
</li>
<li><p><strong>Android</strong> - 일부 권한은 앱 설치 시점에 사용자에게 요청되고 다른 권한은 앱이 실행되는 동안 사용자에게 요청된다.</p>
</li>
</ul>
<hr>
<h2 id="설치">설치</h2>
<pre><code class="language-javascript">
$ npm install --save react-native-permissions

$ yarn add react-native-permissions 
</code></pre>
<h3 id="ios-추가설정">iOS 추가설정</h3>
<h4 id="ios--podfile">ios / Podfile</h4>
<blockquote>
<p><strong>1. 아래 ( - ) 부분을 삭제를 해준다.</strong></p>
</blockquote>
<pre><code class="language-swift"># Transform this into a `node_require` generic function:

- # Resolve react_native_pods.rb with node to allow for hoisting
- require Pod::Executable.execute_command(&#39;node&#39;, [&#39;-p&#39;,
-   &#39;require.resolve(
-     &quot;react-native/scripts/react_native_pods.rb&quot;,
-     {paths: [process.argv[1]]},
-   )&#39;, __dir__]).strip</code></pre>
<blockquote>
<p>*<em>2. 아래 부분 (코드) 을 추가해준다. *</em></p>
<p>( + ) 기호를 당연히 제외하고 ... 겠죠?</p>
</blockquote>
<pre><code class="language-swift">+ def node_require(script)
+   # Resolve script with node to allow for hoisting
+   require Pod::Executable.execute_command(&#39;node&#39;, [&#39;-p&#39;,
+     &quot;require.resolve(
+       &#39;#{script}&#39;,
+       {paths: [process.argv[1]]},
+     )&quot;, __dir__]).strip
+ end

# Use it to require both react-native&#39;s and this package&#39;s scripts:

+ node_require(&#39;react-native/scripts/react_native_pods.rb&#39;)
+ node_require(&#39;react-native-permissions/scripts/setup.rb&#39;)</code></pre>
<blockquote>
<p><strong>3. cd ios</strong> ------&gt; <strong>pod install</strong></p>
</blockquote>
</br>

<h2 id="permissions-의-종류">Permissions 의 종류</h2>
<hr>
<p>필요한 권한만 놔두고 <strong>나머지는 주석 처리</strong>하여 사용하면 된다.</p>
<p>물론, 지워도 무방하다.</p>
<hr>
<h3 id="android">Android</h3>
<h4 id="androidappsrcmainandroidmanifestxml">android/app/src/main/AndroidManifest.xml</h4>
<pre><code class="language-java">&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;

  &lt;!-- 🚨 Keep only the permissions used in your app 🚨 --&gt;

  &lt;uses-permission android:name=&quot;android.permission.ACCEPT_HANDOVER&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.ACCESS_BACKGROUND_LOCATION&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.ACCESS_COARSE_LOCATION&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.ACCESS_FINE_LOCATION&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.ACCESS_MEDIA_LOCATION&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.ACTIVITY_RECOGNITION&quot; /&gt;
  &lt;uses-permission android:name=&quot;com.android.voicemail.permission.ADD_VOICEMAIL&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.ANSWER_PHONE_CALLS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.BLUETOOTH_ADVERTISE&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.BLUETOOTH_CONNECT&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.BLUETOOTH_SCAN&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.BODY_SENSORS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.BODY_SENSORS_BACKGROUND&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.CALL_PHONE&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.CAMERA&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.GET_ACCOUNTS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.NEARBY_WIFI_DEVICES&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.POST_NOTIFICATIONS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.PROCESS_OUTGOING_CALLS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_CALENDAR&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_CALL_LOG&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_CONTACTS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_MEDIA_AUDIO&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_MEDIA_IMAGES&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_MEDIA_VIDEO&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_PHONE_NUMBERS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_PHONE_STATE&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_SMS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.RECEIVE_MMS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.RECEIVE_SMS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.RECEIVE_WAP_PUSH&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.RECORD_AUDIO&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.SEND_SMS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.USE_SIP&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.UWB_RANGING&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.WRITE_CALENDAR&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.WRITE_CALL_LOG&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.WRITE_CONTACTS&quot; /&gt;
  &lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot; /&gt;

  &lt;!-- … --&gt;

&lt;/manifest&gt;</code></pre>
<h3 id="ios">iOS</h3>
<h4 id="iospodfile">ios/Podfile</h4>
<hr>
<p><strong>handler 추가하기</strong> ( 물론 ... 필요한 권한만 )</p>
<hr>
<pre><code class="language-swift"> permissions_path = &#39;../node_modules/react-native-permissions/ios&#39;

  pod &#39;Permission-AppTrackingTransparency&#39;, :path =&gt; &quot;#{permissions_path}/AppTrackingTransparency&quot;
  pod &#39;Permission-BluetoothPeripheral&#39;, :path =&gt; &quot;#{permissions_path}/BluetoothPeripheral&quot;
  pod &#39;Permission-Calendars&#39;, :path =&gt; &quot;#{permissions_path}/Calendars&quot;
  pod &#39;Permission-Camera&#39;, :path =&gt; &quot;#{permissions_path}/Camera&quot;
  pod &#39;Permission-Contacts&#39;, :path =&gt; &quot;#{permissions_path}/Contacts&quot;
  pod &#39;Permission-FaceID&#39;, :path =&gt; &quot;#{permissions_path}/FaceID&quot;
  pod &#39;Permission-LocationAccuracy&#39;, :path =&gt; &quot;#{permissions_path}/LocationAccuracy&quot;
  pod &#39;Permission-LocationAlways&#39;, :path =&gt; &quot;#{permissions_path}/LocationAlways&quot;
  pod &#39;Permission-LocationWhenInUse&#39;, :path =&gt; &quot;#{permissions_path}/LocationWhenInUse&quot;
  pod &#39;Permission-MediaLibrary&#39;, :path =&gt; &quot;#{permissions_path}/MediaLibrary&quot;
  pod &#39;Permission-Microphone&#39;, :path =&gt; &quot;#{permissions_path}/Microphone&quot;
  pod &#39;Permission-Motion&#39;, :path =&gt; &quot;#{permissions_path}/Motion&quot;
  pod &#39;Permission-Notifications&#39;, :path =&gt; &quot;#{permissions_path}/Notifications&quot;
  pod &#39;Permission-PhotoLibrary&#39;, :path =&gt; &quot;#{permissions_path}/PhotoLibrary&quot;
  pod &#39;Permission-PhotoLibraryAddOnly&#39;, :path =&gt; &quot;#{permissions_path}/PhotoLibraryAddOnly&quot;
  pod &#39;Permission-Reminders&#39;, :path =&gt; &quot;#{permissions_path}/Reminders&quot;
  pod &#39;Permission-Siri&#39;, :path =&gt; &quot;#{permissions_path}/Siri&quot;
  pod &#39;Permission-SpeechRecognition&#39;, :path =&gt; &quot;#{permissions_path}/SpeechRecognition&quot;
  pod &#39;Permission-StoreKit&#39;, :path =&gt; &quot;#{permissions_path}/StoreKit&quot;</code></pre>
<h4 id="iosinfoplist">ios/Info.plist</h4>
<hr>
<p><strong>iOS</strong> 의 경우, 아래 <strong>[ YOUR TEXT ]</strong> 부분에 권한 사용 목적을 입력해야되는데, </p>
<p><strong>AppStore</strong> 승인 허가를 위해서 <strong>최대한 자세히 적는 것</strong> 을 권장한다.</p>
<hr>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;

  &lt;!-- 🚨 Keep only the permissions used in your app 🚨 --&gt;

  &lt;key&gt;NSAppleMusicUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSBluetoothAlwaysUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSBluetoothPeripheralUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSCalendarsUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSCameraUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSContactsUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSFaceIDUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSLocationAlwaysAndWhenInUseUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSLocationAlwaysUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSLocationTemporaryUsageDescriptionDictionary&lt;/key&gt;
  &lt;dict&gt;
    &lt;key&gt;YOUR-PURPOSE-KEY&lt;/key&gt;
    &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;/dict&gt;
  &lt;key&gt;NSLocationWhenInUseUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSMicrophoneUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSMotionUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSPhotoLibraryUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSPhotoLibraryAddUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSRemindersUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSSpeechRecognitionUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSSiriUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;
  &lt;key&gt;NSUserTrackingUsageDescription&lt;/key&gt;
  &lt;string&gt;YOUR TEXT&lt;/string&gt;

  &lt;!-- … --&gt;

&lt;/dict&gt;
&lt;/plist&gt;</code></pre>
<hr>
<h2 id="사용법-예시">사용법 예시</h2>
<pre><code class="language-javascript">
  import {PERMISSIONS, RESULTS, request} from &#39;react-native-permissions&#39;;

  const askPermission = async () =&gt; {
    try {
      const result = await request(PERMISSIONS.IOS.PHOTO_LIBRARY);
      if (result === RESULTS.GRANTED) {
        // do something
      }
    } catch (error) {
      console.log(&#39;askPermission&#39;, error);
    }
  };
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native 환경변수 설정]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 19 Apr 2024 06:56:10 GMT</pubDate>
            <description><![CDATA[<h2 id="react-native-dotenv">react-native-dotenv</h2>
<blockquote>
<p>React Native 애플리케이션에서 <strong>.env 파일</strong>을 활용할 수 있도록 도와주는 라이브러리</p>
</blockquote>
<h3 id="설치">설치</h3>
<pre><code class="language-javascript">npm install -D react-native-dotenv
npm insall @types/react-native-dotenv // typescript

-------

yarn add -D react-native-dotenv
yarn add @types/react-native-dotenv // typescript</code></pre>
<h3 id="사용법">사용법</h3>
<h3 id="babelconfigjs">babel.config.js</h3>
<blockquote>
<p>프로젝트 내의 .env.local 파일을 불러올 수 있도록 구성</p>
</blockquote>
<pre><code class="language-javascript">module.exports = {
    presets: [&#39;module:metro-react-native-babel-preset&#39;],
    plugins: [
        // react-native-dotenv
        [
            &#39;module:react-native-dotenv&#39;,
            {
                &quot;envName&quot;: &quot;APP_ENV&quot;,
                &quot;moduleName&quot;: &quot;@env&quot;, // import 해올 때 react-native-env -&gt; @env 가능.
                &quot;path&quot;: &quot;.env&quot;,
                &quot;blocklist&quot;: null,
                &quot;allowlist&quot;: null,
                &quot;blacklist&quot;: null, 
                &quot;whitelist&quot;: null, 
                &quot;safe&quot;: false, // .env 파일에 정의된 환경 변수만 허용
                &quot;allowUndefined&quot;: true, // 정의되지 않은 변수 가져오기를 허용
                &quot;verbose&quot;: false
            },
        ],
    ]
}</code></pre>
<h3 id="env">.env</h3>
<blockquote>
<p>프로젝트의 루트 경로에 파일을 생성</p>
</blockquote>
<pre><code class="language-javascript">
API_URL = https://velog.io/....
</code></pre>
<h3 id="envdts">.env.d.ts</h3>
<blockquote>
<p>타입스크립트 환경에서는 env.d.ts 파일을 생성하고 사용하고자 하는 환경 변수 타입을 지정 해야한다.
추가를 안 할 시, 환경 변수를 사용하려 할 때 해당 변수를 찾을 수 없다고 오류가 발생하게 된다.</p>
</blockquote>
<pre><code class="language-javascript">declare module &#39;@env&#39; {
    export const API_URL: string;
      // ... export const 환경변수 이름: string;
}</code></pre>
<h3 id="import">Import</h3>
<pre><code class="language-javascript">
import {API_URL} from &quot;@env&quot;
// 또는
// import {API_URL, API_TOKEN} from &quot;react-native-dotenv&quot;

fetch(`${API_URL}/users`, {
  headers: {
    &#39;Authorization&#39;: `Bearer ${API_TOKEN}`
  }
});

</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native 절대경로 설정]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-%EC%A0%88%EB%8C%80%EA%B2%BD%EB%A1%9C-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-%EC%A0%88%EB%8C%80%EA%B2%BD%EB%A1%9C-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 19 Apr 2024 06:22:48 GMT</pubDate>
            <description><![CDATA[<h2 id="절대경로-설정하기--typescript-">절대경로 설정하기 ( TypeScript )</h2>
<h3 id="babel-plugin-module-resolver">Babel Plugin Module Resolver</h3>
<blockquote>
<p><strong>[Babel_Plugin_Module_Resolver 공식문서]</strong>
<a href="https://www.npmjs.com/package/babel-plugin-module-resolver">https://www.npmjs.com/package/babel-plugin-module-resolver</a></p>
</blockquote>
<pre><code class="language-javascript">npm install -D babel-plugin-module-resolver

yarn add babel-plugin-module-resolver</code></pre>
<h3 id="babelconfigjs">babel.config.js</h3>
<h4 id="alias-설정하기">Alias 설정하기</h4>
<pre><code class="language-javascript">module.exports = {
  presets: [&#39;module:metro-react-native-babel-preset&#39;],
  plugins: [
    [
      &#39;module-resolver&#39;,
      {
        root: [&#39;./src&#39;],
        extensions: [
          &#39;.ios.ts&#39;,
          &#39;.android.ts&#39;,
          &#39;.ts&#39;,
          &#39;.ios.tsx&#39;,
          &#39;.android.tsx&#39;,
          &#39;.tsx&#39;,
          &#39;.jsx&#39;,
          &#39;.js&#39;,
          &#39;.json&#39;,
        ],
        alias: {
          &#39;~&#39;: &#39;./src&#39;,
          &#39;@components&#39;: &#39;./src/components&#39;,
          &#39;@screens&#39;: &#39;./src/screens&#39;,
          &#39;@assets&#39;: &#39;./src/assets&#39;,
          &#39;@query&#39;: &#39;./src/query&#39;,
          ...
        },
      },
    ],
  ],
};</code></pre>
<h3 id="tsconfig-js--json-">tsconfig( .js / .json )</h3>
<h4 id="typescript-사용시">TypeScript 사용시...</h4>
<p>tsconfig.js 기본적으로 다음과 같이 구성되어있다.</p>
<pre><code class="language-javascript">{
  &quot;compilerOptions&quot;: {
    /* Basic Options */
    &quot;target&quot;: &quot;esnext&quot;,                       /* Specify ECMAScript target version: &#39;ES3&#39; (default), &#39;ES5&#39;, &#39;ES2015&#39;, &#39;ES2016&#39;, &#39;ES2017&#39;,&#39;ES2018&#39; or &#39;ESNEXT&#39;. */
    &quot;module&quot;: &quot;commonjs&quot;,                     /* Specify module code generation: &#39;none&#39;, &#39;commonjs&#39;, &#39;amd&#39;, &#39;system&#39;, &#39;umd&#39;, &#39;es2015&#39;, or &#39;ESNext&#39;. */
    &quot;lib&quot;: [&quot;es2017&quot;],                        /* Specify library files to be included in the compilation. */
    &quot;allowJs&quot;: true,                          /* Allow javascript files to be compiled. */
    // &quot;checkJs&quot;: true,                       /* Report errors in .js files. */
    &quot;jsx&quot;: &quot;react-native&quot;,                    /* Specify JSX code generation: &#39;preserve&#39;, &#39;react-native&#39;, or &#39;react&#39;. */
    // &quot;declaration&quot;: true,                   /* Generates corresponding &#39;.d.ts&#39; file. */
    // &quot;sourceMap&quot;: true,                     /* Generates corresponding &#39;.map&#39; file. */
    // &quot;outFile&quot;: &quot;./&quot;,                       /* Concatenate and emit output to single file. */
    // &quot;outDir&quot;: &quot;./&quot;,                        /* Redirect output structure to the directory. */
    // &quot;rootDir&quot;: &quot;./&quot;,                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // &quot;removeComments&quot;: true,                /* Do not emit comments to output. */
    &quot;noEmit&quot;: true,                           /* Do not emit outputs. */
    // &quot;incremental&quot;: true,                   /* Enable incremental compilation */
    // &quot;importHelpers&quot;: true,                 /* Import emit helpers from &#39;tslib&#39;. */
    // &quot;downlevelIteration&quot;: true,            /* Provide full support for iterables in &#39;for-of&#39;, spread, and destructuring when targeting &#39;ES5&#39; or &#39;ES3&#39;. */
    &quot;isolatedModules&quot;: true,                  /* Transpile each file as a separate module (similar to &#39;ts.transpileModule&#39;). */

    /* Strict Type-Checking Options */
    &quot;strict&quot;: true,                           /* Enable all strict type-checking options. */
    // &quot;noImplicitAny&quot;: true,                 /* Raise error on expressions and declarations with an implied &#39;any&#39; type. */
    // &quot;strictNullChecks&quot;: true,              /* Enable strict null checks. */
    // &quot;strictFunctionTypes&quot;: true,           /* Enable strict checking of function types. */
    // &quot;strictPropertyInitialization&quot;: true,  /* Enable strict checking of property initialization in classes. */
    // &quot;noImplicitThis&quot;: true,                /* Raise error on &#39;this&#39; expressions with an implied &#39;any&#39; type. */
    // &quot;alwaysStrict&quot;: true,                  /* Parse in strict mode and emit &quot;use strict&quot; for each source file. */

    /* Additional Checks */
    // &quot;noUnusedLocals&quot;: true,                /* Report errors on unused locals. */
    // &quot;noUnusedParameters&quot;: true,            /* Report errors on unused parameters. */
    // &quot;noImplicitReturns&quot;: true,             /* Report error when not all code paths in function return a value. */
    // &quot;noFallthroughCasesInSwitch&quot;: true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    &quot;moduleResolution&quot;: &quot;node&quot;,               /* Specify module resolution strategy: &#39;node&#39; (Node.js) or &#39;classic&#39; (TypeScript pre-1.6). */
    // &quot;baseUrl&quot;: &quot;./&quot;,                       /* Base directory to resolve non-absolute module names. */
    // &quot;paths&quot;: {},                           /* A series of entries which re-map imports to lookup locations relative to the &#39;baseUrl&#39;. */
    // &quot;rootDirs&quot;: [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // &quot;typeRoots&quot;: [],                       /* List of folders to include type definitions from. */
    // &quot;types&quot;: [],                           /* Type declaration files to be included in compilation. */
    &quot;allowSyntheticDefaultImports&quot;: true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    &quot;esModuleInterop&quot;: true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies &#39;allowSyntheticDefaultImports&#39;. */
    // &quot;preserveSymlinks&quot;: true,              /* Do not resolve the real path of symlinks. */
    &quot;skipLibCheck&quot;: false,                    /* Skip type checking of declaration files. */
    &quot;resolveJsonModule&quot;: true                 /* Allows importing modules with a ‘.json’ extension, which is a common practice in node projects. */

    /* Source Map Options */
    // &quot;sourceRoot&quot;: &quot;./&quot;,                    /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // &quot;mapRoot&quot;: &quot;./&quot;,                       /* Specify the location where debugger should locate map files instead of generated locations. */
    // &quot;inlineSourceMap&quot;: true,               /* Emit a single file with source maps instead of having a separate file. */
    // &quot;inlineSources&quot;: true,                 /* Emit the source alongside the sourcemaps within a single file; requires &#39;--inlineSourceMap&#39; or &#39;--sourceMap&#39; to be set. */

    /* Experimental Options */
    // &quot;experimentalDecorators&quot;: true,        /* Enables experimental support for ES7 decorators. */
    // &quot;emitDecoratorMetadata&quot;: true,         /* Enables experimental support for emitting type metadata for decorators. */
  },
  &quot;exclude&quot;: [
    &quot;node_modules&quot;, &quot;babel.config.js&quot;, &quot;metro.config.js&quot;, &quot;jest.config.js&quot;
  ]
}</code></pre>
<hr>
<p>아래의 코드를 추가해준다.
<strong>baseUrl</strong>을 기준으로 각각의 폴더의 하위 모든 파일들을 &#39;@&#39; 를 이용한 경로로 치환하는 설정</p>
<hr>
<pre><code class="language-javascript">{
  &quot;compilerOptions&quot;: {
    ...
    &quot;baseUrl&quot;: &quot;./src&quot;,
    &quot;paths&quot;: {
      &quot;~/*&quot;: [&quot;./*&quot;],
      &quot;@components/*&quot;: [&quot;components/*&quot;],
      &quot;@screens/*&quot;: [&quot;screens/*&quot;],
      &quot;@assets/*&quot;: [&quot;assets/*&quot;],
      &quot;@query/*&quot;: [&quot;query/*&quot;]
    },
    ...
  }  
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native App Icon 적용]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-App-Icon</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-App-Icon</guid>
            <pubDate>Fri, 19 Apr 2024 03:53:42 GMT</pubDate>
            <description><![CDATA[<h2 id="app-icon-설정하기">App Icon 설정하기</h2>
<h3 id="이미지-준비하기">이미지 준비하기</h3>
<hr>
<p><strong>LOGO</strong> 는 1024x1024 사이즈 하나만 준비</p>
<p><a href="https://www.appicon.co/#app-icon">App_Icon</a> 에서 아이콘으로 사용할 이미지를 드래그해서 넣고 <strong>Generate</strong> 눌러서 다운</p>
<hr>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/b88a5a7b-8619-4987-94df-c5af13371902/image.png" alt=""></p>
<hr>
<p>아래와 같이 Android에서 사용할 이미지와 AppIcon Asset이 다운받아진다.
Android에서는 추가적으로 round와 square 모양의 이미지가 필요하다.
round 이미지는 <a href="https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=image&amp;foreground.space.trim=1&amp;foreground.space.pad=-0.05&amp;foreColor=rgba(96%2C%20125%2C%20139%2C%200)&amp;backColor=rgb(255%2C%20255%2C%20255)&amp;crop=0&amp;backgroundShape=circle&amp;effects=none&amp;name=ic_launcher_round">Android_Asset_Studio</a> 에서 추출하면 된다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/6718737e-a73e-4fa3-81cd-af0831e148df/image.png" alt=""></p>
<h4 id="android_asset_studio">Android_Asset_Studio</h4>
<hr>
<p>아래와 같이 Circle을 선택하고 name은 ic_launcher_round로 바꿔준다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/8014f062-b8db-49d7-b3a1-1c210db0215d/image.png" alt=""></p>
<hr>
<p>다운받으면 아래와 같은 이미지들이 나온다.
res/mipmap 폴더에 있는 이미지들을 사용하면 된다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/db6d8240-30ee-4e24-89c4-22cea6d5c626/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/56646418-b5bc-44c3-a6d9-8042daf556b1/image.png" alt=""></p>
<h3 id="ios">Ios</h3>
<hr>
<p><strong>ios</strong> 앱아이콘 수정은 Xcode에서 한다.
프로젝트 루트에서 아래 명령어로 Xcode를 실행</p>
<pre><code class="language-javascript">&gt;&gt; $ xed ./ios
</code></pre>
<p><strong>[프로젝트명]/Images.scassets</strong> 로 들어가서 AppIcon을 누르면 아래와 같이 등록창이 나온다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/8b346b63-d4b2-4a95-8625-165d73073d55/image.png" alt=""></p>
<hr>
<p>위에서 다운 받은 앱아이콘들을 사이즈에 맞게 넣어주면 끝.</p>
<hr>
<h3 id="android">Android</h3>
<h4 id="프로젝트명androidappsrcmainres">[프로젝트명]/android/app/src/main/res</h4>
<hr>
<p>폴더 안에 들어있는 이미지들을 위에 만든 이미지들로 <strong>Replace</strong></p>
<ul>
<li>ic_launcher.png -&gt; 다운받은 앱 아이콘의 안드로이드 이미지</li>
<li>ic_launcher_round.png -&gt; round 이미지만 별도로 다운받은 이미지</li>
</ul>
<hr>
<pre><code class="language-javascript">&gt;&gt; 아래와 같은 구조로 이미지들을 대체 &lt;&lt;

 res/
      mipmap-xxxhdpi/
        ic_launcher.png
        ic_launcher_round.png
      mipmap-xxhdpi/
        ic_launcher.png
        ic_launcher_round.png
      mipmap-xhdpi/
        ic_launcher.png
        ic_launcher_round.png
      mipmap-hdpi/
        ic_launcher.png
        ic_launcher_round.png
      mipmap-mdpi/
        ic_launcher.png
        ic_launcher_round.png</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native Splash Screen 적용]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-Splash-Screen-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-Splash-Screen-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Thu, 18 Apr 2024 07:23:53 GMT</pubDate>
            <description><![CDATA[<h2 id="splash-screen">Splash Screen</h2>
<blockquote>
<p>일반적으로 애플리케이션 실행 시 페이지의 <strong>컨텐츠가 로딩되기 전까지 일시적으로 보여주는 화면</strong></p>
</blockquote>
<h3 id="설치">설치</h3>
<pre><code class="language-node">npm install --save react-native-splash-screen

yarn add react-native-splash-screen</code></pre>
<h3 id="사용법">사용법</h3>
<h4 id="apptsx">App.tsx</h4>
<pre><code class="language-javascript">
import SplashScreen from &quot;react-native-splash-screen&quot;;

useEffect(() =&gt; {
  setTimeout(() =&gt; {
    SplashScreen.hide();
  }, 1000);
});

--------

&gt;&gt; 스플래쉬 스크린 보이기
SplashScreen.show();

&gt;&gt; 스플래쉬 스크린 숨기기
SplashScreen.hide();

</code></pre>
<h3 id="android">Android</h3>
<h4 id="icon-추가">Icon 추가</h4>
<p>안드로이드에서는 <strong>android/app/src/main/res</strong> 에서 다음과 같이 디렉토리에 따라 파일을 추가한다.</p>
<ul>
<li>mipmap-mdpi -&gt; splash_icon.png</li>
<li>mipmap-hdpi -&gt; <a href="mailto:splash_icon@2x.png">splash_icon@2x.png</a></li>
<li>mipmap-xhdpi, mipmap-xxhdpi, mipmap-xxxhdpi -&gt; <a href="mailto:splash_icon@3x.png">splash_icon@3x.png</a></li>
</ul>
<p>추가 후, 모든 파일이름은 <strong>splash_icon.png</strong>로 수정한다.</p>
<h4 id="androidsettingsgradle">android/settings.gradle</h4>
<pre><code class="language-java">
include &#39;:app&#39;
includeBuild(&#39;../node_modules/react-native-gradle-plugin&#39;)

include &#39;:react-native-splash-screen&#39;
project(&#39;:react-native-splash-screen&#39;).projectDir = new File(rootProject.projectDir, &#39;../node_modules/react-native-splash-screen/android&#39;)
</code></pre>
<h4 id="androidappbuildgradle">android/app/build.gradle</h4>
<pre><code class="language-java">
dependencies {
  // ...
  implementation project(&#39;:react-native-splash-screen&#39;)
}
</code></pre>
<h4 id="androidappsrcmainreslayoutlaunch_screenxml">android/app/src/main/res/layout/launch_screen.xml</h4>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;RelativeLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:orientation=&quot;vertical&quot; android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;&gt;
    &lt;ImageView android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot; android:src=&quot;@drawable/launch_screen&quot; android:scaleType=&quot;centerCrop&quot; /&gt;
&lt;/RelativeLayout&gt;</code></pre>
<h4 id="androidappsrcmainresvaluescolorsxml">android/app/src/main/res/values/colors.xml</h4>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;resources&gt;
    &lt;color name=&quot;primary_dark&quot;&gt;#000000&lt;/color&gt;
&lt;/resources&gt;</code></pre>
<h4 id="mainactivityjava">MainActivity.java</h4>
<pre><code class="language-java">import org.devio.rn.splashscreen.SplashScreen;

// ...
@Override
protected void onCreate(Bundle savedInstanceState) {
  SplashScreen.show(this);
  super.onCreate(savedInstanceState);
}</code></pre>
<h4 id="androidappsrcmainresdrawablelaunch_screenpng">android/app/src/main/res/drawable/launch_screen.png</h4>
<blockquote>
<p>Splash Screen으로 사용할 이미지 (launch_screen.png) 를 넣어준다.</p>
</blockquote>
<h3 id="ios">IOS</h3>
<h4 id="cocoapod-실행">CocoaPod 실행</h4>
<pre><code>cd ios
pod insall</code></pre><p><strong>1. 루트에서 open ios/[project_name].xcworkspace을 실행하여 Xcode를 실행한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/dbc578a1-de90-4e5a-8afd-a1beadca1969/image.png" alt=""></p>
<p><strong>2. [project_name] &gt; [project_name] &gt; Imagex.xcassets 에서 + 를 누르고 Image Set 을 클릭하여 SplashIcon 을 입력한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/e297b4eb-02c2-4077-b89e-5e5171490cad/image.png" alt=""></p>
<blockquote>
<p>세가지 사이즈(300px, 600px @x2, 900px @x3)의 png 파일을 끌어넣을 수 있다.</p>
</blockquote>
<h4 id="background-설정">background 설정</h4>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/c18e95ab-db1e-4986-a8dc-4a66ee2b90dc/image.png" alt=""></p>
<blockquote>
<p>LaunchScreen.storyBoard 에서 기본적으로 설정되어있는 프로젝트 네임과 Powered by React Native 를 지우고 Background &gt; custom 에서 색상을 변경한다. 두번째 탭에서 코드로 변경할 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/fda7f283-e3a8-4676-9897-0727c75f860e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/3e9e1e5e-9208-4fb0-9117-557f6989e312/image.png" alt=""></p>
<h4 id="icon-추가-1">Icon 추가</h4>
<blockquote>
<p>상단의 + (Library) 를 클릭하고 Image를 검색하여 Image View를 추가한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/16bc53ce-9722-445c-9101-5566edf5f091/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/749c8401-2082-4de5-a10f-9ee4201024e6/image.png" alt=""></p>
<blockquote>
<p>SplashIcon를 입력하여 이미지를 선택하고 Content Mode 옵션은 Aspect Fit로 설정한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/132de24b-f31f-4929-b92c-60ea15b41bdc/image.png" alt=""></p>
<h4 id="중앙정렬">중앙정렬</h4>
<blockquote>
<p>디바이스에 상관없이 중앙 정렬하려면 Align 에서 Horizontally in container와 Vertically in container를 체크하여 추가한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/d01df4af-159e-40d9-8c00-638e10b64671/image.png" alt=""></p>
<h4 id="appdelegatem">AppDelegate.m</h4>
<pre><code class="language-swift">
#import &quot;AppDelegate.h&quot;

#import &lt;React/RCTBundleURLProvider.h&gt;
#import &lt;React/RCTRootView.h&gt;
#import &quot;RNSplashScreen.h&quot;  // 추가

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // ...other code

    [RNSplashScreen show];  // 추가
    // or
    //[RNSplashScreen showSplash:@&quot;LaunchScreen&quot; inRootView:rootView];
    return YES;
}

@end
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native Libraries]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-Libraries</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-Libraries</guid>
            <pubDate>Tue, 16 Apr 2024 03:03:16 GMT</pubDate>
            <description><![CDATA[<h2 id="react-query">React Query</h2>
<blockquote>
<p>[React Query 공식문서]
<a href="https://tanstack.com/query/latest/docs/framework/react/overview">https://tanstack.com/query/latest/docs/framework/react/overview</a></p>
</blockquote>
<h4 id="설치">설치</h4>
<pre><code class="language-javascript">npm install @tanstack/react-query

yarn add @tanstack/react-query</code></pre>
<hr>
<p>데이터를 가져오는 과정에는 일반적으로 많은 코드가 필요하다.
React Query를 사용하면 네트워크 요청을 할 때 작성하는 코드의 양을 줄일 수 있다.
훅을 사용하여 useQuery 이전에 작성해야 했던 모든 코드를 대체할 수 있다.
상태 변수를 선언하지 않고도 필요한 모든 데이터를 제공한다.
그럼에도 불구하고 데이터 검색을 더 쉽게 만드는 것은 React Query가 하는 일의 작은 부분만을 다루고 있다. 그 엄청난 힘은 우리가 만드는 요청을 캐시하는 능력에 있다. 결과적으로 이미 요청한 항목이 있으면 별도의 요청을 하지 않고 캐시에서 읽어오기만 하면 되는 경우가 많다. 이는 코드의 반복을 줄이고 API에 가해지는 부하를 줄이며 애플리케이션 관리를 간소화하기 때문에 매우 유용하다.</p>
<hr>
<h2 id="zustand">Zustand</h2>
<blockquote>
<p>[Zustand 공식문서]
<a href="https://zustand-demo.pmnd.rs/">https://zustand-demo.pmnd.rs/</a></p>
</blockquote>
<h4 id="설치-1">설치</h4>
<pre><code class="language-javascript">npm install zustand

yarn add zustand</code></pre>
<hr>
<p>Zustand는 독일어로 &#39;상태&#39;라는 뜻을 가졌고, 간결한 플럭스 원칙을 바탕으로 작고 빠르게 확장 가능한 상태 관리 라이브러리다.</p>
<p><strong>사용방법이 굉장히 간결하고 배우기 싶다.</strong></p>
<hr>
<h2 id="react-hook-form">React Hook Form</h2>
<blockquote>
<p>[React Hook Form 공식문서]
<a href="https://react-hook-form.com/">https://react-hook-form.com/</a></p>
</blockquote>
<h4 id="설치-2">설치</h4>
<pre><code class="language-javascript">npm install react-hook-form

yarn add react-hook-form</code></pre>
<hr>
<p>React Hook Form은 간단한 데이터 유효성 검사를 허용하는 간단한 후크 기반 라이브러리이다. 벤치마크에 따르면 다른 대안보다 훨씬 빠르다. Typescript로 작성되어 양식 값을 지원하기 위해 양식 데이터 유형을 작성하는데 도움이 된다. 이 라이브러리를 사용하면 양식에 오류가 없어져 렌더딩 시간이 영구적으로 단축된다. 또한 React의 상태 관리 라이브러리와 통합하여 사용가능하다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RN] React Native Packages]]></title>
            <link>https://velog.io/@sukeun_youn/RN-React-Native-Packages</link>
            <guid>https://velog.io/@sukeun_youn/RN-React-Native-Packages</guid>
            <pubDate>Sun, 14 Apr 2024 06:28:18 GMT</pubDate>
            <description><![CDATA[<h2 id="asyncstorage">AsyncStorage</h2>
<blockquote>
<p><strong>[Async_Storage 공식문서]</strong>
<a href="https://react-native-async-storage.github.io/async-storage/">https://react-native-async-storage.github.io/async-storage/</a></p>
</blockquote>
<h4 id="설치">설치</h4>
<pre><code class="language-node">npm install @react-native-async-storage/async-storage

yarn add @react-native-async-storage/async-storage</code></pre>
<hr>
<p>앱을 종류 후 다시 실행하였을 때, 데이터가 남아있지 않는 문제가 있을 수 있다.
이때, LocalStorage처럼 <strong>key-value 기반</strong>으로 로컬에 데이터를 저장할 수 있게 해주는 라이브러리를 사용한다. LocalStorage와 마찬가지로, <strong>문자열 데이터만 사용이 가능</strong>하다.
따라서, <strong>JSON.Stringfy</strong> 메서드와 <strong>JSON.parse</strong> 메서드의 사용이 필요하다.
하지만, AsyncStorage는 LocalStorage와는 다르게 비동기 처리가 필요하다.</p>
<hr>
<h4 id="사용법">사용법</h4>
<pre><code class="language-javascript">&gt;&gt; 1. Storing String Value

const storeData = async (value) =&gt; {
  try {
    await AsyncStorage.setItem(&#39;my-key&#39;, value);
  } catch (e) {
    // saving error
  }
};

&gt;&gt; 2. Storing Object Value

const storeData = async (value) =&gt; {
  try {
    const jsonValue = JSON.stringify(value);
    await AsyncStorage.setItem(&#39;my-key&#39;, jsonValue);
  } catch (e) {
    // saving error
  }
};

&gt;&gt; 3. Reading String Value

const getData = async () =&gt; {
  try {
    const value = await AsyncStorage.getItem(&#39;my-key&#39;);
    if (value !== null) {
      // value previously stored
    }
  } catch (e) {
    // error reading value
  }
};

&gt;&gt; 4. Reading Object Value

const getData = async () =&gt; {
  try {
    const jsonValue = await AsyncStorage.getItem(&#39;my-key&#39;);
    return jsonValue != null ? JSON.parse(jsonValue) : null;
  } catch (e) {
    // error reading value
  }
};</code></pre>
<h2 id="mmkv-storage">MMKV Storage</h2>
<h3 id="high-performant-localstorage--in-memory-storage">High Performant LocalStorage &amp; In-Memory Storage</h3>
<blockquote>
<p><strong>[MMKV 공식문서]</strong>
<a href="https://github.com/mrousavy/react-native-mmkv">https://github.com/mrousavy/react-native-mmkv</a></p>
</blockquote>
<h4 id="설치-1">설치</h4>
<pre><code class="language-javascript">yarn add react-native-mmkv
cd ios &amp;&amp; pod install</code></pre>
<h4 id="사용법-1">사용법</h4>
<pre><code class="language-javascript">&gt;&gt; 1. Create a new Instance

import { MMKV } from &#39;react-native-mmkv&#39;

// customize
export const storage = new MMKV({
  id: `user-${userId}-storage`,
  path: `${USER_DIRECTORY}/storage`,
  encryptionKey: &#39;hunter2&#39;
})

&gt;&gt; 2. GET

const username = storage.getString(&#39;user.name&#39;) // &#39;Marc&#39;
const age = storage.getNumber(&#39;user.age&#39;) // 21
const isMmkvFastAsf = storage.getBoolean(&#39;is-mmkv-fast-asf&#39;) // true

&gt;&gt; 3. SET

storage.set(&#39;user.name&#39;, &#39;Marc&#39;)
storage.set(&#39;user.age&#39;, 21)
storage.set(&#39;is-mmkv-fast-asf&#39;, true)

&gt;&gt; 4. Keys

// checking if a specific key exists
const hasUsername = storage.contains(&#39;user.name&#39;)

// getting all keys
const keys = storage.getAllKeys() // [&#39;user.name&#39;, &#39;user.age&#39;, &#39;is-mmkv-fast-asf&#39;]

// delete a specific key + value
storage.delete(&#39;user.name&#39;)

// delete all keys
storage.clearAll()

&gt;&gt; 5. Objects

const user = {
  username: &#39;Marc&#39;,
  age: 21
}

// Serialize the object into a JSON string
storage.set(&#39;user&#39;, JSON.stringify(user))

// Deserialize the JSON string into an object
const jsonUser = storage.getString(&#39;user&#39;) // { &#39;username&#39;: &#39;Marc&#39;, &#39;age&#39;: 21 }
const userObject = JSON.parse(jsonUser)

&gt;&gt; 6. Encryption

// encrypt all data with a private key
storage.recrypt(&#39;hunter2&#39;)

// remove encryption
storage.recrypt(undefined)

&gt;&gt; 7. Buffers

storage.set(&#39;someToken&#39;, new Uint8Array([1, 100, 255]))
const buffer = storage.getBuffer(&#39;someToken&#39;)
console.log(buffer) // [1, 100, 255]</code></pre>
<h2 id="encrypted-storage">Encrypted Storage</h2>
<blockquote>
<p><strong>[Encrypted_Storage 공식문서]</strong>
<a href="https://www.npmjs.com/package/react-native-encrypted-storage">https://www.npmjs.com/package/react-native-encrypted-storage</a></p>
</blockquote>
<h4 id="설치-2">설치</h4>
<pre><code>npm install react-native-encrypted-storage

yarn add react-native-encrypted-storage</code></pre><hr>
<p>일반적으로 사용하는 LocalStorage인 AsyncStorage 는 엑세스 토큰, 결제 정보 등 민감한 데이터를 저장하기엔 보안상 이상적이지 못하다. 
따라서, 위의 문제를 해결하기 위해 보안적으로 개선된 Encryted Storage를 따로 사용하는 것을 권장한다.</p>
<hr>
<h4 id="사용법-2">사용법</h4>
<pre><code class="language-javascript">import EncryptedStorage from &#39;react-native-encrypted-storage&#39;;

&gt;&gt; 1. Storing Value

async function storeUserSession() {
    try {
        await EncryptedStorage.setItem(
            &quot;user_session&quot;,
            JSON.stringify({
                age : 21,
                token : &quot;ACCESS_TOKEN&quot;,
                username : &quot;emeraldsanto&quot;,
                languages : [&quot;fr&quot;, &quot;en&quot;, &quot;de&quot;]
            })
        );

        // Congrats! You&#39;ve just stored your first value!
    } catch (error) {
        // There was an error on the native side
    }
}

&gt;&gt; 2. Retrieving a Value

async function retrieveUserSession() {
    try {   
        const session = await EncryptedStorage.getItem(&quot;user_session&quot;);

        if (session !== undefined) {
            // Congrats! You&#39;ve just retrieved your first value!
        }
    } catch (error) {
        // There was an error on the native side
    }
}

&gt;&gt; 3. Removing a Value

async function removeUserSession() {
    try {
        await EncryptedStorage.removeItem(&quot;user_session&quot;);
        // Congrats! You&#39;ve just removed your first value!
    } catch (error) {
        // There was an error on the native side
    }
}

&gt;&gt; 4. Clearing All Previously Saved Values

async function clearStorage() {
    try {
        await EncryptedStorage.clear();
        // Congrats! You&#39;ve just cleared the device storage!
    } catch (error) {
        // There was an error on the native side
    }
}

&gt;&gt; 5. Error Handling

async function removeUserSession() {
    try {
        await EncryptedStorage.removeItem(&quot;user_session&quot;);
    } catch (error) {
        // There was an error on the native side
        // You can find out more about this error by using the `error.code` property
        console.log(error.code); // ex: -25300 (errSecItemNotFound)
    }
}</code></pre>
<h2 id="flashlist">FlashList</h2>
<h3 id="fast--performant-react-native-list">Fast &amp; Performant React Native List</h3>
<blockquote>
<p><strong>[FlashList 공식문서]</strong> 
<a href="https://shopify.github.io/flash-list/">https://shopify.github.io/flash-list/</a></p>
<p><strong>(React-Native FlatList &gt; 성능 개선)</strong></p>
</blockquote>
<pre><code class="language-javascript">yarn add @shopify/flash-list
cd ios &amp;&amp; pod install</code></pre>
<h4 id="사용법-3">사용법</h4>
<pre><code class="language-javascript">import React from &quot;react&quot;;
import { View, Text, StatusBar } from &quot;react-native&quot;;
import { FlashList } from &quot;@shopify/flash-list&quot;;

const DATA = [
  {
    title: &quot;First Item&quot;,
  },
  {
    title: &quot;Second Item&quot;,
  },
];

const MyList = () =&gt; {
  return (
    &lt;FlashList
      data={DATA}
      renderItem={({ item }) =&gt; &lt;Text&gt;{item.title}&lt;/Text&gt;}
      estimatedItemSize={200}
    /&gt;
  );
};</code></pre>
<h2 id="dropdown-picker">DropDown Picker</h2>
<blockquote>
<p><strong>[DropDown_Picker 공식문서]</strong>
<a href="https://github.com/hossein-zare/react-native-dropdown-picker">https://github.com/hossein-zare/react-native-dropdown-picker</a>
<a href="https://hossein-zare.github.io/react-native-dropdown-picker-website/">https://hossein-zare.github.io/react-native-dropdown-picker-website/</a></p>
</blockquote>
<h4 id="설치-3">설치</h4>
<pre><code class="language-javascript">npm install react-native-dropdown-picker

yarn add react-native-dropdown-picker</code></pre>
<hr>
<p>React Native에서 DropDown을 구현할 때, 일반적으로 플랫폼 별로 제공하는 네이티브 Picker를 사용하는 경우가 많다보니, 크로스 플랫폼 개발 시에 UI의 통일성이 떨어지고, 디자인이 마음에 들지 않는 경우가 많다. 그렇다고 Picker를 직접 개발하거나 이를 위해 다른 UI 프레임워크를 적용하기에는 다소 부담이 있다. </p>
<p>스타일 커스터마이징도 자유롭고 다중 선택이나, 다크모드, 카테고리, 검색 등도 지원하고 있다.
또한 IOS와 Android 모두 거의 동일한 스타일과 결과물을 보이는 점이 마음에 든다.</p>
<hr>
<h4 id="사용법-4">사용법</h4>
<pre><code class="language-javascript">import DropDownPicker from &#39;react-native-dropdown-picker&#39;;

function App() {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState(null);
  const [items, setItems] = useState([
    {label: &#39;Apple&#39;, value: &#39;apple&#39;},
    {label: &#39;Banana&#39;, value: &#39;banana&#39;}
  ]);

  return (
    &lt;DropDownPicker
      open={open}
      value={value}
      items={items}
      setOpen={setOpen}
      setValue={setValue}
      setItems={setItems}
    /&gt;
  );
}</code></pre>
<h2 id="bottom-sheet-modal">Bottom Sheet Modal</h2>
<blockquote>
<p><strong>[Bottom Sheet 공식문서]</strong>
<a href="https://github.com/gorhom/react-native-bottom-sheet">https://github.com/gorhom/react-native-bottom-sheet</a>
<a href="https://ui.gorhom.dev/components/bottom-sheet/">https://ui.gorhom.dev/components/bottom-sheet/</a></p>
</blockquote>
<h4 id="설치-4">설치</h4>
<pre><code class="language-javascript">&gt;&gt; Dependencies

yarn add react-native-reanimated react-native-gesture-handler

&gt;&gt; Installation

yarn add @gorhom/bottom-sheet@^4</code></pre>
<h4 id="사용법-5">사용법</h4>
<pre><code class="language-javascript">import React, { useCallback, useMemo, useRef } from &#39;react&#39;;
import { View, Text, StyleSheet } from &#39;react-native&#39;;
import BottomSheet from &#39;@gorhom/bottom-sheet&#39;;

const App = () =&gt; {
  // ref
  const bottomSheetRef = useRef&lt;BottomSheet&gt;(null);

  // variables
  const snapPoints = useMemo(() =&gt; [&#39;25%&#39;, &#39;50%&#39;], []);

  // callbacks
  const handleSheetChanges = useCallback((index: number) =&gt; {
    console.log(&#39;handleSheetChanges&#39;, index);
  }, []);

  // renders
  return (
    &lt;View&gt;
      &lt;BottomSheet
        ref={bottomSheetRef}
        index={1}
        snapPoints={snapPoints}
        onChange={handleSheetChanges}
      &gt;
        &lt;View&gt;
          &lt;Text&gt;Awesome 🎉&lt;/Text&gt;
        &lt;/View&gt;
      &lt;/BottomSheet&gt;
    &lt;/View&gt;
  );
};

export default App;</code></pre>
<hr>
<p>가장 기본적인 BottomSheet를 가지고 안의 View를 직접 구현해도 되지만, 다음과 같은 여러 컴포넌트를 제공하므로 활용해보시는 것도 좋을 것 같습니다.</p>
<hr>
<ul>
<li>BottomSheetView</li>
<li>BottomSheetScrollView</li>
<li>BottomSheetFlatList</li>
<li>BottomSheetSectionList</li>
<li>BottomSheetVirtualizedList</li>
<li>BottomSheetBackdrop</li>
<li>BottomSheetFooter</li>
<li>BottomSheetTextInput</li>
</ul>
<pre><code class="language-javascript">&gt;&gt; BottomSheet처럼 지속적으로 동작하는 것이 아닌 Modal형태가 필요한 경우
&gt;&gt; BottomSheetModal 사용

import React, { useCallback, useMemo, useRef } from &#39;react&#39;;
import { View, Text, StyleSheet, Button } from &#39;react-native&#39;;
import {
  BottomSheetModal,
  BottomSheetModalProvider,
} from &#39;@gorhom/bottom-sheet&#39;;

const App = () =&gt; {
  // ref
  const bottomSheetModalRef = useRef&lt;BottomSheetModal&gt;(null);

  // variables
  const snapPoints = useMemo(() =&gt; [&#39;25%&#39;, &#39;50%&#39;], []);

  // callbacks
  const handlePresentModalPress = useCallback(() =&gt; {
    bottomSheetModalRef.current?.present();
  }, []);
  const handleSheetChanges = useCallback((index: number) =&gt; {
    console.log(&#39;handleSheetChanges&#39;, index);
  }, []);

  // renders
  return (
    &lt;BottomSheetModalProvider&gt;
      &lt;View&gt;
        &lt;Button
          onPress={handlePresentModalPress}
          title=&quot;Present Modal&quot;
          color=&quot;black&quot;
        /&gt;
        &lt;BottomSheetModal
          ref={bottomSheetModalRef}
          index={1}
          snapPoints={snapPoints}
          onChange={handleSheetChanges}
        &gt;
          &lt;View&gt;
            &lt;Text&gt;Awesome 🎉&lt;/Text&gt;
          &lt;/View&gt;
        &lt;/BottomSheetModal&gt;
      &lt;/View&gt;
    &lt;/BottomSheetModalProvider&gt;
  );
};

export default App;</code></pre>
<h2 id="toast-message">Toast Message</h2>
<blockquote>
<p><strong>[Toast Message 공식문서]</strong>
 <a href="https://github.com/calintamas/react-native-toast-message">https://github.com/calintamas/react-native-toast-message</a></p>
</blockquote>
<h4 id="설치-5">설치</h4>
<pre><code class="language-javascript">npm install -save react-native-toast-message

yarn add react-native-toast-message</code></pre>
<hr>
<p>Toast Message는 앱 내에서 사용자들에게 이벤트의 발생과 그 내용을 알려줄 수 있는 직관적인 UI 요소로 많은 앱에서 사용된다. 물론 구현 난이도 자체는 높지 않기 때문에 직접 구현하는 것도 좋은 방법이지만, 서비스의 핵심적인 기능은 아니다보니 적당한 라이브러리를 활용하면 시간을 절약할 수 있다.</p>
<hr>
<h4 id="사용법-6">사용법</h4>
<pre><code class="language-javascript">&gt;&gt; App.tsx에서 아래와 같은 구조로 &lt;Toast /&gt;를 추가

import Toast from &#39;react-native-toast-message&#39;;

export function App(props) {
  return (
    &lt;&gt;
      {/* ... React Navigation 등 */}
      &lt;Toast /&gt;
    &lt;/&gt;
  );
}</code></pre>
<pre><code class="language-javascript">&gt;&gt;  Toast Message를 보여주는 방법

import Toast from &#39;react-native-toast-message&#39;;
import { Button } from &#39;react-native&#39;

export function Foo(props) {
  const showToast = () =&gt; {
    Toast.show({
      type: &#39;success&#39;,
      text1: &#39;Hello&#39;,
      text2: &#39;This is some something 👋&#39;
    });
  }

  return (
    &lt;Button
      title=&#39;Show toast&#39;
      onPress={showToast}
    /&gt;
  )
}</code></pre>
<h2 id="magical-modal">Magical Modal</h2>
<blockquote>
<p><strong>[Magical Modal 공식문서]</strong>
 <a href="https://github.com/GSTJ/react-native-magic-modal">https://github.com/GSTJ/react-native-magic-modal</a></p>
</blockquote>
<h4 id="설치-6">설치</h4>
<pre><code class="language-javascript">npm install -save react-native-magic-modal

yarn add react-native-magic-modal</code></pre>
<hr>
<p> 아주 디테일한 부분까지 모달을 컨트롤 해야 하는 경우가 아니라면, 
 거의 이 라이브러리를 계속 활용하게 될 것 같다.</p>
<hr>
<h4 id="사용법-7">사용법</h4>
<pre><code class="language-javascript">&gt;&gt; Modal을 관리해 줄 Provider를 상위 컴포넌트에 아래와 같은 형태로 추가 &lt;&lt;

import { MagicModalPortal } from &#39;react-native-magic-modal&#39;;

export default function App() {
  return (
    &lt;SomeRandomProvider&gt;
      &lt;MagicModalPortal /&gt;  // &lt;-- On the top of the app component hierarchy
      &lt;Router /&gt; // Your app router or something could follow below
    &lt;/SomeRandomProvider&gt;
  );
}

&gt;&gt; magicModal.show() &amp;&amp; magicModal.hide() &lt;&lt;

import React from &#39;react&#39;;
import { View, Text, TouchableOpacity } from &#39;react-native&#39;;
import { MagicModalPortal, magicModal } from &#39;react-native-magic-modal&#39;;

const ConfirmationModal = () =&gt; (
  &lt;View&gt;
    &lt;TouchableOpacity onPress={() =&gt; magicModal.hide({ success: true })}&gt;
      &lt;Text&gt;Click here to confirm&lt;/Text&gt;
    &lt;/TouchableOpacity&gt;
  &lt;/View&gt;
);

const handleConfirmationFlow = async () =&gt; {
  const result = await magicModal.show(ConfirmationModal);
};

export const MainScreen = () =&gt; {
  return (
    &lt;View&gt;
      &lt;TouchableOpacity onPress={handleConfirmationFlow}&gt;
        &lt;Text&gt;Start the modal flow!&lt;/Text&gt;
      &lt;/TouchableOpacity&gt;
    &lt;/View&gt;
  );
};
</code></pre>
<h2 id="splash-screen">Splash Screen</h2>
<blockquote>
<p><strong>[Splash Screen 공식문서]</strong>
<a href="https://github.com/crazycodeboy/react-native-splash-screen">https://github.com/crazycodeboy/react-native-splash-screen</a></p>
</blockquote>
<h4 id="설치-7">설치</h4>
<pre><code class="language-javascript">
npm install -save react-native-splash-screen

yarn add react-native-splash-screen</code></pre>
<hr>
<p>RN는 기본적으로 Splash 스크린을 제공한다. 하지만 실제 앱을 구동하면 Splash 스크린이 너무 빨리 종료된다. 보통의 앱에서는 Splash 스크린을 표시하고 뒤에서 필요한 정보를 API를 통해 받아 온 후 Splash 스크린을 종료하여 자연스러운 사용자 경험을 제공하지만 RN에서 기본으로 제공하는 Splash 스크린은 스런 사용자 경험을 제공하기 어렵다.</p>
<hr>
<h4 id="사용법-android">사용법 (ANDROID)</h4>
<pre><code class="language-java">&gt;&gt; MainActivity.java(kt) 파일을 열고 아래와 같이 수정 &lt;&lt;

...
import android.os.Bundle; // 추가
import com.facebook.react.ReactActivity;
import org.devio.rn.splashscreen.SplashScreen; // 추가
...
public class MainActivity extends ReactActivity {
   @Override
    protected void onCreate(Bundle savedInstanceState) {
        SplashScreen.show(this);
        super.onCreate(savedInstanceState);
    }
    ...
}</code></pre>
<pre><code class="language-javascript">&gt;&gt; Android에서 사용할 Splash Screen 추가 &lt;&lt;
&gt;&gt; android/app/src/main/res/layout/launch_screen.xml 파일을 생성하고 아래 코드를 추가</code></pre>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;RelativeLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:orientation=&quot;vertical&quot; android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;&gt;
    &lt;ImageView android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot; 
               android:src=&quot;@drawable/launch_screen&quot; android:scaleType=&quot;centerCrop&quot; /&gt;
&lt;/RelativeLayout&gt;</code></pre>
<hr>
<p>준비된 splash 이미지들을 <strong>/android/app/src/main/res</strong> 폴더 안에 아래와 같이 넣는다. 생성된 이미지 파일을 res/의 적절한 하위 디렉터리에 배치하면 시스템에서 앱이 실행되는 기기의 픽셀 밀도에 따라 자동으로 알맞은 크기를 선택한다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/83a07dc1-1644-43d5-b6c8-89e3ce582a23/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB] 파일 업로드 종류]]></title>
            <link>https://velog.io/@sukeun_youn/SpringBoot-S3-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%A2%85%EB%A5%98</link>
            <guid>https://velog.io/@sukeun_youn/SpringBoot-S3-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%A2%85%EB%A5%98</guid>
            <pubDate>Sun, 24 Mar 2024 03:51:08 GMT</pubDate>
            <description><![CDATA[<p>참고) <a href="https://techblog.woowahan.com/11392/">https://techblog.woowahan.com/11392/</a></p>
<ol>
<li>Stream 업로드</li>
<li>Multipart 업로드</li>
<li>AWS Multipart 업로드</li>
</ol>
<h2 id="기본-세팅">기본 세팅</h2>
<pre><code class="language-java">dependencies {
    ...
    implementation(&quot;com.amazonaws:aws-java-sdk-s3:1.12.174&quot;)}</code></pre>
<blockquote>
<p>AWS SDK에서 제공하는 S3 업로드 인터페이스를 빈으로 등록</p>
</blockquote>
<pre><code class="language-java">@Configuration
class S3Config(
    @Value(&quot;\${aws.s3.accessKey}&quot;)
    private val accessKey: String,
    @Value(&quot;\${aws.s3.secretKey}&quot;)
    private val secretKey: String,
) {
    @Bean
    fun amazonS3Client(): AmazonS3 {
        return AmazonS3ClientBuilder.standard()
            .withCredentials(
                AWSStaticCredentialsProvider(BasicAWSCredentials(accessKey, secretKey))
            )
            .withRegion(Regions.AP_NORTHEAST_2)
            .build()
    }
}</code></pre>
<blockquote>
<p>사용되는 모든 AWS S3Client는 위에 정의된 빈으로 사용된다.</p>
</blockquote>
<hr>
<h2 id="stream-업로드">Stream 업로드</h2>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/6d22d4ab-7fe3-4c58-abaf-2967bd32f49d/image.jpg" alt=""></p>
<h3 id="stream-업로드-란">&quot;Stream 업로드&quot; 란?</h3>
<blockquote>
<p>HttpServletRequest의 InputStream을 이용하여 AWS S3에 다이렉트로 파일을 전송하는 방식</p>
</blockquote>
<h3 id="특징">특징</h3>
<blockquote>
<ul>
<li>모든 바이너리를 메모리에 로드하지 않는 이상 이미지 리사이징과 같은 전처리가 불가능하다.</li>
<li>업로드할 파일의 바이너리 전체를 Springboot을 실행하고 있는 서버의 디스크나 힙 메모리에 저장하지 않는다는 점이다.</li>
<li>1회 API 호출에 1개의 파일만을 업로드할 수 있다.</li>
</ul>
</blockquote>
<h3 id="사용-전-고려사항">사용 전 고려사항</h3>
<blockquote>
<ol>
<li>클라이언트 네트워크 환경, 클라우드 인스턴스 유형에 따라 업로드 속도 편차가 크기 때문에 운영환경과 동일한 인프라로 충분한 속도 테스트가 필요하다.</li>
<li>구조적으로 클라이언트에게 업로드 현황을 제공할 수 없다.
따라서 클라이언트가 기다릴 수 있을만큼 적당한 파일사이즈 제한이 필요하다.</li>
<li>대용량 파일을 업로드 중 오류가 발생하였을 때 전체 파일을 처음부터 다시 업로드해야하기 때문에 시간과 대역폭이 낭비될 수 있다.</li>
</ol>
</blockquote>
<br />

<h4 id="ex-약-1gb-크기의-파일을-업로드">EX. 약 1GB 크기의 파일을 업로드</h4>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/57cd0304-c22d-4262-b803-9c361e69a351/image.png" alt=""></p>
<h4 id="결과">결과</h4>
<blockquote>
<ul>
<li>메모리 사용량은 거의 없었다.</li>
<li>937MB 파일을 업로드하는데 16분정도 소요</li>
</ul>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/c7c366d1-b5a1-47d3-ac4c-9edf7568c088/image.png" alt=""></p>
<blockquote>
<p><strong>Stream 업로드 방식</strong>은 서버의 리소스가 한정적이고 작은 용량의 파일을 업로드할 때 효과적</p>
</blockquote>
<p>만약 프로덕트에서 Stream 업로드 방식을 채택한다면, 위에서 이야기한 것처럼 운영 환경과 동일한 인프라로 충분한 업로드 속도 테스트가 필요하다.</p>
<hr>
<h2 id="multipartfile-업로드">MultipartFile 업로드</h2>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/4abff65c-e028-467c-8d46-b741d308f72d/image.jpg" alt=""></p>
<h3 id="multipartfile-업로드-1">MultipartFile 업로드</h3>
<blockquote>
<p>Spring에서 제공하는 MultipartFile Interface를 이용하여 파일을 업로드하는 방식</p>
</blockquote>
<h3 id="multipartfile-사용-전-설정">MultipartFile 사용 전 설정</h3>
<pre><code class="language-java">spring:
  servlet:
    multipart:
      enabled: true # 멀티파트 업로드 지원여부 (default: true)
      file-size-threshold: 0B # 파일을 디스크에 저장하지 않고 메모리에 저장하는 최소 크기 (default: 0B)
      location: /users/charming/temp # 업로드된 파일이 임시로 저장되는 디스크 위치 (default: WAS가 결정)
      max-file-size: 100MB # 한개 파일의 최대 사이즈 (default: 1MB)
      max-request-size: 100MB # 한개 요청의 최대 사이즈 (default: 10MB)</code></pre>
<blockquote>
<p>클라이언트가 파일을 업로드했을 때 WAS(Tomcat)가 해당 파일을 임시 디렉터리에 저장한다.
여기서 임시 디렉터리에 저장된 파일은 힙 메모리가 아닌 Servlet Container Disk에 저장
요청 처리가 끝나면 임시 저장된 파일이 삭제</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/447f11bd-db5c-4655-8df5-e5f144f3a1f8/image.jpg" alt=""></p>
<blockquote>
<p>업로드된 파일의 크기가 file-size-threshold 값 이하라면 WAS가 임시파일을 생성하지 않고 파일 바이너리를 메모리에 다이렉트로 할당</p>
<p>파일 처리 속도는 더 빠르겠지만, 스레드가 작업을 수행하는 동안 부담이 될 수 있기 때문에 충분한 검토가 필요하다.</p>
</blockquote>
<p>Stream 업로드 및 MultipartFile 업로드 방식을 사용할 때 다수의 사용자로부터 동시에 요청이 들어올 경우, 서버의 스레드가 빠르게 소진될 위험이 있다. 이에 따라 스레드 풀 설정이 적절하지 않으면 스레드 고갈로 인해 타임아웃이 발생할 위험이 있다.</p>
<p>이러한 문제를 대비해 resilience4j의 bulkhead 패턴을 활용하여 동시에 처리될 수 있는 작업의 최대 수를 제한하고, 별도의 스레드 풀을 사용함으로써 다른 비즈니스 로직에 영향을 주지 않도록 방지할 수 있습니다. 또한 파일 업로드를 전담하는 서버를 별도로 분리하는 전략도 고려해볼 수 있습니다.</p>
<hr>
<h2 id="aws-multipart">AWS Multipart</h2>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/9909b43d-b682-4225-b421-e2c598147336/image.jpg" alt=""></p>
<h4 id="aws-multipart-1">AWS Multipart</h4>
<blockquote>
<p>AWS S3에서 제공하는 파일 업로드 방식
업로드할 파일을 작은 part로 나누어 각 부분을 개별적으로 업로드한다.
파일의 바이너리가 Springboot를 거치지 않고 AWS S3에 다이렉트로 업로드되기 때문에 서버의 부하를 고려하지 않아도 된다는 큰 장점이 있다.
만약 모든 part가 업로드 되었을 경우 AWS에서 하나의 객체로 조립하여 저장한다.
몇 개의 파트가 업로드되었는지 확인하여 위와 같이 사용자에게 업로드 진행사항을 제공할 수 있다.</p>
</blockquote>
<h4 id="1-multipart-업로드-시작">1. Multipart 업로드 시작</h4>
<p>멀티파트 업로드 시작(initiate-upload)을 요청하면 서버는 멀티파트 업로드에 대한 고유 식별자인 Upload ID를 응답합니다. 부분 업로드, 업로드 완료 또는 업로드 중단 요청 시 항상 Upload ID를 포함해야 하기 때문에 클라이언트는 이 값을 잘 저장해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[플로우 차트 (Flowchart)]]></title>
            <link>https://velog.io/@sukeun_youn/%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%B0%A8%ED%8A%B8-Flowchart</link>
            <guid>https://velog.io/@sukeun_youn/%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%B0%A8%ED%8A%B8-Flowchart</guid>
            <pubDate>Mon, 25 Sep 2023 11:56:46 GMT</pubDate>
            <description><![CDATA[<h2 id="1-flowchart순서도-란">1. Flowchart(순서도) 란?</h2>
<p>처리하고자 하는 문제를 분석하여 국제표준기구(ISO : International Standardization Organization)에서 지정한 기호와 흐름선을 활용, 프로세스의 처리 순서를 포함한 단계 간의 상호관계를 알기 쉽게 나타낸 그림</p>
<ul>
<li>원고의 초안, 건축의 설계도와 같은 프로그래밍의 기초가 된다.</li>
<li>타인에게 전달, 크로스랭귀징, 유지보수 등에 기반이 되는 중요한 요소</li>
</ul>
<blockquote>
<p>논리의 흐름을 특정한 순서도 기호 (flow chart symbol) 를 사용하여 도식적으로 표현한 것</p>
</blockquote>
<h2 id="2-순서도-차트의-종류">2. 순서도 차트의 종류</h2>
<h3 id="1-시스템-순서도">1) 시스템 순서도</h3>
<ul>
<li>어떤 작업을 하는지 알려주는 순서도로 자료가 정보로 변환되는 과정을 컴퓨터가 처리하는 과정, 데이터의 흐름을 중심으로 도식화한 것</li>
<li>데이터 흐름을 중심으로 작성하는 특징으로 인해 데이터가 처리되는 작업 단위로 나타내고 데이터가 변환되는 매체들을 표현하며 프로그램 논리는 작성하지 않는다.</li>
<li>작업 내용을 총괄적으로 검토하고 프로그래밍 작업과 연결해줄 때, 사용하는 순서도이다.</li>
</ul>
<h3 id="2-프로그램-순서도">2) 프로그램 순서도</h3>
<ul>
<li>프로그램 순서도는 작업을 어떤 식으로 하는지 표시해 주는 순서도로 처리 단위 하나하나 단위로 작성하게 되며 순서도 설명의 세밀도에 따라 개략 순서도와 상세 순서도로 나뉜다</li>
<li>프로그램을 작성할 때 일반적으로 쓰는 순서도이다.</li>
</ul>
<h4 id="2-1-일반-순서도">2-1) 일반 순서도</h4>
<ul>
<li>하나의 업무를 전체적, 종합적으로 나타낸 순서도로 해당 직업의 진행 순서를 표시한다.</li>
<li>프로그램 작성 시 개략 순서도를 작성하고 논리적으로 이상이 없는지 검토하게 되면 이어서 <strong>상세 순서도</strong>를 작성한다.</li>
</ul>
<h4 id="2-2-상세-순서도">2-2) 상세 순서도</h4>
<ul>
<li>프로그램 내부를 상세히 나타내는 순서도로 컴퓨터의 모든 조작과 자료의 이동 과정을 순서대로 나타내 그대로 코딩할 수 있도록 상세하게 작성한 순서도</li>
<li>프로그램 작성 시 최종 검토 자료로 쓰게 됨
<img src="https://velog.velcdn.com/images/sukeun_youn/post/429b8dbb-4ce8-4de3-abe3-8d8945af43fc/image.png" alt=""></li>
</ul>
<h3 id="기호-설명">기호 설명</h3>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/252a157c-7f08-4de9-8e67-0ec0e90d368c/image.png" alt=""></p>
<h2 id="3-순서도-작성-시-주의점">3. 순서도 작성 시 주의점</h2>
<ul>
<li>기호 내에는 최대한 간략하게 내용을 기재하여 <code>**가독성을 향상**</code>시킨다</li>
<li>비교/판단 기호 사용 시 입/출력은 반드시 <strong><code>하나</code></strong>여야 하며, 결과는 Yes or No여야 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sukeun_youn/post/3a86eee8-5c9e-4281-b665-c507c7a5e868/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>