<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>dev_0hoon</title>
    <link>https://dev0hoon.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 09:24:31 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dev_0hoon</managingEditor>
    <item>
      <title>이메일 발송, 알림, 통계 집계, 쿠폰 발급 등 회원에게 불필요한 로직일 때, 내 로직이 느릴 때, 본질로직과 부수로직 분리, 리스너, 이벤트 리스너 개발(JAVA SPRING)</title>
      <link>https://dev0hoon.tistory.com/437</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. 헬스앱 사이드 프로젝트 진행 중 기획자에게 연락이 왔다.&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;2. &quot;센터 관장이 센터 등록할 때 로딩이 너무 긴것 같아서.. 중간에 이탈이 발생할 것 같아요. 속도를 빠르게 할 수 없을까요?&quot;&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;3. 물론 안되죠! 라고 말하고 싶었지만, 나는 '친절한 개발자'이다.&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;4. 로직을 살펴 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777947147105&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Transactional
    public void saveCenterFirst(HttpServletRequest request, SaveCenterFirstReqeust saveCenterFirstReqeust) {
        Users findUser;

        try {
            // 1) 로그인 사용자 가져오기
            UsersDto user = UserContext.getUser(request);
            ...
            
            // 2) 가입 중인 경우 트레이너 프로필 Y로 변경
            findUser.setUserType(UserTypeEnum.TRAINER);
            ...
            
            // 3) 센터 저장 및 센터 트레이너로 등록
            Center center = saveCenterFirstReqeust.toEntity();
            center.setCenterStatus(CenterStatus.PENDING); // 승인 중
            ...
            ...
            centerTrainerRepository.save(CenterTrainer.create(saveCenter, trainer, CenterTrainerRole.OWNER, REQUESTED)); // 센터 오너이며 아직 요청 중

            
            // 4)사업자 등록증 저장
            try {
                List&amp;lt;UploadFileDto&amp;gt; uploadFileDtos = saveCenterFirstReqeust.getUploadFiles();
                fileService.linkToReferenceIdList(uploadFileDtos, saveCenter.getId(), FileType.BUSINESS_REGISTRATION_CERTIFICATE); //
            } catch (Exception e) {
                log.error(e.getMessage());
                throw new RestApiException(INTERNAL_SERVER_ERROR);
            }

            // 5) 센터장 가입 알림 메일 발송 (사업자 등록증 이미지 첨부)
            try {
                List&amp;lt;UploadFileDto&amp;gt; businessCertFiles = fileService.findUploadFilesByReferenceIdAndTypeAndStatusForMail(
                        saveCenter.getId(), FileType.BUSINESS_REGISTRATION_CERTIFICATE, FileStatus.SAVED, Sort.by(Sort.Direction.ASC, &quot;ord&quot;)
                );

                SendSaveCenterEmail sendSaveCenterEmail = SendSaveCenterEmail.create(saveCenterFirstReqeust.getCenterName(), saveCenterFirstReqeust.getCenterLicenseNumber(), saveCenterFirstReqeust.getCenterMasterName()
                        , saveCenterFirstReqeust.getCenterMasterTel(), saveCenterFirstReqeust.getCenterIntro(), saveCenterFirstReqeust.getCenterTimeCode(), saveCenterFirstReqeust.getCenterAddress(), saveCenterFirstReqeust.getCenterAddressDetail()
                        , saveCenterFirstReqeust.getCenterPostcode(), saveCenterFirstReqeust.getCenterLat(), saveCenterFirstReqeust.getCenterLng(), saveCenterFirstReqeust.getUploadFiles()
                );
                emailService.sendSaveCenterEmail(
                        findUser.getUserName(),
                        sendSaveCenterEmail,
                        businessCertFiles
                );
            } catch (Exception e) {
                log.error(&quot;센터장 가입 알림 메일 발송 실패: {}&quot;, e.getMessage());
            }

        } catch (Exception e) {
            throw new RestApiException(INTERNAL_SERVER_ERROR);
        }
    }&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;5. 디버깅을 진행해보니 '센터장 가입 알림 메일 발송'에서 간헌절으로 속도가 느려짐을 캐치했다.&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;6. 운영에서는 서버속도가 빨라 dev나 qa에서는 충분히 타임아웃 등의 실패 요소가 숨어있어 보였다.&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;7. 이럴 때 사용하기 좋은 방법이 있다.&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;설명 1. Before/ After&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Before &amp;mdash; 직접 호출 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777947353895&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 UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final SlackService slackService;
    private final StatisticsService statisticsService;

    @Transactional
    public void signUp(SignUpRequest request) {
        User user = userRepository.save(new User(request));
        emailService.sendWelcomeMail(user);      // 부수 효과 1
        slackService.notifyNewUser(user);         // 부수 효과 2
        statisticsService.increaseSignUpCount();  // 부수 효과 3
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777947499785&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;signUp() 호출
  &amp;darr;
① User 저장
  &amp;darr; (대기)
② 환영 메일 발송 (예: 2초)
  &amp;darr; (대기)
③ 슬랙 알림 (예: 1초)
  &amp;darr; (대기)
④ 통계 증가 (예: 0.5초)
  &amp;darr;
응답 반환 (총 3.5초+)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;UserService가 이메일, 슬랙, 통계까지 다 알고 있음 (의존성 폭발)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;알림 하나 추가할 때마다 UserService 수정 필요&lt;/li&gt;
&lt;li&gt;이메일 발송이 느리면 회원가입 응답도 느려짐&lt;/li&gt;
&lt;li&gt;슬랙 API가 죽으면 회원가입 자체가 실패할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;After &amp;mdash; 이벤트 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777947394181&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 UserService {
    private final UserRepository userRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public void signUp(SignUpRequest request) {
        User user = userRepository.save(new User(request));
        eventPublisher.publishEvent(new UserSignedUpEvent(user));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UserService는 &quot;회원가입 했음&quot;만 알리고, 나머지는 각 리스너가 알아서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1777947561999&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Spring이 애플리케이션 시작 시점에 @EventListener 메서드들을 스캔해서 
&quot;이벤트 타입 &amp;rarr; 리스너 목록&quot; 매핑 테이블을 만들어둔다. 
publishEvent()가 호출되면 이 테이블에서 해당 이벤트 타입의 리스너들을 찾아 호출한다.&lt;/code&gt;&lt;/pre&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;b&gt;세 가지 모드&lt;/b&gt;로 동작할 수 있고, 어떤 어노테이션을 붙이느냐로 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 기본 (@EventListener) &amp;mdash; 동기 + 같은 트랜잭션&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777947653626&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;signUp() 호출
  &amp;darr;
① User 저장 (트랜잭션 시작)
  &amp;darr;
② publishEvent() 호출
   ├─ WelcomeEmailListener 실행 (대기)
   ├─ SlackListener 실행 (대기)
   └─ StatisticsListener 실행 (대기)
  &amp;darr;
③ 트랜잭션 커밋
  &amp;darr;
응답 반환&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제: 리스너들이 같은 트랜잭션 안에서 동기로 돕니다. Before와 사실상 동일하고, &lt;span style=&quot;color: #ee2323;&quot;&gt;게다가 메일 발송 후 트랜잭션이 롤백되면 메일은 이미 나간 상태가 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;② @TransactionalEventListener(AFTER_COMMIT) &amp;mdash; 동기, 커밋 후&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777947727555&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;signUp() 호출
  &amp;darr;
① User 저장
  &amp;darr;
② publishEvent() 호출 (이벤트는 일단 큐에 보관됨)
  &amp;darr;
③ 트랜잭션 커밋 ✓
  &amp;darr;
④ 보관된 이벤트의 리스너들 실행
   ├─ WelcomeEmailListener (대기)
   ├─ SlackListener (대기)
   └─ StatisticsListener (대기)
  &amp;darr;
응답 반환&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;롤백 시 메일이 안 나가는 문제는 해결됐지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;여전히 순차 실행이라 응답이 느리다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;③ @Async + @TransactionalEventListener(AFTER_COMMIT) &amp;mdash; 비동기, 커밋 후&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777947754854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;signUp() 호출
  &amp;darr;
① User 저장
  &amp;darr;
② publishEvent() (큐에 보관)
  &amp;darr;
③ 트랜잭션 커밋 ✓
  &amp;darr;
④ 리스너들을 별도 스레드풀로 던지고 즉시 리턴
  &amp;darr;
응답 반환 ⚡ (빠름)

  [백그라운드에서 병렬로]
  ├─ Thread-1: WelcomeEmailListener
  ├─ Thread-2: SlackListener
  └─ Thread-3: StatisticsListener&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;이게 실무에서 가장 많이 쓰는 형태. 본질 작업(회원가입)은 즉시 응답하고, 부수 효과는 백그라운드에서 병렬 처리된다.&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp; ‼️ 이 경우 3번 방식에 대해 주의&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async + ApplicationEventPublisher는 &quot;단일 JVM 안에서만&quot; 동작합니다. 이중화/다중 서버 환경에서는 발행한 서버에서만 리스너가 실행됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777948089373&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;		[Load Balancer]
               /            \
         [Server A]      [Server B]
         (JVM A)         (JVM B)
         
         
         
         
Server A:
  signUp() &amp;rarr; publishEvent(UserSignedUpEvent)
    &amp;darr;
  Server A의 리스너들만 실행됨
    ├─ WelcomeEmailListener ✓
    ├─ SlackListener ✓
    └─ StatisticsListener ✓

Server B: 아무 일도 안 일어남&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 이중화 된 경우에는 A에서 리스너를 사용하면 문제가 없습니다.&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_1777948142451&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[웹 서버 A] &amp;mdash; API 받는 역할
[배치 서버 B] &amp;mdash; 메일 발송 전담&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;문제 케이스: 이벤트를 받는 서버가 발행 서버와 달라야 할 때&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: 주문 서버에서 OrderPlacedEvent를 발행하고, 재고 서버&amp;middot;결제 서버&amp;middot;알림 서버가 각각 받아야 하는 마이크로서비스 구조.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 케이스 2: 처리 보장이 필요한 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async 리스너가 실행되는 도중 서버가 죽으면 &lt;b&gt;이벤트는 그냥 사라집니다&lt;/b&gt;. 메모리에만 있었으니까요. &quot;메일 발송 100% 보장&quot;이 필요하면 이걸로는 안 됩니다.&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;해결책: 외부 메시지 브로커 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 간 이벤트 전파가 필요하면 &lt;b&gt;메시지 큐/브로커&lt;/b&gt;를 도입해야 한다.&lt;/p&gt;
&lt;div&gt;도구특징사용 시점
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Redis Pub/Sub&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;간단, 빠름, 메시지 보장 X&lt;/td&gt;
&lt;td&gt;캐시 무효화 같은 가벼운 신호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메시지 보장, 라우팅 유연&lt;/td&gt;
&lt;td&gt;일반적인 비동기 작업 큐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Kafka&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;대용량, 이벤트 영속성, 재처리 가능&lt;/td&gt;
&lt;td&gt;이벤트 스트리밍, MSA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;AWS SQS/SNS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;관리형, 인프라 부담 적음&lt;/td&gt;
&lt;td&gt;AWS 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;패턴: Spring 이벤트 + 외부 브로커 조합&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 자주 쓰는 패턴입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777948286494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. 도메인 로직: Spring 이벤트 발행 (JVM 내부)
@Service
public class UserService {
    @Transactional
    public void signUp(...) {
        userRepository.save(user);
        eventPublisher.publishEvent(new UserSignedUpEvent(...));
    }
}

// 2. 브릿지 리스너: 트랜잭션 커밋 후 외부 브로커로 전달
@Component
public class UserEventBridge {
    
    @TransactionalEventListener(phase = AFTER_COMMIT)
    public void publishToKafka(UserSignedUpEvent event) {
        kafkaTemplate.send(&quot;user-signed-up&quot;, event);
    }
}

// 3. 다른 서버의 컨슈머가 받아서 처리
@Component
public class WelcomeEmailConsumer {
    
    @KafkaListener(topics = &quot;user-signed-up&quot;)
    public void handle(UserSignedUpEvent event) {
        emailService.sendWelcomeMail(...);
    }
}&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;판단 기준:&lt;/b&gt; &quot;이 이벤트가 발행된 서버 안에서만 처리되어도 비즈니스적으로 문제 없는가?&quot;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 후 메일 발송 &amp;rarr; 어느 서버가 처리하든 OK &amp;rarr; Spring 이벤트로 충분&lt;/li&gt;
&lt;li&gt;모든 서버의 캐시를 갱신해야 함 &amp;rarr; Spring 이벤트로 부족 &amp;rarr; Redis Pub/Sub 필요&lt;/li&gt;
&lt;/ul&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;문제 케이스 2번&lt;/b&gt;에 대해서는 어떤 서버든 실패가 나올 수 있기 때문에, 차라리 이벤트를 보내기 전에 아웃박스로 진행하려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 이벤트 테이블을 만들고, 이벤트를 생성한 로그를 남긴 뒤에 완료시에 SUCCESS 값을 남겨준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 추가로 배치를 만들어&amp;nbsp; EMAIL이 FAIL된 경우 따로 처리하도록 진행을 돕는 것도 방법이 된다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  처리 현황&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777953777185&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;          // 5) 센터장 가입 알림 메일 outbox 적재 + AFTER_COMMIT 발송 트리거
            //    실패 시 외부 catch에서 INTERNAL_SERVER_ERROR로 변환되어 트랜잭션이 롤백된다.
            MailOutboxPayload payload = MailOutboxPayload.builder()
                    .recipientUserName(findUser.getUserName())
                    .centerId(saveCenter.getId())          // 발송 시점에 PII 복호화/파일 lookup
                    .centerName(center.getCenterName())
                    .build();
            String json = objectMapper.writeValueAsString(payload);

            MailOutbox outbox = MailOutbox.create(
                    &quot;Center&quot;,                  // aggregateType
                    saveCenter.getId(),        // aggregateId
                    &quot;CENTER_REGISTERED&quot;,       // eventType
                    json
            );
            MailOutbox saved = mailOutboxRepository.save(outbox);   // 같은 TX1
            eventPublisher.publishEvent(new CenterRegisteredEvent(saved.getId()));

        } catch (Exception e) {
            throw new RestApiException(INTERNAL_SERVER_ERROR);
        }&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;아웃박스 테이블에 create 및 save후에 이벤트를 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777953901156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record CenterRegisteredEvent(Long outboxId) {}

...

@Component
@RequiredArgsConstructor
public class MailOutboxEventListener {
    private final MailDispatcher mailDispatcher;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onCenterRegistered(CenterRegisteredEvent event) {
        mailDispatcher.dispatch(event.outboxId());   // 다른 빈 &amp;rarr; @Async 발화
    }
}

....

@Async(&quot;mailExecutor&quot;)
public void dispatch(Long outboxId) {
    if (!txService.claim(outboxId)) return;        // 단일 진입 보장 (★ 2회 발송 방지)

    MailOutbox outbox = outboxRepository.findById(outboxId).orElse(null);
    if (outbox == null) return;

    try {
        MailOutboxPayload p = objectMapper.readValue(outbox.getPayload(), MailOutboxPayload.class);
        Center center = centerRepository.findById(p.getCenterId());
        if (center == null) {
            throw new IllegalStateException(&quot;Center not found: &quot; + p.getCenterId());
        }

        List&amp;lt;UploadFileDto&amp;gt; files = fileService.findUploadFilesByReferenceIdAndTypeAndStatusForMail(
                p.getCenterId(),
                FileType.BUSINESS_REGISTRATION_CERTIFICATE,
                FileStatus.SAVED,
                Sort.by(Sort.Direction.ASC, &quot;ord&quot;));

        // 평문 PII는 발송 시점에 복호화 (outbox에는 안 들어감)
        SendSaveCenterEmail dto = SendSaveCenterEmail.create(
                center.getCenterName(),
                encryptUtil.decryptLicenseNumber(center.getCenterLicenseNumber()),
                encryptUtil.decryptName(center.getCenterMasterName()),
                encryptUtil.decryptPhone(center.getCenterMasterTel()),
                center.getCenterIntro(),
                center.getCenterTimeCode(),
                center.getCenterAddress(),
                center.getCenterAddressDetail(),
                center.getCenterPostcode(),
                center.getCenterLat(),
                center.getCenterLng(),
                files
        );

        emailService.sendSaveCenterEmail(p.getRecipientUserName(), dto, files);
        txService.markSent(outboxId);
    } catch (Exception e) {
        log.error(&quot;메일 발송 실패 outboxId={}&quot;, outboxId, e);
        txService.markFailed(outboxId, e.getMessage());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 슈팅 박물관</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/437</guid>
      <comments>https://dev0hoon.tistory.com/437#entry437comment</comments>
      <pubDate>Tue, 5 May 2026 13:05:08 +0900</pubDate>
    </item>
    <item>
      <title>Nuxt 3에서 페이지 상태 유지하기 (뒤로가기 시 스크롤/데이터 복원)</title>
      <link>https://dev0hoon.tistory.com/436</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;1. KeepAlive 방식(시도)&lt;/p&gt;
&lt;pre id=&quot;code_1776258516210&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app.vue - NuxtPage에 keepalive 활성화
&amp;lt;NuxtPage :keepalive=&quot;true&quot; /&amp;gt;

// 페이지 파일 - 캐시할 페이지에 keepalive: true
definePageMeta({ layout: 'member', keepalive: true })&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;KeepAlive는 Vue와 Nuxt에서 컴포넌트의 상태를 메모리에 박제(캐싱) 해두는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 A페이지에서 B페이지로 이동하면, A컴포넌트는 파괴(Unmount)되고 메모리에서 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 KeepAlive로 감싸주면 컴포넌트가 파괴되지 않고 '비활성화(Deactivated)' 상태로 메모리에서 머물다가, 다시 돌아왔을 때 이전의 데이터, 입력값, 스크롤 위치 등을 그대로 보여준다.&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;KeepAlive를 사용하면 컴포넌트가 다시 나타날 때 onMounted가 호출되지 않는다. 대신 전용 훅을 사용해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;onActivated&lt;/b&gt;: 캐시된 컴포넌트가 화면에 다시 나타날 때 실행됩니다. (데이터 갱신 로직을 여기에 넣습니다.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;onDeactivated&lt;/b&gt;: 다른 페이지로 이동하여 컴포넌트가 숨겨질 때 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 왈:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KeepAlive는 &lt;b data-index-in-node=&quot;11&quot; data-path-to-node=&quot;22&quot;&gt;&quot;사용자가 입력하던 폼 내용이 많거나, 리스트 렌더링 비용이 너무 커서 0.1초의 딜레이도 없어야 할 때&quot;&lt;/b&gt; 사용하면 좋습니다.&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;/h2&gt;
&lt;pre id=&quot;code_1776258801529&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 목록(/member/feedback/board, layout: member) &amp;rarr; 상세(/common/feedback/detail/123, layout: default) 이동 시 레이아웃이 바뀐다.

2. 레이아웃을 바꿔 시도하려했으나, 해당 목록은 main의 성격을 띄고있어 상세와의 레이아웃이 같지 않다. 
3. 만약 레이아웃을 바꿔서(후에 레이아웃 안에 추가코드가 발생확률 큼) 시도한다면 괜찮을지 시도해봣으나,
이미 작업이 완료된 페이지인 터라, KeepAlive를 적용하려 보니 SSR 컴포넌트 생명주기와 엉켜서 제대로 작동되지 않았다.

원인을 파악하는데에 시간 소요가 클 것이라 판단, Vue에서 권장하는 캐시방식으로 작업을 진행했다.&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;1. 목록에서 돌아온 후 필요한 캐시 값들을 state로 선언해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1776259101182&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const cache = useState&amp;lt;{
  list: FeedbackPost[]
  ranking: FeedbackRanking | null
  category: string
  page: number
  hasMore: boolean
  scrollY: number
} | null&amp;gt;('feedback-board-cache', () =&amp;gt; null)&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;2. 상세 이동 시 캐시 값을 담아준다. 참고로 현재의 scrollY와 현재 page는 페이징을 위해 필수가 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1776259069001&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 상세로 이동 시 &amp;rarr; 현재 상태 저장
const goToDetail = (id: number) =&amp;gt; {
  cache.value = {
    list: feedbackList.value,
    ranking: ranking.value,
    category: activeCategory.value,
    page: currentPage.value,
    hasMore: hasMore.value,
    scrollY: window.scrollY,
  }
  router.push(`/detail/${id}`)
}&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;3. 상세에서 router.back()으로 돌아올 시에 마운트에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 값을 확인하는 코드를 담아서 복원해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1776259161238&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 목록 마운트 시 &amp;rarr; 캐시 있으면 복원, 없으면 fetch
onMounted(async () =&amp;gt; {
  if (cache.value &amp;amp;&amp;amp; cache.value.list.length &amp;gt; 0) {
    // 캐시에서 복원
    feedbackList.value = cache.value.list
    ranking.value = cache.value.ranking
    activeCategory.value = cache.value.category
    currentPage.value = cache.value.page
    hasMore.value = cache.value.hasMore
    const savedScrollY = cache.value.scrollY
    cache.value = null  // 사용 후 캐시 초기화
    isLoading.value = false
    nextTick(() =&amp;gt; {
      setupObserver()
      setTimeout(() =&amp;gt; window.scrollTo(0, savedScrollY), 50)
    })
    return
  }

  // 캐시 없음 &amp;rarr; 새로 API 호출
  isLoading.value = true
  await fetchData()
  isLoading.value = false
})&lt;/code&gt;&lt;/pre&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;/h2&gt;
&lt;pre id=&quot;code_1776259349871&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;목록 (최초 진입)
  &amp;rarr; cache === null &amp;rarr; API fetch &amp;rarr; 렌더링

목록 &amp;rarr; 상세 이동
  &amp;rarr; cache에 {list, scrollY: 500, ...} 저장
  &amp;rarr; 컴포넌트 파괴 (but useState는 살아있음)

상세 &amp;rarr; 뒤로가기
  &amp;rarr; 목록 마운트 &amp;rarr; cache !== null
  &amp;rarr; 캐시에서 복원 (API 안 부름)
  &amp;rarr; scrollTo(500) &amp;rarr; 원래 위치

목록 (탭 메뉴에서 재진입)
  &amp;rarr; cache === null (이전에 초기화됨) &amp;rarr; API fetch&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;☝️ useState&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState와 ref의 차이점은 ref는 컴포넌트 내부에서만 사용되며, useStatesms 전역에서 사용된다. 따라서 페이지 이동시에도 유지가 된다. useStete(키, ()=&amp;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;</description>
      <category>prod/Nuxt.js</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/436</guid>
      <comments>https://dev0hoon.tistory.com/436#entry436comment</comments>
      <pubDate>Wed, 15 Apr 2026 22:22:45 +0900</pubDate>
    </item>
    <item>
      <title>Nuxt 사용기 (1)</title>
      <link>https://dev0hoon.tistory.com/435</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개인 운동기록 &amp;amp; pt 매칭 프로그램을 만들며 Nuxt를 사용하게 됐다. 몇 주간 Nuxt을 개인적으로 공부하며 잡힌 사용 방법에 대해 기록한다.&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;✅ 파일구조&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAkLYA/btsMMHTOfbj/gKaKKgddiJYS73MVuBKvYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAkLYA/btsMMHTOfbj/gKaKKgddiJYS73MVuBKvYK/img.png&quot; data-alt=&quot;https://nuxt.com/docs/guide/directory-structure/layouts#named-layout&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAkLYA/btsMMHTOfbj/gKaKKgddiJYS73MVuBKvYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAkLYA%2FbtsMMHTOfbj%2FgKaKKgddiJYS73MVuBKvYK%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;222&quot; height=&quot;870&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://nuxt.com/docs/guide/directory-structure/layouts#named-layout&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1️⃣ 서문&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;- 컨트롤러 -&amp;gt; 서비스 -&amp;gt; 맵퍼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 컨트롤러 -&amp;gt; 서비스 -&amp;gt; 레파지토리&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터를 운반하는 dto, vo, entity의 개념이 익숙하다.&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;1) domain별로 컨트롤러,서비스,레파지토리, dto, entity로 레이어드 아키텍처 파일구조 이거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) presentation, domain, infrastructure 로 클린 레이어드 아키텍처의 파일구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2가지를 주로 사용한다.&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️⃣ Nuxt의 파일구조에 대한 생각&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 vue에 대한 개념을 잡고 싶어 인프런의 캡틴 판교의 강의를 주로 참고 했다&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742200468915&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;!-- 전체 컨테이너 --&amp;gt;
  &amp;lt;div class=&quot;container&quot;&amp;gt;
    &amp;lt;!-- 상단 타이틀 (검색창 포함) --&amp;gt;
    &amp;lt;ManageHeader&amp;gt;&amp;lt;/ManageHeader&amp;gt;
    &amp;lt;!-- 카테고리 메뉴 --&amp;gt;
    &amp;lt;MemberList&amp;gt;&amp;lt;/MemberList&amp;gt;
    &amp;lt;!-- 하단 고정 푸터 --&amp;gt;
    &amp;lt;footer class=&quot;fixed-footer&quot;&amp;gt;
      &amp;lt;div v-for=&quot;item in category&quot; @click=&quot;selectCategory(item)&quot; class=&quot;footer-item&quot; :class=&quot;{active : selectedCategory == item}&quot; &amp;gt;{{ item }}&amp;lt;/div&amp;gt;
    &amp;lt;/footer&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
  import ManageHeader from &quot;~/components/user/manage/ManageHeader.vue&quot;;
  import {api} from &quot;~/store/api&quot;;
  import {useMemberStore} from &quot;~/store/member&quot;;
  import MemberList from &quot;~/components/user/trainer/MemberList.vue&quot;;

  const memberStore = useMemberStore();
  const category = ref(['회원관리','마이페이지']);
  const selectedCategory = ref('회원관리');

  const selectCategory = (category) =&amp;gt; {
    selectedCategory.value = category;
  }


&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;

&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- page: page폴더에 아래에 들어오는 vue파일들은 자동으로 path에 연결된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) / 는 index.vue&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) /user 는 user폴더 아래의 index.vue&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) /user/list는 user폴더 아래의 list.vue&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) /user/1 은 user폴더 아래의 [id].vue을 가지며 파라미터의 값은 useRouter를 사용해 값을 꺼내게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742200501662&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;div class=&quot;default-nav&quot;&amp;gt;
      &amp;lt;div v-if=&quot;memberStore.useFilter&quot;&amp;gt;총 {{memberStore.filteredMembersCount}}명&amp;lt;/div&amp;gt;
      &amp;lt;div v-if=&quot;!memberStore.useFilter&quot;&amp;gt;총 {{memberStore.membersCount}}명&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;default-nav-item&quot;&amp;gt;
        &amp;lt;button @click.stop=&quot;clickSort&quot; class=&quot;sort-btn&quot;&amp;gt;
          &amp;lt;img src=&quot;@/assets/icons//swap-vertical.svg&quot; alt=&quot;&quot;&amp;gt;
          &amp;lt;span&amp;gt;
                정렬
          &amp;lt;/span&amp;gt;
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;user-container&quot;&amp;gt;
      &amp;lt;!-- 회원이 없을 경우 표시될 문구 --&amp;gt;
      &amp;lt;div style=&quot;display: none;&quot; class=&quot;no-users-message&quot;&amp;gt;등록하신 회원이 없습니다.&amp;lt;/div&amp;gt;

      &amp;lt;div class=&quot;user-item-container&quot;&amp;gt;
        &amp;lt;!-- 회원 정보 --&amp;gt;
        &amp;lt;div class=&quot;user-item&quot; v-for=&quot;item in membersList&quot; :key=&quot;item.id&quot;&amp;gt;
          &amp;lt;NuxtLink :to=&quot;`/record/${item.userId}`&quot; class=&quot;user-info&quot;&amp;gt;
            &amp;lt;div class=&quot;user-image&quot;&amp;gt;
              &amp;lt;img src=&quot;@/assets/images/1.png&quot; alt=&quot;운동 아이콘&quot;&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;user-text&quot;&amp;gt;
              &amp;lt;div class=&quot;user-title&quot;&amp;gt;{{item.userName}}&amp;lt;/div&amp;gt;
              &amp;lt;div v-if=&quot;!item.isNew&quot; class=&quot;user-descript gray-text&quot;&amp;gt;{{item.age}}세 | 마지막수업: {{item.latestCreatedAt}}&amp;lt;/div&amp;gt;
              &amp;lt;div v-if=&quot;item.isNew&quot; class=&quot;user-descript gray-text&quot;&amp;gt;{{item.age}}세 | 새로운 회원&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/NuxtLink&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;!--회원정보--&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;!-- 정렬 모달 --&amp;gt;
    &amp;lt;div v-if=&quot;isShowSortModal&quot; id=&quot;sortModal&quot; class=&quot;user-sort-modal&quot;&amp;gt;
      &amp;lt;ul class=&quot;sort-options&quot;&amp;gt;
        &amp;lt;li @click=&quot;selectSort(item.sort)&quot; v-for=&quot;item in sortTypes&quot; class=&quot;sort-option&quot; &amp;gt;{{item.name}}&amp;lt;/li&amp;gt;
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
  import {useMemberStore} from &quot;~/store/member&quot;;
  import {computed} from &quot;vue&quot;;

  //data
  const memberStore = useMemberStore();
  const isShowSortModal = ref(false);
  const sortTypes = ref([
      {name: '가나다순', sort: 'userName'},
      {name: '최근 수업순', sort: 'recent'}
  ])

  //lifeCycle
  onMounted(async () =&amp;gt; {
    const data = {sortBy : ''}
    const response = await useMember().trainerMember(data);
    memberStore.setMembers(response.result.list);
    memberStore.setMembersCount(response.result.list.length);
  });

  //method
  const selectSort = async (sort) =&amp;gt; {
    const data = {sortBy : sort}
    const response = await useMember().trainerMember(data);
    memberStore.setMembers(response.result.list);
    memberStore.setMembersCount(response.result.list.length);
    clickSort();
  }

  const clickSort = () =&amp;gt; {
    isShowSortModal.value = !isShowSortModal.value;
  }

  //getters
  const membersList = computed(() =&amp;gt; {
    if(memberStore.useFilter){
      return memberStore.filteredMembers.map(m =&amp;gt; ({
        ...m,
        isNew : m.latestCreatedAt == null ? true : false
      }))
    } else {
      return memberStore.members.map(m =&amp;gt; ({
        ...m,
        isNew : m.latestCreatedAt == null ? true : false
      }))
    }

  })


&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;

&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- component: page 안의 html 코드를 공통으로 나누어 관리할 경우 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;? 1) 의문인 부분이다. 공통으로 사용하지 않아도 코드상의 깔끔함을 위해서도 나누어도 될까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;?2) emit과 props을 이용해 page에서 관리되는 데이터를 운용하게 된다. 지금은 store에 필요한 것들을 넣어서 사용 중인데 이래도 되나 싶다. emit과 props를 꼭 써야하는 것일까&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742200586901&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {api} from &quot;~/store/api&quot;;

