<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>blackbearwow</title>
    <link>https://owwowo.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 20:05:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>blackbearwow</managingEditor>
    <image>
      <title>blackbearwow</title>
      <url>https://tistory1.daumcdn.net/tistory/4106079/attach/ab2b0b1cc931435caf3e524b9f5ec7e0</url>
      <link>https://owwowo.tistory.com</link>
    </image>
    <item>
      <title>Spring Security - jwt 인증</title>
      <link>https://owwowo.tistory.com/321</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;accessToken, refreshToken, refreshTokenRotation을 활용하는 방법을 사용하겠다. refreshToken은 db에 저장해둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 추가(build.gradle)&lt;/p&gt;
&lt;pre id=&quot;code_1771467836228&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	// jwt
	implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0'
	runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. SecurityFilterChain 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1771468246983&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final JWTUtil jwtUtil;
    private final AuthenticationConfiguration authenticationConfiguration;
    private final RefreshTokenRepository refreshTokenRepository;
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class)
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshTokenRepository), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(session -&amp;gt; session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;formLogin과 httpBasic을 disable하고, session을 stateless정책으로 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UsernamePasswordAuthenticationFilter는 formLogin에 의해 만들어지는 필터인데, 우리는 formLogin을 disable했다. 이 필터 대신 직접 만든 LoginFilter를 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginFilter전에 JWTFilter를 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. JWTUtil 클래스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt 생성, 검증하는 클래스이다.&lt;/p&gt;
&lt;pre id=&quot;code_1771483298553&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// jwt 생성, 검증 클래스
@Component
public class JWTUtil {
    private final SecretKey secretKey;
    public final Long accessTokenExpiredMs = 1000*60*60L;
    public final Long refreshTokenExpiredMs = 1000*60*60*24L;
    public JWTUtil(@Value(&quot;${spring.jwt.secret}&quot;)String secret) {
        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }
    public Long getId(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(&quot;id&quot;, Long.class);
    }
    public String getUserId(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(&quot;userid&quot;, String.class);
    }
    public String getRole(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(&quot;role&quot;, String.class);
    }
    // 사실 ExpiredJwtException이 터지므로 사용할 필요 없음
    public Boolean isExpired(String token) {
        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }
    public String createJwtAccessToken(Long id, String userid, MemberRole memberRole) {
        return Jwts.builder()
                .claim(&quot;id&quot;, id)
                .claim(&quot;userid&quot;, userid)
                .claim(&quot;role&quot;, memberRole.name())
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + accessTokenExpiredMs))
                .signWith(secretKey)
                .compact();
    }
    public String createJwtRefreshToken(Long id, String userid, MemberRole memberRole) {
        return Jwts.builder()
                .claim(&quot;id&quot;, id)
                .claim(&quot;userid&quot;, userid)
                .claim(&quot;role&quot;, memberRole.name())
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + refreshTokenExpiredMs))
                .signWith(secretKey)
                .compact();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.properties에 시크릿 키 값을 추가하자&lt;/p&gt;
&lt;pre id=&quot;code_1771483346938&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#jwt 시크릿 키
spring.jwt.secret=e43dba462b2ecde9bae1aa4690f1fe8d068f5c88149e21fba092a77a2ecfa7e7&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. LoginFilter 클래스&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UsernamePasswordAuthenticationFilter를 상속받아 LoginFilter클래스를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginFilter클래스의 역할은 아이디비번 맞는지 확인, 맞으면 jwtUtil로 accessToken과 refreshToken을 발급한다&lt;/p&gt;
&lt;pre id=&quot;code_1771483459722&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
public class JWTFilter extends OncePerRequestFilter {
    private final JWTUtil jwtUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 로그인이나 재발급 경로는 토큰 검사를 하지 않고 바로 다음 필터로 넘김
        String requestURI = request.getRequestURI();
        if (requestURI.equals(&quot;/api/member/login&quot;) || requestURI.equals(&quot;/api/reissue&quot;)) {
            filterChain.doFilter(request, response);
            return;
        }
        String authorization = request.getHeader(&quot;authorization&quot;);
        // 헤더 검증. 토큰이 아예 없는 경우 -&amp;gt; 익명 사용자이므로 통과
        if(authorization == null || !authorization.startsWith(&quot;Bearer &quot;)) {
            filterChain.doFilter(request, response);
            return;
        }
        String token = authorization.split(&quot; &quot;)[1];
        try {
            // 소멸 시간 검증은 따로 안해도 된다. 예외가 터진다.
            // 토큰에서 값 획득
            Long id = jwtUtil.getId(token);
            String userid = jwtUtil.getUserId(token);
            MemberRole memberRole = MemberRole.valueOf(jwtUtil.getRole(token));

            // 세션에 저장할 정보 만들기
            Member member = new Member();
            member.setForStatelessSession(id, userid, memberRole);
            List&amp;lt;GrantedAuthority&amp;gt; authorities = List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + member.getRole().toString()));
            MemberContext memberContext = new MemberContext(member, authorities);
            // 스프링 시큐리티 인증 토큰 생성
            Authentication authToken = new UsernamePasswordAuthenticationToken(memberContext, null, memberContext.getAuthorities());
            // 세션에 사용자 등록
            SecurityContextHolder.getContext().setAuthentication(authToken);

            filterChain.doFilter(request, response);
        } catch(ExpiredJwtException e) {
            log.info(&quot;jwt 토큰 시간 만료&quot;);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setCharacterEncoding(&quot;UTF-8&quot;);
            response.setContentType(&quot;application/json&quot;);
            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;access token expired\&quot;}&quot;);
            // 여기서 filterChain.doFilter를 호출하지 않고 끝낸다. 익명 사용자와 jwt토큰 만료 사용자를 구분하기 위해.
        } catch (SignatureException | MalformedJwtException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setCharacterEncoding(&quot;UTF-8&quot;);
            response.setContentType(&quot;application/json&quot;);
            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;유효하지 않은 토큰\&quot;}&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. JWTFilter&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OncePerRequestFilter를 상속해 JWTFilter를 만든다. 요청당 한번씩 실행되는 필터다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtFilter이 역할은 jwt 확인과정이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더에서&amp;nbsp;jwt토큰이&amp;nbsp;없으면&amp;nbsp;익명이니&amp;nbsp;진행 &lt;br /&gt;토큰이&amp;nbsp;있다면&amp;nbsp; &lt;br /&gt;1.&amp;nbsp;유효하지&amp;nbsp;않다면&amp;nbsp;401 &lt;br /&gt;2.&amp;nbsp;expired되었다면&amp;nbsp;401 &lt;br /&gt;3.&amp;nbsp;유효하다면&amp;nbsp;요청동안&amp;nbsp;유지되는&amp;nbsp;세션&amp;nbsp;생성&amp;nbsp;후&amp;nbsp;진행&lt;/p&gt;
&lt;pre id=&quot;code_1771483522038&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
public class JWTFilter extends OncePerRequestFilter {
    private final JWTUtil jwtUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 로그인이나 재발급 경로는 토큰 검사를 하지 않고 바로 다음 필터로 넘김
        String requestURI = request.getRequestURI();
        if (requestURI.equals(&quot;/api/member/login&quot;) || requestURI.equals(&quot;/api/reissue&quot;)) {
            filterChain.doFilter(request, response);
            return;
        }
        String authorization = request.getHeader(&quot;authorization&quot;);
        // 헤더 검증. 토큰이 아예 없는 경우 -&amp;gt; 익명 사용자이므로 통과
        if(authorization == null || !authorization.startsWith(&quot;Bearer &quot;)) {
            filterChain.doFilter(request, response);
            return;
        }
        String token = authorization.split(&quot; &quot;)[1];
        try {
            // 소멸 시간 검증은 따로 안해도 된다. 예외가 터진다.
            // 토큰에서 값 획득
            Long id = jwtUtil.getId(token);
            String userid = jwtUtil.getUserId(token);
            MemberRole memberRole = MemberRole.valueOf(jwtUtil.getRole(token));

            // 세션에 저장할 정보 만들기
            Member member = new Member();
            member.setForStatelessSession(id, userid, memberRole);
            List&amp;lt;GrantedAuthority&amp;gt; authorities = List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + member.getRole().toString()));
            MemberContext memberContext = new MemberContext(member, authorities);
            // 스프링 시큐리티 인증 토큰 생성
            Authentication authToken = new UsernamePasswordAuthenticationToken(memberContext, null, memberContext.getAuthorities());
            // 세션에 사용자 등록
            SecurityContextHolder.getContext().setAuthentication(authToken);

            filterChain.doFilter(request, response);
        } catch(ExpiredJwtException e) {
            log.info(&quot;jwt 토큰 시간 만료&quot;);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setCharacterEncoding(&quot;UTF-8&quot;);
            response.setContentType(&quot;application/json&quot;);
            // 토큰 만료는 다른 401에러와 구분하기 위해 code를 부여한다.
            response.getWriter().write(&quot;{\&quot;code\&quot;:\&quot;TOKEN_EXPIRED\&quot;, \&quot;message\&quot;:\&quot;access token expired\&quot;}&quot;);
            // 여기서 filterChain.doFilter를 호출하지 않고 끝낸다. 익명 사용자와 jwt토큰 만료 사용자를 구분하기 위해.
        } catch (SignatureException | MalformedJwtException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setCharacterEncoding(&quot;UTF-8&quot;);
            response.setContentType(&quot;application/json&quot;);
            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;유효하지 않은 토큰\&quot;}&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. RefreshToken&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RefreshToken 엔티티를 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1771492175703&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@NoArgsConstructor
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne
    private Member member;
    @Column(nullable = false, length = 500)
    private String tokenValue;
    @NotNull
    private LocalDateTime expirationDate;

    public RefreshToken(Member member, String tokenValue, Long expiredMs) {
        this.member = member;
        this.tokenValue = tokenValue;
        this.expirationDate = LocalDateTime.now().plus(Duration.ofMillis(expiredMs));
    }
    // 토큰 갱신 시 값만 변경하기 위한 메서드
    public void updateToken(String newToken, Long expiredMs) {
        this.tokenValue = newToken;
        this.expirationDate = LocalDateTime.now().plus(Duration.ofMillis(expiredMs));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;RefreshTokenRepository&lt;/b&gt;도 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1771492219908&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface RefreshTokenRepository extends JpaRepository&amp;lt;RefreshToken, Long&amp;gt; {
    Optional&amp;lt;RefreshToken&amp;gt; findByMember(Member member);
    Boolean existsByMember(Member member);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. ReissueController&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 브라우저에서 401에러를 받았을 때, refresh토큰으로 새 access토큰을 받는 api를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReissueController는 refreshToken을 이용해 accessToken을 재발급받는 신청을 받는다.&lt;br /&gt;refresh토큰을&amp;nbsp;검사해 &lt;br /&gt;1)&amp;nbsp;refresh토큰이&amp;nbsp;만료되었다면&amp;nbsp;400 &lt;br /&gt;2)&amp;nbsp;유효하지&amp;nbsp;않다면&amp;nbsp;400 &lt;br /&gt;3)&amp;nbsp;유효하다면&amp;nbsp;accessToken재발급,&amp;nbsp;refresh토큰&amp;nbsp;교체&lt;/p&gt;
&lt;pre id=&quot;code_1771492355903&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
public class ReissueController {
    private final JWTUtil jwtUtil;
    private final RefreshTokenRepository refreshTokenRepository;
    private final MemberRepository memberRepository;

    @PostMapping(&quot;/api/reissue&quot;)
    public String reissue(HttpServletRequest request, HttpServletResponse response) {

        // 1. Get refresh token from cookies
        String refresh = null;
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(&quot;refreshToken&quot;)) {
                    refresh = cookie.getValue();
                }
            }
        }

        // 2. 검증: 쿠키에 토큰이 없는 경우
        if (refresh == null) {
            throw new BadRequestException(&quot;refresh token null&quot;);
        }

        try {
            // 4. DB에 저장되어 있는지 확인 (가장 중요한 보안 단계)
            Long member_id = jwtUtil.getId(refresh);
            Member member = memberRepository.findById(member_id).orElseThrow(()-&amp;gt;new BadRequestException(&quot;멤버를 찾을 수 없음&quot;));
            Boolean isExist = refreshTokenRepository.existsByMember(member);
            if (!isExist) {
                log.info(&quot;refresh token이 존재하지 않는다잉 {} {}&quot;, isExist, member.getId());
                throw new BadRequestException(&quot;해당하는 refresh token이 존재하지 않습니다~!!&quot;);
            }

            // 5. 새로운 JWT 발급
            // 실제 운영 시에는 id와 role 정보도 토큰에서 추출하거나 DB에서 조회해야 합니다.
            String newAccess = jwtUtil.createJwt(member.getId(), member.getUserid(), member.getRole(), 60*60*1000L); // 1시간

            // 6. 응답 헤더에 설정
            response.setHeader(&quot;authorization&quot;, &quot;Bearer &quot; + newAccess);
        } catch (ExpiredJwtException e) {
            // 만료 체크
            throw new BadRequestException(&quot;refresh 토큰이 만료되었습니다. 다시 로그인해주세요&quot;);
        }
        return &quot;refresh토큰으로 새 accesstoken 발급 완료&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7. 클라이언트 요청 부분&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1771493381011&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import axios from &quot;axios&quot;;

const client = axios.create();