export const useRecord  = () =&amp;gt; {
    const recordList = async(data) =&amp;gt; {
        try {
            const response = await api().get(`/record/list`,data);
            return response;
        } catch (e) {
            console.error(&quot;[recordList] 요청실패 : &quot;, e);
        }
    }

    return {recordList}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- composable: 공통이 되는 url의 호출을 관리하게 됌&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 예를 들어 page에서 api를 요청해야 한다면, url 호출을 만든다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742200633948&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {defineStore} from 'pinia'

export const useMemberStore = defineStore('member',{
    state: () =&amp;gt; ({
        members: [],
        membersCount: 0,
        filteredMembers: [], // 검색된 회원 목록
        filteredMembersCount: 0,
        useFilter: false,
        sortBy : '',
        searchUserName : ''
    }),
    actions: {
        setMembers(data) {
            this.members = data;
            this.setMembersCount(data.length);
        },
        setMembersCount(data) {
            this.membersCount = data;
        },
        setFilteredMemberCount(data) {
            this.filteredMembersCount = data;
        },
        setSearchUserName(data) {
            this.searchUserName = data;
            this.filteredMembers = this.members.filter(m =&amp;gt;
                m.userName.includes(data)
            )
            let count = this.filteredMembers.length;

            this.setFilteredMemberCount(count);
        },
        setUseFilter(data) {
            this.useFilter = data;
        }
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- store: 한 섹션 안에 전역으로 사용되는 state, action 들을 관리한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) action은 setState명 식으로 만들어서, page에서 조작해왔다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;?2) 컴포서블을 이곳에서 호출하고 page에서 store을 통해 값을 꺼내서 사용하거나 한다던데.. 그렇게 해도 될지 모르겠다. 전체적인 구조가 아직 좀 낯설다.&lt;/b&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;b&gt;그럼 만약 state의 값을 불러올 때 page에서는 onMounted만 해주고 store의 함수가 state의 값을 조절하는 것인지.. 조금 의문임&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>prod/Nuxt.js</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/435</guid>
      <comments>https://dev0hoon.tistory.com/435#entry435comment</comments>
      <pubDate>Mon, 17 Mar 2025 17:46:22 +0900</pubDate>
    </item>
    <item>
      <title>커스텀 컴포지션 함수</title>
      <link>https://dev0hoon.tistory.com/433</link>
      <description>&lt;pre id=&quot;code_1737765965937&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;TodoHeader :appTitle=&quot;appTitle&quot;&amp;gt;&amp;lt;/TodoHeader&amp;gt;
  &amp;lt;TodoInput :todoItems=&quot;todoItems&quot; @add=&quot;addTodo&quot;&amp;gt;&amp;lt;/TodoInput&amp;gt;
  &amp;lt;TodoList :todoItems=&quot;todoItems&quot; @remove=&quot;removeTodo&quot;&amp;gt;&amp;lt;/TodoList&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import TodoHeader from '@/components/TodoHeader.vue'; //vim
import TodoInput from '@/components/TodoInput.vue';
import TodoList from '@/components/TodoList.vue';
import { onBeforeMount, ref } from 'vue';
export default {
  components: {
    TodoHeader,
    TodoInput,
    TodoList
  },
  setup() {
    //data
    const todoItems = ref([]);

    //methods
    function fetchTodos() {
      const result = [];
      for(let i = 0; i &amp;lt; localStorage.length; i++) {
        const todoItem = localStorage.key(i);
        result.push(todoItem);
      }
      return result;
    }

    //라이프 사이클 api가 적용된 구간
    //todoItems.value = fetchTodos();
    
    //화면이 그려지기 전에 동작
    onBeforeMount(() =&amp;gt;{
      todoItems.value = fetchTodos();
    })

    function addTodo(todo) {
      todoItems.value.push(todo);
    }

    return {todoItems, addTodo}
  },
  methods:{
    removeTodo(index) {
      this.todoItems.splice(index, 1);
    }
  }

  ,
  data() {
    return{
      appTitle: '할일앱'
    }
  }
}
&amp;lt;/script&amp;gt;

&amp;lt;style lang=&quot;scss&quot; scoped&amp;gt;

&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위를 보면 소스가 많이 더럽지 않지만, setup안은 실무레벨에서는 엄청나게 길어 질 것으로 보인다.&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;pre id=&quot;code_1737766402103&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/hooks/useTodo.js

import { onBeforeMount, ref } from &quot;vue&quot;;

function useTodo() {

    const todoItems = ref([]);
    
    //methods
    function fetchTodos() {
        const result = [];
        for(let i = 0; i &amp;lt; localStorage.length; i++) {
            const todoItem = localStorage.key(i);
            result.push(todoItem);
        }
        return result;
    }

    function addTodo(todo) {
        todoItems.value.push(todo);
    }
  
    //화면이 그려지기 전에 동작
    onBeforeMount(() =&amp;gt;{
        todoItems.value = fetchTodos();
    })

    return {todoItems, fetchTodos, addTodo }
}

export {useTodo}


//app.vue

import { useTodo } from './hooks/useTodo';
export default {
  setup() {
    const {todoItems,addTodo} = useTodo();
    return {todoItems, addTodo}
  },&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;useTodo.js에서 정의한 data와 methods를 다른 뷰에서 불러와 사용할 수 있다.&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;&lt;/p&gt;
&lt;pre id=&quot;code_1737766799055&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    //화면이 그려지기 전에 동작
    onBeforeMount(() =&amp;gt;{
        todoItems.value = fetchTodos();
    })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 data, methods가 아닌 형태의 코드는 커스텀컴포넌트(js에 따로 뺴는경우)에 있다보면 확인이 어렵다. 이런 코드는 사용하는 .vue 파일에 두는게 옳다.&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;</description>
      <category>dev/vue3</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/433</guid>
      <comments>https://dev0hoon.tistory.com/433#entry433comment</comments>
      <pubDate>Sat, 25 Jan 2025 10:06:24 +0900</pubDate>
    </item>
    <item>
      <title>watch란?</title>
      <link>https://dev0hoon.tistory.com/432</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;watch API도 컴포지션 API에서 사용하는 watch 속성을 의미한다.&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;watch 속성&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnenWK/btsL1YO28WJ/i7uL9LKJk8LhJFglzK4glk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnenWK/btsL1YO28WJ/i7uL9LKJk8LhJFglzK4glk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnenWK/btsL1YO28WJ/i7uL9LKJk8LhJFglzK4glk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnenWK%2FbtsL1YO28WJ%2Fi7uL9LKJk8LhJFglzK4glk%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;759&quot; height=&quot;529&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;1090&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;watch API&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nnzY5/btsL1FB8Agl/XXb0KDaO62Mi1BtebN68kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nnzY5/btsL1FB8Agl/XXb0KDaO62Mi1BtebN68kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nnzY5/btsL1FB8Agl/XXb0KDaO62Mi1BtebN68kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnnzY5%2FbtsL1FB8Agl%2FXXb0KDaO62Mi1BtebN68kk%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;754&quot; height=&quot;490&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1737765824577&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    export default {
        props:['todoItems', 'userId'],
        setup(props, context) {
            function remove(item,index) {
                localStorage.removeItem(item);
                context.emit('remove',index);
            }

            watch(props.todoItems, (newValue)=&amp;gt;{
                console.log({newValue}); 
            })

            return {remove}
        }
    }&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;위처럼 watch를 사용하면 todoItems가 바뀔 때마다 그 새롭게 바뀐 값이 newValue로 넘어온다.&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;watch는 최대한 사용하지 않는 것이 좋다. 많아지면 추적이 어렵기 때문이다. 기존의 emit, props 등을 이용해서 데이터를 운용하는 것이 바람직하다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>dev/vue3</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/432</guid>
      <comments>https://dev0hoon.tistory.com/432#entry432comment</comments>
      <pubDate>Sat, 25 Jan 2025 09:44:57 +0900</pubDate>
    </item>
    <item>
      <title>Lifecycle API 사용하기, mounted</title>
      <link>https://dev0hoon.tistory.com/431</link>
      <description>&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;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;data 속성의 초기화 및 관찰 (Reactivity 주입)&lt;/li&gt;
&lt;li&gt;뷰 템플릿 코드 컴파일 (Virtual DOM -&amp;gt; DOM 변환)&lt;/li&gt;
&lt;li&gt;인스턴스를 DOM에 부착&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;1094&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EhPAk/btsL0H2bkGy/QyWOYh2k7B2pnxIVDKHWlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EhPAk/btsL0H2bkGy/QyWOYh2k7B2pnxIVDKHWlk/img.png&quot; data-alt=&quot;https://joshua1988.github.io/vue-camp/vue/life-cycle.html#%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%83%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%A5%E1%84%80%E1%85%B3%E1%84%85%E1%85%A2%E1%86%B7&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EhPAk/btsL0H2bkGy/QyWOYh2k7B2pnxIVDKHWlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEhPAk%2FbtsL0H2bkGy%2FQyWOYh2k7B2pnxIVDKHWlk%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;1544&quot; height=&quot;1094&quot; data-origin-width=&quot;1544&quot; data-origin-height=&quot;1094&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://joshua1988.github.io/vue-camp/vue/life-cycle.html#%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%83%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%A5%E1%84%80%E1%85%B3%E1%84%85%E1%85%A2%E1%86%B7&lt;/figcaption&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;b&gt;인스턴스 라이프사이클 훅:&lt;/b&gt; 컴포넌트 생명 주기에 따라 특정 로직을 실행할 수 있는 속성 함수. created, beforeMount() 등, 옵션API에서 사용하던 것&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;라이프사이클 API:&lt;/b&gt; 컴포지션 스타일로 작성된 인스턴스 라이프사이클 훅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1398&quot; data-origin-height=&quot;1188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wocgj/btsL1thKBID/bAh1P1TIhyk8XkLxaLuZnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wocgj/btsL1thKBID/bAh1P1TIhyk8XkLxaLuZnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wocgj/btsL1thKBID/bAh1P1TIhyk8XkLxaLuZnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwocgj%2FbtsL1thKBID%2FbAh1P1TIhyk8XkLxaLuZnk%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;1398&quot; height=&quot;1188&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;1188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://joshua1988.github.io/vue-camp/composition/lifecycle.html#%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%86%AB%E1%84%89%E1%85%B3-%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%92%E1%85%AE%E1%86%A8%E1%84%80%E1%85%AA-lifecycle-api%E1%84%8B%E1%85%B4-%E1%84%8E%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%B7&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://joshua1988.github.io/vue-camp/composition/lifecycle.html#%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%86%AB%E1%84%89%E1%85%B3-%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%92%E1%85%AE%E1%86%A8%E1%84%80%E1%85%AA-lifecycle-api%E1%84%8B%E1%85%B4-%E1%84%8E%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%B7&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737764751082&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Lifecycle API   | Cracking Vue.js&quot; data-og-description=&quot;Lifecycle API Vue 3 라이프사이클(Lifecycle) API란 컴포지션(Composition API)에서 사용된 인스턴스 라이프사이클 훅을 의미합니다. 이 페이지에서는 컴포지션에서 인스턴스 라이프사이클 훅을 정의하는 방&quot; data-og-host=&quot;joshua1988.github.io&quot; data-og-source-url=&quot;https://joshua1988.github.io/vue-camp/composition/lifecycle.html#%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%86%AB%E1%84%89%E1%85%B3-%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%92%E1%85%AE%E1%86%A8%E1%84%80%E1%85%AA-lifecycle-api%E1%84%8B%E1%85%B4-%E1%84%8E%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%B7&quot; data-og-url=&quot;https://joshua1988.github.io/vue-camp/composition/lifecycle.html#%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%86%AB%E1%84%89%E1%85%B3-%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%92%E1%85%AE%E1%86%A8%E1%84%80%E1%85%AA-lifecycle-api%E1%84%8B%E1%85%B4-%E1%84%8E%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%B7&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://joshua1988.github.io/vue-camp/composition/lifecycle.html#%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%86%AB%E1%84%89%E1%85%B3-%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%92%E1%85%AE%E1%86%A8%E1%84%80%E1%85%AA-lifecycle-api%E1%84%8B%E1%85%B4-%E1%84%8E%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%B7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://joshua1988.github.io/vue-camp/composition/lifecycle.html#%E1%84%8B%E1%85%B5%E1%86%AB%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%86%AB%E1%84%89%E1%85%B3-%E1%84%85%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%91%E1%85%B3%E1%84%89%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8F%E1%85%B3%E1%86%AF-%E1%84%92%E1%85%AE%E1%86%A8%E1%84%80%E1%85%AA-lifecycle-api%E1%84%8B%E1%85%B4-%E1%84%8E%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%B7&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Lifecycle API   | Cracking Vue.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Lifecycle API Vue 3 라이프사이클(Lifecycle) API란 컴포지션(Composition API)에서 사용된 인스턴스 라이프사이클 훅을 의미합니다. 이 페이지에서는 컴포지션에서 인스턴스 라이프사이클 훅을 정의하는 방&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;joshua1988.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;2246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rlXH2/btsL2lQsQok/zqyILkZUBFFOjysKO30zK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rlXH2/btsL2lQsQok/zqyILkZUBFFOjysKO30zK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rlXH2/btsL2lQsQok/zqyILkZUBFFOjysKO30zK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrlXH2%2FbtsL2lQsQok%2FzqyILkZUBFFOjysKO30zK1%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;622&quot; height=&quot;1015&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;2246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1737765177478&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;setup(){
    //라이프 사이클 api가 적용된 구간
    //todoItems.value = fetchTodos();

    console.log('setup called');

    //화면이 그려지기 전에 동작
    onBeforeMount(() =&amp;gt;{
      console.log('onBeforeMount called');
      todoItems.value = fetchTodos();
    })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서상 setup called 이후 onBeforeMount called가 발생하게 된다.&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;onUnMounted는 컴포넌트가 사라졌을 때라는데.. 다른 화면으로 갈 때 일어나는 거랑 비슷할까?&lt;/p&gt;</description>
      <category>dev/vue3</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/431</guid>
      <comments>https://dev0hoon.tistory.com/431#entry431comment</comments>
      <pubDate>Sat, 25 Jan 2025 09:36:55 +0900</pubDate>
    </item>
    <item>
      <title>setup()에서 props 접근시 주의 점</title>
      <link>https://dev0hoon.tistory.com/430</link>
      <description>&lt;pre id=&quot;code_1737763921245&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        setup(props, context) {
            function remove(item,index) {
                localStorage.removeItem(item);
                context.emit('remove',index);
            }

            props.todoItems
            props.userId
            return {remove}
        }
        
        위처럼 코드를 생각하면
        
        setup({todoItems, userId}, context) {
            function remove(item,index) {
                localStorage.removeItem(item);
                context.emit('remove',index);
            }

            props.todoItems
            props.userId
            return {remove}
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 props를 디스트럭쳐링을 하면 안된다. 디스트럭쳐링을 하는 순간, vue의 반응성(reactivity)가 사라지게 된다. 그래서 props는 있는 그대로 사용해야한다.&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;만약 디스트럭쳐링을 하면, props의 값이 바뀌어도 해당 함수의 값은 변하지 않게&lt;/b&gt; 된다!&lt;/span&gt;&lt;/p&gt;</description>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/430</guid>
      <comments>https://dev0hoon.tistory.com/430#entry430comment</comments>
      <pubDate>Sat, 25 Jan 2025 09:18:05 +0900</pubDate>
    </item>
    <item>
      <title>computed API란?</title>
      <link>https://dev0hoon.tistory.com/429</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨티드 api는 컴포지션 Api에서 사용된 컴퓨티드 속성을 의미한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737763069705&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;{{ reversedMessage }}&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('');
    }
  }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열을 반대로 정리해주는 computed가 보인다. 특정 연산을 담아두는 속성이라고 보면 된다. 기본이 되는 data를 다른 값으로 표현해주게끔 사용한다. (converter 같은 느낌도 들고?)&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;위는 속성으로 'computed 속성'이라 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737763491234&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;h1&amp;gt;{{appTitle}}&amp;lt;/h1&amp;gt;
    &amp;lt;h4&amp;gt;{{ newTitle }}&amp;lt;/h4&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
import { computed } from 'vue';

export default {
    props:['appTitle'],
    setup(props) {
        
        const newTitle = computed(()=&amp;gt;{
            return props.appTitle + '!!';
        })

        return {newTitle};
    }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포지션 API에서는 컴퓨티드API라고 부른다. 여기에서 왜 컴퓨티드가 아니라 메소드를 사용하는 걸까?&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;computed와 method의 차이점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;캐싱&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computed는 &lt;b&gt;결과를 캐싱&lt;/b&gt;해.&lt;/li&gt;
&lt;li&gt;즉, computed에 사용된 종속 데이터(의존성)가 변하지 않는 이상 재계산을 하지 않아.&lt;/li&gt;
&lt;li&gt;반면 method는 호출할 때마다 &lt;b&gt;다시 실행&lt;/b&gt;돼.&lt;/li&gt;
&lt;li&gt;이 차이가 성능에 중요한 영향을 줄 수 있어&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1737763641387&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default {
  setup() {
    const count = ref(1);

    const computedValue = computed(() =&amp;gt; {
      console.log(&quot;Computed 재계산&quot;);
      return count.value * 2;
    });

    const methodValue = () =&amp;gt; {
      console.log(&quot;Method 실행&quot;);
      return count.value * 2;
    };

    return { count, computedValue, methodValue };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computedValue는 종속된 count.value가 변하지 않으면 콘솔에 로그가 출력되지 않아(캐싱).&lt;/li&gt;
&lt;li&gt;반면, methodValue는 호출할 때마다 로그가 출력돼.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computed는 &lt;b&gt;반응형 시스템&lt;/b&gt;과 밀접하게 연동돼 있어.&lt;/li&gt;
&lt;li&gt;computed 값을 사용하는 컴포넌트는 &lt;b&gt;자동으로 업데이트&lt;/b&gt;돼.&lt;/li&gt;
&lt;li&gt;하지만 method를 사용하면 해당 메서드를 수동으로 호출해야 값이 갱신돼.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1737763679072&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;Computed: {{ computedValue }}&amp;lt;/div&amp;gt;
  &amp;lt;div&amp;gt;Method: {{ methodValue() }}&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computedValue는 count가 변할 때 자동으로 UI가 업데이트돼.&lt;/li&gt;
&lt;li&gt;하지만 methodValue()는 수동으로 호출되기 때문에 더 자주 호출되고, 성능상 비효율적일 수 있어.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;computed를 사용하는 이유와 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;성능 최적화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐싱 덕분에 계산량이 많은 작업에 효율적이야.&lt;/li&gt;
&lt;li&gt;예를 들어, 대량의 데이터 필터링이나 복잡한 계산이 필요할 때 computed가 성능적으로 훨씬 유리해.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 가독성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computed를 사용하면 상태와 로직을 더 &lt;b&gt;선언적&lt;/b&gt;으로 작성할 수 있어.&lt;/li&gt;
&lt;li&gt;&quot;이 값은 이 데이터를 기준으로 계산된다&quot;는 의도를 코드에서 명확히 표현할 수 있지.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computed 값은 다른 computed나 watch에서 쉽게 재사용할 수 있어.&lt;/li&gt;
&lt;li&gt;이런 재사용성은 복잡한 프로젝트에서 특히 유용해.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반응형 데이터와의 통합&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computed는 Vue의 &lt;b&gt;반응형 시스템&lt;/b&gt;과 긴밀히 연결되어 있어.&lt;/li&gt;
&lt;li&gt;Vue가 알아서 데이터의 종속성을 추적해 업데이트를 관리해 주지.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;computed는 &lt;b&gt;캐싱&lt;/b&gt;과 &lt;b&gt;반응형 업데이트&lt;/b&gt; 덕분에 성능 최적화와 개발자 경험에 유리해.&lt;/li&gt;
&lt;li&gt;method는 호출할 때마다 실행되기 때문에 간단한 작업에는 적합할 수 있지만, 복잡한 연산이 필요하거나 UI 업데이트와 밀접한 작업에는 computed가 훨씬 효율적이야.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>dev/vue3</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/429</guid>
      <comments>https://dev0hoon.tistory.com/429#entry429comment</comments>
      <pubDate>Sat, 25 Jan 2025 09:08:52 +0900</pubDate>
    </item>
    <item>
      <title>composition API에서는 왜 setup에서 .value로 접근해야 할까?</title>
      <link>https://dev0hoon.tistory.com/428</link>
      <description>&lt;pre id=&quot;code_1737762416807&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;createApp(){
	setup(){
    	const message = ref('hi');
        
        const changeMessge = () =&amp;gt; {
        	message.value = 'hello';
        }
        
        return { message }
    }
}&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;ref가 없다는 가정하에 'hi'만 message로 초기화 할 경우 'hi'가 바뀐 것을 추적하려면 proxy같은 것이 필요하다.&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;a href=&quot;https://dev0hoon.tistory.com/404&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev0hoon.tistory.com/404&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737762543003&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Vue 3 Reactivity - Proxy 소개&quot; data-og-description=&quot;new Proxy()를 통해서 프록시 객체를 만들었다. 프록시란 값을 넣어 모방하는 객체라고 볼 수 있다. 가짜 객체와도 비슷한데 여기에서는 값을 사용할 때에 추가적인 동작을 할 수 있게 만들었다.&amp;nbsp; v&quot; data-og-host=&quot;dev0hoon.tistory.com&quot; data-og-source-url=&quot;https://dev0hoon.tistory.com/404&quot; data-og-url=&quot;https://dev0hoon.tistory.com/404&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Ca4Lb/hyX7UTGinY/LQRKvNKvx1syUbiiwkaZU1/img.png?width=800&amp;amp;height=221&amp;amp;face=0_0_800_221,https://scrap.kakaocdn.net/dn/cMKvWP/hyX4nwug7l/e0TkgO19k3D0hwiuXeKMq1/img.png?width=800&amp;amp;height=221&amp;amp;face=0_0_800_221,https://scrap.kakaocdn.net/dn/cWCDGZ/hyX70M9MCR/bKORQhPIl1qDEbiKftcLMK/img.png?width=753&amp;amp;height=601&amp;amp;face=0_0_753_601&quot;&gt;&lt;a href=&quot;https://dev0hoon.tistory.com/404&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev0hoon.tistory.com/404&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Ca4Lb/hyX7UTGinY/LQRKvNKvx1syUbiiwkaZU1/img.png?width=800&amp;amp;height=221&amp;amp;face=0_0_800_221,https://scrap.kakaocdn.net/dn/cMKvWP/hyX4nwug7l/e0TkgO19k3D0hwiuXeKMq1/img.png?width=800&amp;amp;height=221&amp;amp;face=0_0_800_221,https://scrap.kakaocdn.net/dn/cWCDGZ/hyX70M9MCR/bKORQhPIl1qDEbiKftcLMK/img.png?width=753&amp;amp;height=601&amp;amp;face=0_0_753_601');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Vue 3 Reactivity - Proxy 소개&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;new Proxy()를 통해서 프록시 객체를 만들었다. 프록시란 값을 넣어 모방하는 객체라고 볼 수 있다. 가짜 객체와도 비슷한데 여기에서는 값을 사용할 때에 추가적인 동작을 할 수 있게 만들었다.&amp;nbsp; v&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev0hoon.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 vue를 사용할 때 자연스레 reactivity를 사용하고 있어서 못느끼겠지만 사실 내부에서는 값이 바뀔 때마다 set(), get()을 해준다고 봐야한다. 그걸 ref() 함수로 대체했다는 것이 이해하기 좋다.&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;a href=&quot;https://vuejs.org/guide/essentials/reactivity-fundamentals.html#why-refs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://vuejs.org/guide/essentials/reactivity-fundamentals.html#why-refs&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737762669909&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Vue.js&quot; data-og-description=&quot;Vue.js - The Progressive JavaScript Framework&quot; data-og-host=&quot;vuejs.org&quot; data-og-source-url=&quot;https://vuejs.org/guide/essentials/reactivity-fundamentals.html#why-refs&quot; data-og-url=&quot;https://vuejs.org/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kpblD/hyX73XpOrZ/cYLqaKWQmfmQYK7PH7GKw1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://vuejs.org/guide/essentials/reactivity-fundamentals.html#why-refs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://vuejs.org/guide/essentials/reactivity-fundamentals.html#why-refs&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kpblD/hyX73XpOrZ/cYLqaKWQmfmQYK7PH7GKw1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vue.js - The Progressive JavaScript Framework&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;vuejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1518&quot; data-origin-height=&quot;1218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zygck/btsL2Jjgw8R/MXU2AIh9J2104gca18eP50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zygck/btsL2Jjgw8R/MXU2AIh9J2104gca18eP50/img.png&quot; data-alt=&quot;vue.js3 공식 문서의 설명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zygck/btsL2Jjgw8R/MXU2AIh9J2104gca18eP50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzygck%2FbtsL2Jjgw8R%2FMXU2AIh9J2104gca18eP50%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;1518&quot; height=&quot;1218&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;1218&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;vue.js3 공식 문서의 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1737762844432&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const num = 0; x

const num = {
	value : 0,
    get() {
    },
    set() {
    }
}&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;</description>
      <category>dev/vue3</category>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/428</guid>
      <comments>https://dev0hoon.tistory.com/428#entry428comment</comments>
      <pubDate>Sat, 25 Jan 2025 08:55:59 +0900</pubDate>
    </item>
    <item>
      <title>vue3 컴포서블 사용, 디스터럭쳐링 문법</title>
      <link>https://dev0hoon.tistory.com/427</link>
      <description>&lt;pre id=&quot;code_1737632369736&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/composible/useMessage.js

import { ref } from &quot;vue&quot;;

function useMessage() {
    //data
    const message = ref('hello');

    //methods
    const changeMessage = () =&amp;gt; {
    message.value = 'hi';
    }

    return {message, changeMessage}
}

export { useMessage }



/App.vue

export default {
     setup() {
      const {message, changeMessage} = useMessage;


꺼내기가 가능하다.&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;위처럼 useMessage.message가 아닌 const에서 객체상태로 꺼낼 수 있는 것을 디스터럭쳐링 문법이라한다. es6에서 사용하는 문법이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 따로 js에 소스를 관심사 분리를 해두는것이 컴포서블 api인데 깔끔하지만, 나중에는 소스를 보기 위해 이 파일 저 파일을 옮겨다녀야 해서 비용이 많이 나가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737632504459&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {
  a:10,
  b:'hi'
}

const {a,b} = obj;

이렇게 사용한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>dev_0hoon</author>
      <guid isPermaLink="true">https://dev0hoon.tistory.com/427</guid>
      <comments>https://dev0hoon.tistory.com/427#entry427comment</comments>
      <pubDate>Thu, 23 Jan 2025 20:44:18 +0900</pubDate>
    </item>
  </channel>
</rss>