// 요청 인터셉터: 모든 요청에 토큰 부착
client.interceptors.request.use((config) =&amp;gt; {
  const token = localStorage.getItem(&quot;authorization&quot;);
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
// 응답 인터셉터: 401에러(만료) 발생 시 처리
client.interceptors.response.use(
  (response) =&amp;gt; response, // 성공시 그대로 반환
  async (error) =&amp;gt; {
    const originalRequest = error.config;
    // 401에러이고, 재시도한 적이 없는 요청일 때
    if (error.response.status === 401 &amp;amp;&amp;amp; !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        // 서버의 재발급 엔드포인트 호출 /api/reissue
        // 이때는 accessToken을 보내면 안된다.
        const res = await axios.post(&quot;/api/reissue&quot;);
        if (res.status === 200) {
          const newAccessToken = res.headers[&quot;authorization&quot;];
          // 새 토큰 저장
          localStorage.setItem(
            &quot;authorization&quot;,
            newAccessToken.replace(&quot;Bearer &quot;, &quot;&quot;),
          );
          // 2. 실패했던 원래 요청의 헤더를 새 토큰으로 교체
          originalRequest.headers.Authorization = newAccessToken;
          // 3. 원래 요청 다시 보내기
          return client(originalRequest);
        }
      } catch (reissueError) {
        // 리프레시 토큰도 만료되었거나 오류가 난 경우 -&amp;gt; 로그아웃 처리
        localStorage.removeItem(&quot;authorization&quot;);
        return Promise.reject(reissueError);
      }
    }
    return Promise.reject(error);
  },
);

export default client;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://www.youtube.com/playlist?list=PLJkjrxxiBSFCcOjy0AAVGNtIa08VLk1EJ&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/playlist?list=PLJkjrxxiBSFCcOjy0AAVGNtIa08VLk1EJ&lt;/a&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/321</guid>
      <comments>https://owwowo.tistory.com/321#entry321comment</comments>
      <pubDate>Thu, 19 Feb 2026 18:38:47 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security - SecurityConfig 클래스</title>
      <link>https://owwowo.tistory.com/320</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티는 다양한 인증과 보안 기능을 제공하는 도구이다. 단순히 개발자의 코드를 줄여주는 것이 아니다. 보안 취약점이 계속 보완된 검증된 보안, 보안 로직과 비즈니스 로직을 분리해 코드가 깔끔해지고, 간단한 로그인에서 jwt나 oauth등으로의 확장도 편하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 추가하기&lt;/p&gt;
&lt;pre id=&quot;code_1770690596208&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. SecurityFilterChain&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티는 http요청이 controller에 도달하기 전, 필터들을 거치게 만든다. 이 필터들을 로그인 했는지, 권한은 있는지, 해킹 시도는 아닌지 등을 검사한다.&lt;/p&gt;
&lt;pre id=&quot;code_1770776205960&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -&amp;gt; csrf.disable()) // 테스트를 위해 일단 csrf비활성화
                .formLogin(login -&amp;gt; login
                        .loginProcessingUrl(&quot;/api/member/login&quot;)
                        .usernameParameter(&quot;userid&quot;)
                        .successHandler(((request, response, authentication) -&amp;gt; {
                            response.setStatus(HttpServletResponse.SC_OK);
                            response.setCharacterEncoding(&quot;UTF-8&quot;);
                            response.setContentType(&quot;application/json&quot;);
                            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;로그인 성공\&quot;}&quot;);
                        }))
                        .failureHandler(((request, response, exception) -&amp;gt; {
                            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                            response.setCharacterEncoding(&quot;UTF-8&quot;);
                            response.setContentType(&quot;application/json&quot;);
                            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;로그인 실패: &quot;+exception.getMessage()+&quot;\&quot;}&quot;);
                        }))
                )
                .logout(logout -&amp;gt; logout
                        .logoutUrl(&quot;/api/member/logout&quot;)
                        .logoutSuccessHandler(((request, response, authentication) -&amp;gt; {
                            response.setStatus(HttpServletResponse.SC_OK);
                            response.setCharacterEncoding(&quot;UTF-8&quot;);
                            response.setContentType(&quot;application/json&quot;);
                            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;로그아웃 성공\&quot;}&quot;);
                        }))
                        .deleteCookies(&quot;JSESSIONID&quot;)
                )
                .authorizeHttpRequests(auth -&amp;gt; auth
                        .requestMatchers(HttpMethod.GET, &quot;/api/member&quot;).authenticated()
                        .requestMatchers(HttpMethod.PUT, &quot;/api/member&quot;).authenticated()
                        .requestMatchers(HttpMethod.DELETE, &quot;/api/member&quot;).authenticated()
                        .requestMatchers(HttpMethod.POST, &quot;/api/category&quot;).authenticated()
                        .requestMatchers(HttpMethod.DELETE, &quot;/api/category&quot;).authenticated()
                        .anyRequest().permitAll()
                )
                .exceptionHandling(exception -&amp;gt; exception
                        .authenticationEntryPoint(((request, response, authException) -&amp;gt; {
                            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                            response.setCharacterEncoding(&quot;UTF-8&quot;);
                            response.setContentType(&quot;application/json&quot;);
                            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;인증이 필요합니다\&quot;}&quot;);
                        }))
                        .accessDeniedHandler(((request, response, accessDeniedException) -&amp;gt; {
                            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                            response.setCharacterEncoding(&quot;UTF-8&quot;);
                            response.setContentType(&quot;application/json&quot;);
                            response.getWriter().write(&quot;{\&quot;message\&quot;:\&quot;허가되지 않은 권한입니다\&quot;}&quot;);
                        }))
                );
        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. csrf&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssr이면 세션이든 jwt든 설정해줘야 한다. 둘다 쿠키를 이용하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thymeleaf를 사용하고 th:action속성을 사용한다면 자동으로 hidden필드가 추가되어 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;csr이면 세션을 사용하면 설정해줘야 한다. 쿠키에 저장하는 것이 아닌 jwt를 사용하면 disable해도 된다. 토큰이 로컬(또는 세션)스토리지에 저장되고, 요청시 직접 헤더를 만들어야 하기 때문이다. 하지만 완벽한 보안은 httpOnly쿠키에 저장하고, csrf를 설정하는것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.2. formLogin&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증과 관련된 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loginPage는 사용자가 로그인이 필요할 때, 어떤 페이지를 보여줄지 지정해주는 것이다. 이 값을 설정하지 않으면 시큐리티가 제공하는 투박한 기본적인 로그인 창이 뜬다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loginProcessingUrl은 지정된 url로 post요청이 올 때, 컨트롤러로 가지 않고 시큐리티 필터에서 가로채 로그인을 진행시킨다. 이 기능을 사용하려면 유저 서비스에서 UserDetailsService를 구현해 loadUserByUsername메소드를 오버라이드해야한다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1771293826832&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public void join(MemberRequestDTO memberRequestDTO) {
        Member member = new Member();
        member.setUsername(memberRequestDTO.getUsername());
        member.setPassword(passwordEncoder.encode(memberRequestDTO.getPassword()));
        member.setRole(UserRole.USER);

        memberRepository.save(member);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByUsername(username).orElseThrow();
        return User.builder()
                .username(member.getUsername())
                .password(member.getPassword())
                .roles(member.getRole().name())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션에 username, password, roles말고도 다른 정보도 저장해놓고 싶다면 커스텀UserDetails를 만들어 사용하면 된다&lt;/p&gt;
&lt;pre id=&quot;code_1771328002238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class MemberContext extends User {
    private final Long id;
    private final MemberRole memberRole;
    private final Member member;
    public MemberContext(Member member, Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities) {
        super(member.getUserid(), member.getPassword(), authorities);
        this.id = member.getId();
        this.memberRole = member.getRole();
        this.member = member;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1771328041075&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info(&quot;username: {}&quot;, username);
        Member member = memberRepository.findByUserid(username).orElseThrow();
        //Role이 있다면 추가
        List&amp;lt;GrantedAuthority&amp;gt; authorities = List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + member.getRole().toString()));
        return new MemberContext(member, authorities);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법으로 로그인에 성공하면, SecurityContextHolder에 유저 정보가 저장된다. 이 정보를 읽고싶다면 @AuthenticationPrincipal을 사용하면 된다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rjM1I/dJMcabXsTpq/lIdjbcEEJhYoKDKKoSmaXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rjM1I/dJMcabXsTpq/lIdjbcEEJhYoKDKKoSmaXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rjM1I/dJMcabXsTpq/lIdjbcEEJhYoKDKKoSmaXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrjM1I%2FdJMcabXsTpq%2FlIdjbcEEJhYoKDKKoSmaXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;486&quot; height=&quot;179&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre id=&quot;code_1771326582704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping
    public MemberResponseDTO getMemberInfo(@AuthenticationPrincipal User user) {
        return new MemberResponseDTO(memberService.getMemberByUser(user));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@AuthenticationPrincipal은 SecurityContextHolder.getContext().getAuthentication().getPrincipal()한 값과 같다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;usernameParameter로 html의 name값을 지정해줄 수 있다. 기본적으로 username이지만 userid등으로 지정 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;successHandler는 로그인에 성공했을 시 동작을 정의한다. 기본적으로 메인 페이지로 리다이렉트하는데, csr방식이면 커스터마이징이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;failureHandler는 로그인이 실패시 동작을 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.3. logout&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logoutUrl은 어떤 url로 요청이 들어오면 로그아웃을 할지 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logoutSuccessHandler는 로그아웃이 성공했을 때 실행될 핸들러이다. csr방식일 때 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logoutSuccessUrl은 로그아웃이 성공했을 때 리다이렉트 할 url이다. ssr방식일 때 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deleteCookies는 삭제할 쿠키 이름을 지정한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.4. authorizeHttpRequests&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.requestMatchers().permitAll() =&amp;gt; 매칭되는 것은 허용된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.requestMatchers().authenticated() =&amp;gt; 매칭되는 것은 인증되어야 한다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;.anyRequest().authenticated() =&amp;gt; 모든 요청은 인증되어야 한다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;.anyRequest().permitAll() =&amp;gt; 모든 요청은 허용된다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞에는 .requestMatchers()와 .anyRequest()가 올 수 있다. 각각 매칭되는 것, 모든 요청을 의미한다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;뒤에는 .permitAll() .authenticated() .denyAll() .hasRole() .hasAnyRole() .hasAuthority() .hasAnyAuthority()등이 올 수 있다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.5. exceptionHandling&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;csr이라면 authorizeHttpRequests에서 거부된 응답을 커스터마이징 해야한다. 기본은 /로 리다이렉션 동작이기 때문.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;authenticationEntryPoint는 .authenticated()로 인해 거부된 응답을 커스터마이징한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;accessDeniedHandler는 .hasRole() .hasAnyRole()로 인해 거부된 응답을 커스터마이징한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. PasswordEncoder - 패스워드 암호화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 Bean 등록하기&lt;/p&gt;
&lt;pre id=&quot;code_1770774041090&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity // 스프링 시큐리티 활성화
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BCrypt는 암호화 할 때마다 랜덤으로 salt값을 추가해 암호화 하기 때문에, 같은 암호더라도 다른 결과값이 나오게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멤버 서비스에서 PasswordEncoder를 주입받아 encode메소드를 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1770774204208&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MemberService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    public void join(Member member) {
        // 비밀번호를 암호화해서 저장!
        String encodedPw = passwordEncoder.encode(member.getPassword());
        member.setPassword(encodedPw);
        memberRepository.save(member);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 할 때는 matches메소드를 사용하면 편하다.&lt;/p&gt;
&lt;pre id=&quot;code_1770774406164&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean login(String rawPassword, String encodedPassword) {
    // 입력한 비번과 DB의 암호화된 비번이 맞는지 확인
    return passwordEncoder.matches(rawPassword, encodedPassword);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 formLogin의 loginProcessingUrl필터를 사용하면 matches를 사용할 일이 없다. 회원가입할 때만 encode하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. 그런데 솔트값을 매번 랜덤으로 주면 회원가입 할 때와 로그인 할 때 인코드 된 값이 달라지는 거 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 회원가입 할 때 인코드 된 결과값의 일부가 솔트값이 들어가있다. 이 솔트값으로 로그인 할 때 비밀번호를 인코드하는 것이다.&lt;/p&gt;</description>
      <category>Java</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/320</guid>
      <comments>https://owwowo.tistory.com/320#entry320comment</comments>
      <pubDate>Thu, 19 Feb 2026 11:00:11 +0900</pubDate>
    </item>
    <item>
      <title>postgreSQL 설치 &amp;amp; 설정</title>
      <link>https://owwowo.tistory.com/319</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Windows 환경&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1. 설치&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.enterprisedb.com/downloads/postgres-postgresql-downloads&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.enterprisedb.com/downloads/postgres-postgresql-downloads&lt;/a&gt; 에 접속 후 원하는 플랫폼 선택해 다운로드.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1017&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckNi4B/dJMcaaRL1rl/e6nP8uayeUY2sXXWN5lf0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckNi4B/dJMcaaRL1rl/e6nP8uayeUY2sXXWN5lf0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckNi4B/dJMcaaRL1rl/e6nP8uayeUY2sXXWN5lf0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckNi4B%2FdJMcaaRL1rl%2Fe6nP8uayeUY2sXXWN5lf0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1017&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1017&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치파일 실행 후 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 위치 - 기본으로 설정하자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 선택 - 모두 선택해도 상관없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 디렉토리 - 기본값으로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패스워드 설정 - supersuer인 'postgres'의 비밀번호이다. 꼭 기억해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트번호 - 기본 5432를 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;locale - 기본으로 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 끝나면 Stack Builder 어쩌고 선택이 나오는데, 해제 후 Finish하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GUI기반 postgresql 관리 도구는 &lt;b&gt;pgAdmin&lt;/b&gt;을 설치하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pgAdmin 4를 사용해 지지고 볶으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;paAdmin자동완성 키기: File - Preferences - Query Tool - Auto completion - Autocomplete on keypress를 키고 저장하면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2. 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;새로운 계정 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770791807734&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE USER tungtungtung WITH PASSWORD 'sahur';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 데이터베이스에 대한 모든 권한 부여&lt;/p&gt;
&lt;pre id=&quot;code_1770791840452&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GRANT ALL PRIVILEGES ON DATABASE tunginside TO tungtungtung;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 권한 부여&lt;/p&gt;
&lt;pre id=&quot;code_1770791866519&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GRANT ALL ON SCHEMA public TO tungtungtung;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DB생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770791722830&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE DATABASE tunginside;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgresql은 기본적으로 UTF8을 사용하기 때문에 한글 깨짐 걱정이 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1770791763205&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select datname from pg_database;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 잘 생성 되었는지 확인 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Ubuntu 환경&lt;/b&gt;&lt;/h3&gt;</description>
      <category>데이터베이스 (Database)</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/319</guid>
      <comments>https://owwowo.tistory.com/319#entry319comment</comments>
      <pubDate>Wed, 11 Feb 2026 15:45:47 +0900</pubDate>
    </item>
    <item>
      <title>DBMS, BAAS</title>
      <link>https://owwowo.tistory.com/318</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대학교를 다닐 때는 MySql만 사용했다. 그런데 실제로 사용되는 거는 많은 DBMS들이 사용된다. 대표적인 DBMS들을 알아보자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyybT/dJMcabQDMVh/5bSpqjPerYw5whoNHM4TjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyybT/dJMcabQDMVh/5bSpqjPerYw5whoNHM4TjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyybT/dJMcabQDMVh/5bSpqjPerYw5whoNHM4TjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkyybT%2FdJMcabQDMVh%2F5bSpqjPerYw5whoNHM4TjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;459&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. rdbms와 nosql dbms&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1. rdbms (relational dbms)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rdbms는 관계형 dbms로 데이터를 행(row)과 열(column)이 있는 테이블에 저장하며, 테이블 간의 '관계'를 맺어 데이터를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 무결성, 신뢰성, sql활용이 장점이다. 대신 데이터 구조가 미리 정해져야하고 변경이 어렵다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 MySQL, Oracle Database, PostgreSQL, Microsoft SQL Server등이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2. nosql&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정해진 트리 업이 데이터의 형태에 맞게 저장하는 방식이다. 대규모 분산 처리에 최적화되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유연성, 확장성, 속도가 장점이다. 대신 데이터 중복문제와 일관성 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 MongoDB, Redis등이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.3. 각 dbms의 특징&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- Oracle Database&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기업용 시장 점유율 1위의 유료 DB다. 대용량 데이터 처리 성능이 압도적이며, 보안과 장애 복구 기능이 매우 강력하다. 대기업 메인 시스템, 금융권, 공공기관 등 안정성이 최우선이 곳에서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- MySQL&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 세계에서 가장 많이 사용되는 오픈 소스 DB이다. 설치가 쉽고 사용법이 간단하며, 정보를 찾기 쉽다. 대부분의 웹 서비스 표준에서 사용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- PostgreSQL&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스임에도 Oracle에 버금가는 기능을 제공한다. 복잡한 쿼리 처리 능력이 뛰어나고, GIS(지리정보)나 JSON 데이터 처리 등 확장 기능이 강력하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- Microsoft SQL Server &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로코스프에서 개발한 DB로, windows 환경에 최적화되어있다. 윈도우 기반 기업용 솔루션, 중대형 내부 업무 시스템에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- MongoDB&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 문서 지향 DB이다. 데이터를 JSON과 거의 똑같은 형식(BSON)으로 저장한다. 스키마가 없어서 데이터 구조를 언제든 바꿀 수 있고, 개발 속도가 매우 빠르다. SNS 서비스, 비정형 데이터 저장, 실시간 이벤트 로그 처리에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- Redis&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 hdd가 아닌 ram에 저장하는 in-memory DB이다. 읽기/쓰기가 굉장히 빠르다. 다만 메모리 기반이라 전원이 꺼지면 데이터가 사라질 위험이 있어 보조DB로 사용된다. 실시간 랭킹 시스템, 로그인 세션 유지, 캐시 서버, 짧은 시간 내 대량의 데이터 처리에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. BaaS (Backend as a Service)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 firebase나 supabase는 뭐라할까? firebase는 내부에서 Cloud Firestore이라는 NoSQL을 사용하고 supabase는 PostgreSQL이라는 rdbms를 사용한다. 그러나 dbms역할 말고도 사용자 인증, 파일 스토리지, API생성, 클라우스 함수 등의 기능을 제공하기 때문에 이 도구들을 묶어 baas라고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://namu.wiki/w/DBMS&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://namu.wiki/w/DBMS&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;/p&gt;</description>
      <category>데이터베이스 (Database)</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/318</guid>
      <comments>https://owwowo.tistory.com/318#entry318comment</comments>
      <pubDate>Tue, 10 Feb 2026 17:32:07 +0900</pubDate>
    </item>
    <item>
      <title>tailwind css</title>
      <link>https://owwowo.tistory.com/317</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;- react에 tailwindcss적용하기&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 프로젝트 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. tailwindcss vite버전 설치&lt;/p&gt;
&lt;pre id=&quot;code_1771048433992&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install tailwindcss @tailwindcss/vite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. vite.config.ts에 플러그인 추가&lt;/p&gt;
&lt;pre id=&quot;code_1771048486797&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. css파일&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;(index.css나 App.css)&lt;/span&gt; 에 tailwindcss 가져오기&lt;/p&gt;
&lt;pre id=&quot;code_1771048513739&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@import &quot;tailwindcss&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. cdn이나 빌드된 css파일 html로 첨부하기&lt;/p&gt;
&lt;pre id=&quot;code_1771048614579&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 자동완성기능은 vscode에 Tailwind CSS IntelliSense를 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://tailwindcss.com/docs/installation/using-vite&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tailwindcss.com/docs/installation/using-vite&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 기능을 알아보는게 아닌 핵심 기능만을 정리할 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- max-w-{...xs|lg|xl|2xl|3xl...}&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위 요소에 넣어야 하는 클래스이다. 최대 가로 길이를 제한한다. 텍스트나 이미지가 화면 끝에서 끝까지 늘어지면 가독성이 매우 떨어지기 때문에 넣는다. mx-auto를 함께 넣어 가운데 정렬을 한다. 안넣는다면 화면 왼쪽에 사이트가 붙는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- Grid (2차원 레이아웃)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 클래스&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3953%; height: 21px;&quot;&gt;부모 요소 클래스&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%; height: 21px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3953%; height: 21px;&quot;&gt;grid&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%; height: 21px;&quot;&gt;컨테이너를 grid 환경으로 설정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3953%; height: 21px;&quot;&gt;gap-{n}&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%; height: 21px;&quot;&gt;아이템 사이의 간격을 정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3953%; height: 21px;&quot;&gt;grid-cols-{n}&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%; height: 21px;&quot;&gt;열의 개수를 정한다 (1~12개)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 26.3953%; height: 21px;&quot;&gt;justify-items-end&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%; height: 21px;&quot;&gt;(좌우정렬) 아이템들을 칸의 끝에 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;justify-items-center&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;(좌우정렬) &lt;/span&gt;아이템들을 칸의 가운데에 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;justify-items-stretch&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;(좌우정렬) &lt;/span&gt;아이템들을 칸에 꽉차게 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;content-center&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;(상하정렬) 아이템들을 칸의 가운데에 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;content-end&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;(상하정렬) &lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;아이템들을 칸의 끝에 놓는다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;content-between&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;(상하정렬) 아이템들을 최대한 떨어뜨려놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;content-stretch&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;(상하정렬) 아이템들을 칸에 꽉차게 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;place-content-start&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;아이템들을 왼쪽 위에 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;place-content-end&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;아이템들을 오른쪽 아래 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;place-content-between&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;아이템들을 최대한 떨어뜨려놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;place-content-stretch&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;아이템들을 꽉차게 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;place-items-end&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;아이템을 칸 오른쪽 아래 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;place-items-center&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;아이템을 칸 가운데 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.3953%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;place-items-stretch&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.6047%;&quot;&gt;아이템을 칸에 꽉차게 놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;자식 요소 클래스&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;col-span-{n}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;여러 칸을 차지하게 한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;col-start-{n}&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;n번째 칸에서 시작하게 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;col-end-{n}&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;n-1번째 칸을 마지막으로 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;justify-self-start&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬)&lt;/span&gt; 칸 시작에 놓인다. 부모의 justify-items-{}를 무시한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;justify-self-center&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬)&lt;/span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;칸 가운데에 놓인다. 부모의 justify-items-{}를 무시한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;justify-self-end&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬)&lt;/span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;칸 끝에 놓인다. 부모의 justify-items-{}를 무시한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;justify-self-stretch&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬)&lt;/span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;칸에 꽉차게 놓인다. 부모의 justify-items-{}를 무시한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6279%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;place-self-start&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;place-self-center&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;place-self-end&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;place-self-stretch&lt;/span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.3721%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아이템을 칸에 (왼쪽위|가운데|오른쪽아래|꽉차게) 놓는다. 부모의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;place-items-{}를 무시한다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- flex (1차원 레이아웃)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 클래스&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 273px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;부모 요소 클래스&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;flex&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;컨테이너를 flex 환경으로 설정한다.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;gap-{n}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아이템 사이의 간격을 정한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;flex-col&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;자식 요소들이 세로 방향으로 나란히 배치된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;flex-row&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;자식 요소들이 가로 방향으로 나란히 배치된다. 기본 상태이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;flex-wrap&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;자식 요소들이 가로 방향으로 나란히 배치된다. 가로 길이가 부족하면 자동으로 줄바꿈이 된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;justify-center&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬)&lt;/span&gt; 자식 요소들을 가운데에 모은다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;justify-end&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬)&lt;/span&gt; 자식 요소들을 끝에 모은다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;justify-between&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬)&lt;/span&gt; 자식 요소들을 최대한 떨어뜨려놓는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;justify-around&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(좌우정렬) 자식 요소들을 적당히 떨어뜨려놓는다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;items-start&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(상하정렬) 아이템들을 위에 놓는다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;items-center&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(상하정렬) 아이템들을 가운데 놓는다.&lt;/span&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.093%; height: 21px;&quot;&gt;items-end&lt;/td&gt;
&lt;td style=&quot;width: 72.907%; height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(상하정렬) 아이템들을 아래에 놓는다.&lt;/span&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;자식 요소 클래스&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;flex-1&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;필요한만큼 요소가 늘어나거나 줄어든다. grow와 shrink를 둘다 적용한 것이다. 이 요소들이 여러개라면 모두 같은 높이와 길이를 가진다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;flex-auto&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;공간이 남는다면 요소가 늘어난다. 기본크기보다 작아지지 않는다.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;grow&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;공간이 남는다면 요소가 늘어난다. 기본크기보다 작아지지 않는다.&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;shrink-0&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;공간이 부족해도 요소가 줄어들지 않는다. grow옆에 줄어들지 말아야 할 요소가 있다면 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;self-start&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(상하정렬) 위에 놓인다. 부모의 items-{}를 무시한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;self-center&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(상하정렬) 가운데 놓인다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;부모의 items-{}를 무시한다&lt;/span&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;self-end&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(상하정렬) 아래에 놓인다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;부모의 items-{}를 무시한다&lt;/span&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;self-stretch&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(상하정렬) 꽉차게 놓인다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;부모의 items-{}를 무시한다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;grid를 사용할지 flex를 사용할지는 어떻게 정하는가? &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면이 커짐에 따라 칸의 개수가 변한다면 grid를 써야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1770273090010&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4&quot;&amp;gt;
  &amp;lt;div&amp;gt;카드 1&amp;lt;/div&amp;gt;
  &amp;lt;div&amp;gt;카드 2&amp;lt;/div&amp;gt;
  &amp;lt;div&amp;gt;카드 3&amp;lt;/div&amp;gt;
  &amp;lt;div&amp;gt;카드 4&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠 크기가 유연하다면 flex를 쓰는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1770273169496&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;flex flex-wrap gap-4&quot;&amp;gt;
  &amp;lt;div class=&quot;flex-1 min-w-[200px]&quot;&amp;gt;유연한 박스 1&amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;flex-1 min-w-[200px]&quot;&amp;gt;유연한 박스 2&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- spacing (padding, margin)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p-{n}으로 패딩을 준다. 상하만 패딩을 주고 싶다면 py-{n}, 좌우만 패딩을 주고 싶다면 px-{n}을 주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상하좌우 따로 패딩을 주고 싶다면 pt-{n}, pb-{n}, pl-{n}, pr-{n}으로 주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;margin도 패딩과 같은 패턴이다. m-{n}, my-{n}, mx-{n}, m&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;t-{n},&lt;span&gt; m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;b-{n},&lt;span&gt; m&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;l-{n},&lt;span&gt; m&lt;/span&gt;&lt;/span&gt;r-{n}으로 주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- typography&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text-{xs|sm|base|lg|xl|2xl|...|8xl|9xl} 로 텍스트 크기를 조절한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text-{color}-{n}으로 텍스트 색깔을 조절한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text-{left|center|right}으로 텍스트를 정렬한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;font-{thin|extralight|light|normal|medium|semibold|bold|extrabold|black}로 폰트 굵기를 조절한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tracking-{tighter|tight|normal|wide|wider|widest}로 글자 사이 간격을 조절한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;line-clamp-{n}으로 글이n줄까지만 보이게 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;underline|overline|line-through|no-underline으로 텍스트를 장식한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- whitespace&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;div나 p태그 내부에 띄어쓰기, 탭, 엔터 같은 것들이 어떻게 화면에 표현될지 정하는 것.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 143px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 31.0465%; height: 21px;&quot;&gt;클래스&lt;/td&gt;
&lt;td style=&quot;width: 68.9535%; height: 21px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 31.0465%; height: 21px;&quot;&gt;whitespace-normal&lt;/td&gt;
&lt;td style=&quot;width: 68.9535%; height: 21px;&quot;&gt;모든 whitespace를 띄어쓰기 한칸로 표현된다. 디폴트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 31.0465%; height: 21px;&quot;&gt;whitespace-nowrap&lt;/td&gt;
&lt;td style=&quot;width: 68.9535%; height: 21px;&quot;&gt;한줄로 모든게 표현된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 31.0465%; height: 21px;&quot;&gt;whitespace-pre&lt;/td&gt;
&lt;td style=&quot;width: 68.9535%; height: 21px;&quot;&gt;모든 whitespace가 표현된다. 한줄이 길다면 줄바꿈을 하지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 31.0465%; height: 21px;&quot;&gt;whitespace-pre-line&lt;/td&gt;
&lt;td style=&quot;width: 68.9535%; height: 21px;&quot;&gt;엔터는 표현되지만 띄어쓰기들은 한칸으로 표현된다. 자동 줄바꿈.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 31.0465%; height: 21px;&quot;&gt;whitespace-pre-wrap&lt;/td&gt;
&lt;td style=&quot;width: 68.9535%; height: 21px;&quot;&gt;모든 whitespace가 표현된다. 자동 줄바꿈. 줄이 바뀌면 whitespace가 없어짐.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 31.0465%; height: 17px;&quot;&gt;whitespace-break-spaces&lt;/td&gt;
&lt;td style=&quot;width: 68.9535%; height: 17px;&quot;&gt;모든 whitespace가 표현된다. 자동 줄바꿈. 줄이 바뀌어도 whitespace가 표현.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- variants&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에 붙여서 특정 상황일 때 클래스를 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가로길이에 따른 variants - sm: md: lg: xl: 2xl:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테마에 따른 variants - dark:&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://v3.tailwindcss.com/docs/flex&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://v3.tailwindcss.com/docs/flex&lt;/a&gt;&lt;/p&gt;</description>
      <category>web/css</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/317</guid>
      <comments>https://owwowo.tistory.com/317#entry317comment</comments>
      <pubDate>Thu, 5 Feb 2026 16:52:45 +0900</pubDate>
    </item>
    <item>
      <title>CSS 웹 디자인 접근, 업그레이드 방법</title>
      <link>https://owwowo.tistory.com/316</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;고민&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;css는 자유도가 너무 높다. 내가 원하는 대로 디자인이 가능한 것이다. 하지만 내가 디자인을 정하고 css코드를 치려고 하면 굉장히 어렵다. 내가 디자인을 정하는 것 부터가 문제이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 디자인을 해야 접근성이 좋을지, 글자가 너무 크지도 않고 작지도 않을지, 모바일에서와 pc에서 사용경험이 다를지, 사용자 경험이 직관적일지, 일관성 있는 색상 요소들... 너무나 많은 생각을 해 시작부터 난관에 빠지는 것이다. 게다가 내가 생각한 디자인을 css코드로 만들 수 있는 디자인인지조차 생각해보면서 하면, 디자인을 하려고 시작할 때부터 스트레스를 엄청 받는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 것도 모를 때 알면 좋은 것들&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 도구 활용(Figma 등): css코드로 구현할 수 있을지는 생각하지 말고 일단 시각화에만 집중하는 단계가 필요하다. 피그마에서 만드는 레이아웃은 tailwind css같은 유명 프레임워크로 거의 100% 구현 가능하기 때문에 css코드로 구현할 수 있을지 고민하지 않아도 된다. 일관성 있는 색상 요소들인지도 눈으로 볼 수 있으니 해결 가능한 고민이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 우선주의: 모바일에서와 pc에서 사용경험이 다른것은 당연하다. pc기준으로 만든 레이아웃이면 모바일에서는 컴포넌트들이 작고, 모바일 기준으로 만든 레이아웃이면 컴포넌트들이 큰 것이다. 하지만 다른점은 pc기준으로 만든 레이아웃을 모바일에 맞추기는 어려우나 모바일기준으로 만든 레이아웃을 pc기준으로 바꾸기는 훨씬 쉽다는 것이다. 모바일에서 없는 부분을 확장하면 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;css 프레임워크(tailwind, bootstrap 등) 활용: 사용할 수 있는 색상과 글자 크기가 몇개 정해져있다. 이중에서 골라서 사용하면 되는것이다.&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;css 프레임워크 존재 이유와 공부법&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;생 css의 복잡함을 어떻게 하면 질서있게 관리할까? 라는 질문은 나만 한 것이 아니다. 모든 css프레임워크들이 css를 질서있게 관리하기 위해 만들어졌다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 css프레임워크들이 공유하는 핵심 공통 개념이 있다. 핵심 공통 개념들만 알아놓으면 css프레임워크들이 어떤건지 하는지 금방 이해할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 유틸리티 퍼스트 또는 클래스 조합: Bootstrap은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #8cb3be;&quot;&gt;.btn-primary&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;tailwind는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #8cb3be;&quot;&gt;flex pt-4 text-center&lt;/span&gt;처럼 클래스들을 조립해서 사용한다. 이미 준비된 클래스 이름을 HTML에 갖다 붙인다는 공통점이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 반응형 디자인 접두사 (Breakpoints): 화면 크기에 따라 스타일을 바꾸는 기능을 '접두사'로 해결한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;sm:&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;md:&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;lg:&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;xl:&lt;/span&gt;등.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 디자인 토큰 (Design Tokens): 미리 약속된 수치가 있다. p-1, blue-500처럼 숫자를 붙여 정해진 단위를 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 그리드 시스템 (Grid &amp;amp; Container): 모든 프레임워크는 화면을 나누는 '틀'을 가지고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 박스 모델의 시각화 (Flex &amp;amp; Grid 제어): 프레임워크들은 Flexbox속성을 짧은 클래스로 배치할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;items-center&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;justify-between&lt;/span&gt;등&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 프레임워크의 공식문서를 모두 읽으면 시간이 너무 많이 걸린다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. Layout: 화면 전체 틀을 어떻게 잡는지 (Container, Grid)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. Flexbox: 요소를 가로/세로로 어떻게 세우는지&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. Spacing: 안쪽/바깥쪽 여백은 어떻게 주는지 (padding, margin)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. Typhgraphy: 글자 크기와 굵기는 어떻게 바꾸는지&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 4가지만 훑어보면 대부분 ui를 바로 코딩 가능하다. 나머지는 필요할 때 검색해보면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 어떤 순서로 디자인과 코딩을 진행해야 하는가?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;top-down으로 계획하고, bottom-up으로 구현하는것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;피그마나 메모장에 큼직하게 사각형을 그려 헤더, 콘텐츠, 푸터 등등 구역을 나눈다. 이때 세부 디자인은 하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체를 한번에 그리려고 하지 말고, 작은 컴포넌트부터 만들고 작은 컴포넌트들을 조립해 중간 컴포넌트를 만들고.. 를 반복해 조립하여 전체를 만드는 bottom-up방식이 개발자에게 좋은 방법이다.&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;디자인 업그레이드 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;색상과 대비&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러가지 색깔을 사용하여 웹페이지 요소들을 배치하면, 시선이 분산되고 난잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜드 컬러를 하나 정하자. 그리고 나머지는 무채색(흰색, 회색, 검정색)위주로 사용하는 것이다. 다른 사이트들을 봐라. 페이지 대부분이 무채색일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>web/css</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/316</guid>
      <comments>https://owwowo.tistory.com/316#entry316comment</comments>
      <pubDate>Thu, 5 Feb 2026 12:17:43 +0900</pubDate>
    </item>
    <item>
      <title>React</title>
      <link>https://owwowo.tistory.com/315</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;- 사용 이유와 장점&lt;/b&gt;&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 프로젝트에서 사용할 때 좋다. 대규모 프로젝트에서의 장점은 수만줄의 코드를 수십명의 개발자가 함께 관리할 수 있는 구조이다. ui를 컴포넌트 단위로 개발해서 재사용성이 좋고 작업 영역이 분리되어 유지보수하기 쉽다. 규모가 커질수록 상태가 복잡해져 코드가 꼬이기 쉬운데, 상태에 따른 결과만 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 정보를 보여주는 페이지를 만드는데는 react가 불필요하다. 유지보수와 확장성을 생각한다면 괜찮은 선택이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;- 설치, 빌드&lt;/b&gt;&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;nodejs가 설치되어있지 않다면 설치한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- webpack버전 (React 18이하 버전 방식)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1769992163498&quot; class=&quot;dsconfig&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npx create-react-app {app 이름}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;를 입력하면 app이름으로 폴더가 만들어진다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;vscode - 폴더열기로 app이름 폴더를 열고 터미널에 npm start를 하면 앱을 편집하면서 저장만 하면 브라우저에서 바로 변경본이 적용된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;npm run build를 하면 build에 제품이 build되고, serve -s build를 하면 build된 제품을 로컬호스트에 띄워준다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;- vite버전(React 18버전이상부터 추천되는 방식)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770094534327&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm create vite@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 입력하고 프로젝트 이름을 정하고 react를 선택하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리보기는 npm run dev를 하면 된다.&lt;br /&gt;npm run build를 하면 dist에 제품이 build되고, npm run preview하여 배포될 앱의 최종점검을 하면 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;- JSX와 데이터바인딩&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react에서는 jsx문법을 사용해 javascript코드 내에서 html과 비슷한 마크업을 작성할 수 있게 해준다. 변수, 함수, 제어문 등을 이용해 동적 html생성이 쉬운 것이다.&amp;nbsp;&lt;br /&gt;또한 {}문법을 사용하면 데이터 바인딩이 가능한데, 데이터를 html에 쉽게 삽입할 수 있다. 그리고 state 데이터를 바인딩하면 html이 알아서 업데이트된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;- useEffect&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 로딩될 때, 변수값이 바뀔 때 특정 작업을 지시할 수 있는 함수다.&lt;/p&gt;
&lt;pre id=&quot;code_1770182694443&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    if (categories.length === 0) {
      fetchCategories();
    }
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1770182743660&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
    localStorage.setItem('like_cut', likeCut);
    localStorage.setItem('size', size);
    localStorage.setItem('orderby', orderBy);

    async function getPosts() {
      setIsLoading(true);
      try {
        // 글목록 가져오기
        const response = await axios.get(
          &quot;/api/post&quot;,
          {
            params: {
              page, abbr, like_cut: likeCut, size, orderby: orderBy, search
            }
          }
        );
        if (response.status === 200 || response.status === 201) {
          setPosts(response.data.posts);
          console.log(response.data);
        }
      } catch (error) {
        console.error('글목록 가져오기 에러', error);
        alert(JSON.stringify(error.response.data));
      } finally {
        setIsLoading(false);
      }
    }
    getPosts();
  }, [abbr, page, likeCut, size, orderBy, search]);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;- state(상태)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태에 따른 ui 구조를 정의해놓으면, ui를 우리가 직접 바꾸지 않아도 상태만 바뀌면 react가 상태의 변경을 감지해 알아서 ui를 바꿔준다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770024646951&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const [isLoggedIn, setIsLoggedIn] = useState(false);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState(초기값)을 사용하면 [읽기전용변수, 값바꾸기함수]를 반환해준다. jsx에 읽기전용변수를 데이터 바인딩하여 화면에 나타내면 된다. 그리고 값바꾸기함수를 사용해 상태를 바꾸면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역 상태가 아닌 &lt;b&gt;전역 상태&lt;/b&gt;를 써야 하는 상황이라면 zustand같은 라이브러리를 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1770080801356&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { create } from 'zustand'

const useBear = create((set) =&amp;gt; ({
  bears: 0,
  increasePopulation: () =&amp;gt; set((state) =&amp;gt; ({ bears: state.bears + 1 })),
  removeAllBears: () =&amp;gt; set({ bears: 0 }),
  updateBears: (newBears) =&amp;gt; set({ bears: newBears }),
}))&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1770080857303&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function BearCounter() {
  const bears = useBear((state) =&amp;gt; state.bears)
  return &amp;lt;h1&amp;gt;{bears} bears around here...&amp;lt;/h1&amp;gt;
}

function Controls() {
  const increasePopulation = useBear((state) =&amp;gt; state.increasePopulation)
  return &amp;lt;button onClick={increasePopulation}&amp;gt;one up&amp;lt;/button&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬스토리지에 전역 상태를 저장해야된다면 &lt;b&gt;persist 미들웨어&lt;/b&gt;를 사용하면 된다. 상태변화를 감지해 알아서 저장과 불러오기를 해준다. 내부적으로 JSON.stringify와 JSON.parse를 해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1770099865418&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useThemeStore = create(
  persist(
    (set) =&amp;gt; ({
      isDark: false,
      toggleTheme: () =&amp;gt; set((state) =&amp;gt; ({ isDark: !state.isDark })),
    }),
    { name: 'theme-storage' } // 로컬 스토리지에 저장될 이름
  )
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;- react-router-dom&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1771049741468&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install react-router-dom&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태만을 사용해 ui를 변경시킨다면, 앞으로가기 뒤로가기 같은 브라우저 기능을 사용할 수 없게된다. 웹브라우저는 url이 변경되는것을 감지해 이동시키기 때문이다. 이때 react-router-dom라이브러리를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위에 &amp;lt;BrowserRouter&amp;gt; 또는 &amp;lt;HashRouter&amp;gt;를 둔다.&lt;/p&gt;
&lt;pre id=&quot;code_1770182965770&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;BrowserRouter&amp;gt;
      &amp;lt;div className=&quot;App w-full bg-orange-200 min-h-screen flex flex-col&quot;&amp;gt;
        &amp;lt;Header&amp;gt;&amp;lt;/Header&amp;gt;
        &amp;lt;Routes&amp;gt;
          &amp;lt;Route path='/signup' element={&amp;lt;Signup&amp;gt;&amp;lt;/Signup&amp;gt;}&amp;gt;&amp;lt;/Route&amp;gt;
          &amp;lt;Route path='/login' element={&amp;lt;Login&amp;gt;&amp;lt;/Login&amp;gt;}&amp;gt;&amp;lt;/Route&amp;gt;
          &amp;lt;Route path='/logout' element={&amp;lt;Login&amp;gt;&amp;lt;/Login&amp;gt;}&amp;gt;&amp;lt;/Route&amp;gt;
          &amp;lt;Route path='/myinfo' element={&amp;lt;Myinfo&amp;gt;&amp;lt;/Myinfo&amp;gt;}&amp;gt;&amp;lt;/Route&amp;gt;
          &amp;lt;Route path='/myinfoedit' element={&amp;lt;MyinfoEdit&amp;gt;&amp;lt;/MyinfoEdit&amp;gt;}&amp;gt;&amp;lt;/Route&amp;gt;
          &amp;lt;Route path='/category/:abbr' element={
            &amp;lt;&amp;gt;
              &amp;lt;Category&amp;gt;&amp;lt;/Category&amp;gt;
              &amp;lt;PostList&amp;gt;&amp;lt;/PostList&amp;gt;
            &amp;lt;/&amp;gt;
          }&amp;gt;&amp;lt;/Route&amp;gt;
          &amp;lt;Route path='/' element={
            &amp;lt;&amp;gt;
              &amp;lt;CategoryList&amp;gt;&amp;lt;/CategoryList&amp;gt;
              &amp;lt;PostList&amp;gt;&amp;lt;/PostList&amp;gt;
            &amp;lt;/&amp;gt;
          }&amp;gt;&amp;lt;/Route&amp;gt;
        &amp;lt;/Routes&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/BrowserRouter&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 &amp;lt;Switch&amp;gt;를 썼지만 현재 &amp;lt;Routes&amp;gt;를 사용한다. 이 안에 &amp;lt;Route path=''&amp;gt;를 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1770183078883&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Link to={'/category/' + abbr}&amp;gt;{currentCategory.name}&amp;lt;/Link&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Link to=''&amp;gt;를 사용해 url을 바꿀 수 있다. &amp;lt;a href&amp;gt;는 새로 서버에 요청하는 반면, 이건 요청이 아니라 url만 바꾸는 기능이다.&lt;/p&gt;
&lt;pre id=&quot;code_1770183194153&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const navigate = useNavigate();
navigate('/login');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;javascript로 url을 변경하고싶다면 navigate를 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1770183246990&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { abbr } = useParams();
  const [searchParams] = useSearchParams();
  const page = searchParams.get('page') || '1';
  const search = searchParams.get('search');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;url의 변수가 변함에 따라 자동으로 ui가 업데이트되는 기능을 사용하려면 useParams()와 useSearchParams()를 사용하면 된다. document.URL같은 걸 사용하면 ui자동 업데이트가 되지 않는다. /category/:abbr 같은 url은 useParams()로, /?page=2&amp;amp;search=0206 같은거는 useSearchParams()를 사용하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;- 디자인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tailwind를 사용하는게 대표적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람들이 사용하지만 클래스가 너무 많아 가독성이 떨어지고 겹치는 코드가 많아지는데, 이때 컴포넌트 추출 방식(자주 사용되는 디자인 패턴을 별도의 리액트 컴포넌트로 분할하는 방식)을 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1770085215196&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// components/common/Button.jsx
export default function Button({children, onClick, type=&quot;button&quot;, className=&quot;&quot;}) {
    return (
        &amp;lt;button type={type} onClick={onClick} 
        className={`rounded-md bg-indigo-600 px-2 py-1 text-sm text-white shadow-xs gap-1 ${className}`}&amp;gt;
            {children}
        &amp;lt;/button&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1770085280652&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div className='flex justify-end py-1 gap-1'&amp;gt;
  &amp;lt;Button&amp;gt;등록&amp;lt;/Button&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>web</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/315</guid>
      <comments>https://owwowo.tistory.com/315#entry315comment</comments>
      <pubDate>Mon, 2 Feb 2026 18:34:07 +0900</pubDate>
    </item>
    <item>
      <title>PyQt5 라이브러리</title>
      <link>https://owwowo.tistory.com/314</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PyQt5 라이브러리는 많은 것을 지원하지만, 다른 라이브러리보다 뛰어난 기능은 GUI기능이다. 운영체제에 상관없이 코드 호환성이 거의 100%이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 창 띄우기:&lt;/p&gt;
&lt;pre id=&quot;code_1765173547039&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
from PyQt5.QtWidgets import QApplication, QWidget

class MyApp(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle(&quot;내가 만든 창&quot;)
        self.move(500, 100)
        self.resize(400, 200)
        self.show()

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    ex = MyApp()
    app.exec_()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리드 레이아웃으로 적절한 레이아웃 만들기:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;335&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/842gE/dJMcafkN1V0/X7D6KH9tKoXLOyBwxwX6r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/842gE/dJMcafkN1V0/X7D6KH9tKoXLOyBwxwX6r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/842gE/dJMcafkN1V0/X7D6KH9tKoXLOyBwxwX6r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F842gE%2FdJMcafkN1V0%2FX7D6KH9tKoXLOyBwxwX6r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;428&quot; height=&quot;335&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;335&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1765177669373&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QIcon

class MyApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        centralWidget = QWidget()
        self.setCentralWidget(centralWidget)

        mainGrid = QGridLayout()
        centralWidget.setLayout(mainGrid)

        leftGrid = QGridLayout()

        leftGrid.addWidget(QLabel('title'), 0, 0)
        leftGrid.addWidget(QLabel('author'), 1, 0)
        leftGrid.addWidget(QLabel('review'), 2, 0)

        leftGrid.addWidget(QLineEdit(), 0, 1)
        leftGrid.addWidget(QLineEdit(), 1, 1)
        leftGrid.addWidget(QTextEdit(), 2, 1)

        rightGrid = QGridLayout()

        btn = QPushButton('버튼', self)
        btn.clicked.connect(self.buttonF)
        rightGrid.addWidget(btn, 3, 0)

        mainGrid.addLayout(leftGrid, 0, 0)
        mainGrid.addLayout(rightGrid, 0, 1)

        self.setWindowTitle(&quot;내가 만든 창&quot;)
        self.move(500, 100)
        self.statusBar().showMessage('statusbar')
        self.show()

    def buttonF(self):
        print(&quot;버튼 클릭됨&quot;)

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    ex = MyApp()
    app.exec_()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 위젯과 시그널:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wikidocs.net/21933&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wikidocs.net/21933&lt;/a&gt;에 보면 여러 위젯과 해당 위젯의 시그널을 설명해주고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼, 체크박스, 콤보박스, 텍스트 브라우저 등 다양한 위젯을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시그널 메소드.connect를 하면 해당 시그널이 일어났을 때 실행해야할 함수를 지정할 수 있다. 예를들어 버튼 클릭이 일어났을 때, btn.clicked.connect(funcA) 를 하면 btn이 클릭되었을 때 funcA가 실행된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://pypi.org/project/PyQt5/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pypi.org/project/PyQt5/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wikidocs.net/book/2165&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wikidocs.net/book/2165&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wikidocs.net/book/2944&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wikidocs.net/book/2944&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;/p&gt;</description>
      <category>python</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/314</guid>
      <comments>https://owwowo.tistory.com/314#entry314comment</comments>
      <pubDate>Mon, 8 Dec 2025 15:14:55 +0900</pubDate>
    </item>
    <item>
      <title>파이썬 소켓 통신</title>
      <link>https://owwowo.tistory.com/313</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기본 예시)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버&lt;/p&gt;
&lt;pre id=&quot;code_1764749005152&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_address = ('localhost', 61001)

s.bind(server_address)

s.listen()

client_s, client_addr = s.accept()
for i in range(10):
    data = client_s.recv(1024)
    data = data.decode()
    print(data)
    
client_s.close()
print('종료')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트&lt;/p&gt;
&lt;pre id=&quot;code_1764749014828&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('localhost', 61001)

s.connect(server_address)

while True:

    data = input('')
    if data == 'q':
        break
    data = data.encode()
    s.send(data)

s.close()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 채팅 주고받기 - 보내고 받는 스레드를 만들면 동시에 실행이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버&lt;/p&gt;
&lt;pre id=&quot;code_1764752081378&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket
import threading

def Send(sock):
    while True:
        rawdata = input('')
        data = rawdata.encode()
        sock.send(data)
        if rawdata == 'q':
            break
    sock.close()

def Recv(sock):
    while True:
        data = sock.recv(1024)
        if not data:
            break
        data = data.decode()
        print(&quot;Client:&quot;, data)
        if(data == 'q'):
            break
    sock.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('localhost', 61001)

s.bind(server_address)

s.listen()

client_socket, client_addr = s.accept()

recvThread = threading.Thread(target=Recv, args=(client_socket,))
sendThread = threading.Thread(target=Send, args=(client_socket,))
recvThread.start()
sendThread.start()
recvThread.join()
sendThread.join()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트&lt;/p&gt;
&lt;pre id=&quot;code_1764752090571&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket
import threading

def Send(sock):
    while True:
        rawdata = input('')
        data = rawdata.encode()
        sock.send(data)
        if rawdata == 'q':
            break
    sock.close()

def Recv(sock):
    while True:
        data = sock.recv(1024)
        if not data:
            break
        data = data.decode()
        print(&quot;Server:&quot;, data)
        if(data == 'q'):
            break
    sock.close()

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('localhost', 61001)

server_socket.connect(server_address)

recvThread = threading.Thread(target=Recv, args=(server_socket,))
sendThread = threading.Thread(target=Send, args=(server_socket,))
recvThread.start()
sendThread.start()
recvThread.join()
sendThread.join()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논 블로킹 소켓&lt;/p&gt;
&lt;pre id=&quot;code_1764833548671&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def Recv(sock):
    sock.setblocking(False)
    while True:
        time.sleep(1)
        try:
            data = sock.recv(1024)
            if not data:
                break
            data = data.decode()
            print(&quot;Server:&quot;, data)
            if(data == 'q'):
                break
        except BlockingIOError:
            print(&quot;아무것도 일어나지 않았다&quot;)
    sock.close()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논 블로킹 소켓을 사용하려면 socket.setblocking(False)를 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무것도 받지 않으면 BlockingIOError를 일으키므로 try except가 필요하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://docs.python.org/ko/3.15/howto/sockets.html#non-blocking-sockets&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/ko/3.15/howto/sockets.html#non-blocking-sockets&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>python</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/313</guid>
      <comments>https://owwowo.tistory.com/313#entry313comment</comments>
      <pubDate>Wed, 3 Dec 2025 17:54:34 +0900</pubDate>
    </item>
    <item>
      <title>asyncio 라이브러리</title>
      <link>https://owwowo.tistory.com/312</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;python 도 js처럼 async await 문법을 사용해 비동기 처리를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. import&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1755946013428&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬은 js와 다르게 asyncio라는 라이브러리를 import해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 함수 정의 &amp;amp; 실행&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1755946222286&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def sleep(n):
    await asyncio.sleep(n)
    print(f'{n}초 sleep 완료')

async def main():
    await sleep(3)

asyncio.run(main())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async def로 비동기 함수를 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 함수는 비동기 함수 내부에서 await가 붙여져 실행되거나 asyncio.run()으로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. asyncio 메소드&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1. asyncio.sleep(delay, result=None)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delay초 동안 sleep한다. 이 메소드는 time.sleep과 다르게 cpu를 다른 작업에 넘긴다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2. asyncio.gather(*aws, return_exceptions=False)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aws시퀀스에 있는 어웨이터블 객체를 동시에 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1755946908704&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def sleep(n):
    await asyncio.sleep(n)
    print(f'{n}초 sleep 완료')

async def main():
    await asyncio.gather(
        sleep(3), sleep(2), sleep(1)
    )

asyncio.run(main())&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3. asyncio.run()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업을 시작한다. 보통 메인 진입점으로 사용된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.4. asyncio.create_task()&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업을 시작한다. 다른 비동기 작업과 같이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;js에서는 일반적인 비동기 함수를 실행시키려면 그냥 호출하면 되지만 python에서는 이 함수를 실행시켜야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://www.youtube.com/watch?v=yYD_brv9R0o&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=yYD_brv9R0o&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wikidocs.net/125092&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wikidocs.net/125092&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/ko/3.13/library/asyncio-task.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/ko/3.13/library/asyncio-task.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&lt;/p&gt;</description>
      <category>python</category>
      <author>blackbearwow</author>
      <guid isPermaLink="true">https://owwowo.tistory.com/312</guid>
      <comments>https://owwowo.tistory.com/312#entry312comment</comments>
      <pubDate>Sat, 23 Aug 2025 20:02:03 +0900</pubDate>
    </item>
  </channel>
</rss>