"분명 탈퇴 여부를 검사하는 if문이 있는데, 왜 탈퇴 계정 복구가 전혀 동작하지 않았을까?" 소프트 딜리트(Soft Delete)를 구현할 때 흔히 사용하는 Hibernate의 전역 엔티티 필터인 @SQLRestriction 이 어떻게 5개 비즈니스 로직을 조용히 '죽은 코드(Dead Code)'로 만들어버렸는지, 그리고 이를 어떻게 해결했는지에 대한 실전 백엔드 트러블슈팅 기록입니다. 🚨 1. 증상: 조용히 마비되어 있던 탈퇴 계정 복구 기능들 보안 코드 리뷰를 진행하던 중, 놀랍게도 서비스 전반에서 '탈퇴(Soft Delete) 회원의 계정 복구'와 관련된 모든 경로가 사실상 100% 마비 되어 있다는 사실을 발견했습니다: 비밀번호 로그인 시 : 탈퇴 회원이 정확한 이메일/비밀번호를 쳐도 복구 안내( DELETED_ACCOUNT ) 대신 단순 *"이메일 또는 비밀번호가 올바르지 않습니다"*만 반환. 탈퇴 계정 복구 시도 시 : 비밀번호 기반 복구든 이메일 인증 기반 복구든, 항상 *"복구 가능한 탈퇴 계정이 없습니다"*라는 에러만 발생. 소셜(OAuth) 로그인 시 (가장 심각) : 탈퇴한 회원이 카카오/구글 로그인을 시도하면, 복구 안내를 띄우는 게 아니라 아예 기존 이메일을 무시하고 신규 가입 화면으로 이동 . 정상 회원들은 아무 문제 없이 로그인되었기 때문에, 이 버그는 운영 환경에서 아무런 에러 로그도 남기지 않은 채 조용히 방치되어 있었습니다. 🔍 2. 원인 분석: @SQLRestriction 이 만든 보이지 않는 벽 엔티티 코드를 열어보자마자 범인이 드러났습니다: @Entity @Table(name = "users") @SQLRestriction("is_deleted = false") // 💥 Hibernate 전역 필터 public class User { // ... private boolean isDeleted; } JPA에서 탈퇴 회원을 일반 조회에서 자동으로 제외하기 위해 걸어둔 클래스 레벨의 @SQLRestriction("is_deleted = false") 이 문제였습니다. 이 어노테이션이 붙으면, UserRepository.findByEmail() 을 포함해 Spring Data JPA가 생성하는 모든 파생 쿼리와 연관관계 조회에 무조건 AND is_deleted = false 조건이 강제로 삽입 됩니다. 비즈니스 로직에 숨어있던 '죽은 코드(Dead Code)' // AuthService.java public LoginResult login(LoginRequest request) { // ❌ findByEmail()은 애초에 is_deleted = false 조건 때문에 탈퇴 회원을 절대 반환하지 않는다! (항상 null) User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new BadCredentialsException("이메일 또는 비밀번호 불일치")); // 💥 아래의 if문은 영원히 실행될 수 없는 100% '죽은 코드'였다! if (user.isDeleted()) { return LoginResult.deletedAccount(); } // ... } 개발자는 당연히 user.isDeleted() 분기를 꼼꼼하게 작성해 두었지만, 정작 JPA 조회 단계에서 이미 탈퇴 회원이 걸러져 null 이 리턴되므로 예외가 터져버려 해당 if문에는 평생 도달할 수 없었던 것 입니다. 이와 똑같은 실수가 복구 로직, 소셜 로그인 성공 핸들러, 인증 코드 발송 로직 등 총 5곳의 메서드에 복사-붙여넣기처럼 퍼져 있었습니다. 🛠️ 3. 해결책: 전역 필터를 우회하는 명시적 네이티브 쿼리 도입 Hibernate의 @SQLRestriction 은 JPQL이나 파생 메서드 쿼리에는 무조건 붙지만, nativeQuery = true 로 작성된 순수 SQL에는 개입하지 않습니다. 1 전용 네이티브 조회 메서드 작성 public interface UserRepository extends JpaRepository<User, Long> { // 일반 조회: @SQLRestriction이 적용되어 활성 회원(is_deleted = false)만 조회 Optional<User> findByEmail(String email); // 🚀 탈퇴 회원 포함 조회: 전역 필터를 우회하기 위해 Native Query 사용 @Query(value = "SELECT * FROM users WHERE lower(email) = lower(:email)", nativeQuery = true) Optional<User> findByEmailIncludingDeleted(@Param("email") String email); } 2 탈퇴 여부를 검사해야 하는 5곳의 호출부 전면 교체 AuthService.login() : findByEmailIncludingDeleted 로 교체하여 DELETED_ACCOUNT 시그널 정상 반환. AuthService.restoreAccountWithPassword() / restoreAccount() : 탈퇴 회원을 정확히 조회해 is_deleted = false 로 원상복구. OAuth2SuccessHandler : 탈퇴 회원이 소셜 로그인 시 신규 가입이 아닌 복구 페이지( /restore )로 올바르게 분기. 🧪 4. 검증: 회귀 테스트 추가 AuthServiceTest 에 탈퇴 계정 시나리오를 꼼꼼하게 추가했습니다: 탈퇴한 계정으로 로그인 시도 시 DELETED_ACCOUNT 예외/응답이 정상 반환되는가? 비밀번호/이메일 인증 복구 시 실제로 계정이 활성화되는가? 이미 활성 상태인 회원이 복구 API를 호출하면 올바르게 거절되는가? ➡️ 테스트 4건 모두 작성 및 통과! 💡 이번 트러블슈팅을 통해 얻은 교훈 엔티티 전역 필터(@SQLRestriction, @Where)의 위험성 : 전역 필터는 편리하지만, 코드를 읽는 사람에게 조건을 "암묵적으로 은폐"시킵니다. 비즈니스 로직 작성자는 쿼리 레벨에서 이미 필터링되었다는 사실을 잊고 중복 분기 처리를 하다가 버그를 만들기 쉽습니다. 우회 메서드의 네이밍과 경고 주석 : findByEmailIncludingDeleted 처럼 "이 조회가 전역 필터를 깬다"는 사실을 이름에 명확히 드러내야 합니다. 원본 findByEmail() 의 Javadoc에도 *"탈퇴 회원 조회가 필요한 경우 반드시 findByEmailIncludingDeleted 를 사용하세요"*라는 주석을 남겨 다음 개발자의 실수를 방지해야 합니다.
0. 들어가며 개인적으로 사내 에이전트 기반 제품 개발 프로젝트를 리드할 때에는 오픈소스 프레임워크를 사용하지는 않았습니다. 당시 검토한 오픈소스 프레임워크는 제품에 필요한 범위에 비해 방대하고 무거웠기 때문에 필요한 개념만 취해서 팀에서 직접 구현하는 식이었습니다. 그러다가 26년 기준 에이전트 프레임워크의 세계는 어떻게 변했을지, 각 프레임워크별 차이가 무엇이고 어떤 설계 사상과 기준이 반영돼 있을지 코드 수준에서 알아보고 싶어졌습니다. 그래서 Codex의 도움을 받아 주요 오픈소스 프레임워크의 실제 실행 경로를 따라가 봤습니다. 오늘 글에서는 기능 지원 여부를 나열하기보다, 각 프레임워크가 ‘Agent를 실행한다’는 문제를 어떻게 다르게 풀고 있는지를 중심으로 살펴보겠습니다. Framework 코드에서 드러나는 핵심 구조 LangGraph 상태를 가진 Graph Runtime 이 실행을 주도 OpenAI Agents SDK Agent와 Runner를 중심으로 한 명확한 Agent Loop Google ADK Agent를 Workflow를 구성하는 Node 로 통합 Microsoft Agent Framework Agent 실행에 Context·Session·Workflow 를 함께 결합 PydanticAI Agent 내부를 Typed State Machine / Graph 로 구현 CrewAI Role·Task 기반 구조에 Flow / Planning Runtime 을 결합 smolagents ReAct Loop가 직접 드러나며 CodeAgent를 주요 특징으로 하는 최소 추상화 Strands Agent Loop에서 Runtime / Harness 영역으로 확장 1. 다 지원한다는데, 왜 Agent Framework는 이렇게 많이 필요한가? 주요 Agent Framework의 기능을 표로 정리해보면 생각보다 큰 차이가 없어 보이는데요. Tool Calling, Memory나 State, Multi-Agent, Human-in-the-loop, Persistence 같은 기능은 이제 대부분의 프레임워크가 지원합니다. Framework Tool Memory / State Multi-Agent HITL Persistence LangGraph O O O O O OpenAI Agents SDK O O O O O Google ADK O O O O O Microsoft Agent Framework O O O O O PydanticAI O O O O O CrewAI O O O O O smolagents O O O 제한적 제한적 Strands O O O O O 이 표만 보면 어떤 프레임워크를 사용해도 크게 다르지 않아 보입니다. 문제는 같은 기능 이름이 실제로 같은 동작을 의미하지 않는다는 것 입니다. 예를 들어 Multi-Agent 는 어떤 프레임워크에서는 다른 Agent를 Tool처럼 호출하고 결과만 돌려받는 것을 의미하지만, 다른 프레임워크에서는 대화의 주도권 자체를 넘기는 Handoff이거나 Graph의 다른 Node나 Subgraph로 이동하는 것을 의미합니다. Persistence 역시 대화 내역을 저장하는 것부터 중단된 Agent Run을 이어가는 것, 실행 중이던 Graph의 어느 단계까지 완료됐는지를 복구하는 것까지 범위가 다릅니다. 그래서 Tool 지원 O , Multi-Agent 지원 O 같은 기능표에서 한 단계 더 내려가 실제 코드를 살펴보았습니다. 가장 작은 실행 단위가 무엇인지, 다음 행동을 Model과 Framework 중 누가 결정하는지, 무엇을 State로 저장하고 어디까지 복구하는지 등등 핵심 개념들을 코드 수준에서 따라가보니 프레임워크별 차이가 훨씬 선명하게 드러났습니다. 2. Loop: Agent 실행의 가장 기본적인 형태 Agent를 가장 단순하게 구현하면 Model을 호출하고, 필요한 Action을 실행하고, 그 결과를 다시 Model에 넣는 과정을 종료 조건이 충족될 때까지 반복 하게 되는데, 이런 실행 구조를 보통 Agent Loop 라고 부릅니다. OpenAI Agents SDK, Google ADK, Strands 등을 보면 형태는 다르지만 내부에 이런 반복 구조가 존재합니다. 차이는 '한 번'의 단위를 어떻게 정할 것인가 , 그리고 반복을 계속할지 누가 결정하는가 입니다. 가장 직접적인 형태는 smolagents 에서 볼 수 있습니다. smolagents 는 2024년 12월 31일 Hugging Face가 처음 공개한 오픈소스 Agent 라이브러리로, 기존 transformers.agents 의 후속 프로젝트입니다. Hugging Face는 공개 당시부터 가능한 한 적은 추상화로 Agent를 구현 하는 것을 주요 방향으로 제시했습니다. ( Hugging Face의 smolagents 최초 공개 글 ) 그럼 실제 코드를 열어볼까요? agents.py 의 MultiStepAgent._run_stream() 을 보면 Loop의 종료 조건을 알 수 있습니다. self.step_number = 0 ...(중간 생략) self.step_number = 1 returned_final_answer = False while not returned_final_answer and self.step_number <= max_steps: ... Source: Hugging Face smolagents · src/smolagents/agents.py · MultiStepAgent._run_stream() · Apache-2.0 · 2026-10-01 main 기준 · 코드 원문 MultiStepAgent 의 중심에는 ActionStep을 반복하는 ReAct Loop가 있습니다. 여기에 필요에 따라 Action Step 사이에 별도의 PlanningStep을 실행할 수 있고, 각 Step에는 callback, error handling, final-answer validation 같은 실행 제어도 붙습니다. 핵심 Control Flow가 직접적으로 드러난다는 점에서 다른 프레임워크보다 구조를 읽기 쉽습니다. _step_stream() 의 정의를 볼까요? def _step_stream( self, memory_step: ActionStep ) -> Generator[ChatMessageStreamDelta | ToolCall | ToolOutput | ActionOutput]: """ Perform one step in the ReAct framework: the agent thinks, acts, and observes the result. Yields ChatMessageStreamDelta during the run if streaming is enabled. At the end, yields either None if the step is not final, or the final answer. """ Source: Hugging Face smolagents · src/smolagents/agents.py · MultiStepAgent._step_stream() · Apache-2.0 · 2026-10-01 main 기준 · 코드 원문 주석을 번역해보면 다음과 같습니다. "ReAct 프레임워크의 한 단계를 수행합니다. 에이전트는 사고하고(think), 행동한 뒤(act), 그 결과를 관찰합니다(observe). 스트리밍이 활성화된 경우 실행 중에 ChatMessageStreamDelta를 반환합니다. 단계가 끝나면 최종 단계가 아닌 경우 None을 반환하고, 최종 단계인 경우 최종 답변을 반환합니다." 구조를 간단히 도식화해보면 다음과 같습니다. Model이 현재까지의 Context를 보고 다음 Action을 결정하고, Framework가 그 Action을 실행합니다. 실행 결과인 Observation은 다시 Memory에 쌓이고 다음 Model 호출의 입력이 됩니다. Final Answer가 나올 때까지 이 과정이 반복됩니다. 실제로 보면 크게 ToolCallingAgent와 CodeAgent로 나뉘는데요. 각 Step에서는 모델이 다음 Action을 생성합니다. ToolCallingAgent에서는 Tool Call이 Action이 됩니다. CodeAgent에서는 실행 가능한 Python 코드가 Action이 됩니다. 실행 결과는 Observation으로 AgentMemory에 남고 다음 Step의 Model Input에 다시 포함됩니다. 이렇게 Core Loop만 놓고 보면 구조가 단순해보이죠. 하지만 다른 Framework를 더 뜯어보면, 이 기본 Loop의 골격 위에 무엇을 추가하느냐에 따라 구조가 많이 달라집니다. OpenAI Agents SDK: Loop를 Turn 단위의 상태 전이로 관리한다 OpenAI Agents SDK로 넘어가보겠습니다. 기본적인 실행 원리는 smolagents 와 크게 다르지 않습니다. Runner.run() 의 docstring을 보면, Agent를 Final Output이 만들어질 때까지 Loop로 실행한다 고 직접 설명합니다. """ Run a workflow starting at the given agent. The agent will run in a loop until a final output is generated. The loop runs like so: 1. The agent is invoked with the given input. 2. If there is a final output, the loop terminates. 3. If there's a handoff, we run the loop again, with the new agent. 4. Else, we run tool calls (if any), and re-run the loop. """ 여기서 Turn은 “한 번의 AI invocation과 그 과정에서 발생하는 Tool Call”을 묶은 단위 로 정의하고 있습니다. max_turns: The maximum number of turns to run the agent for. A turn is defined as one AI invocation (including any tool calls that might occur). Source: OpenAI Agents SDK · src/agents/run.py · Runner.run() · MIT · 2026-10-01 main 기준 코드 원문 — run.py 실제 상위 실행부를 보면 Runner 안에는 큰 while True 가 있고, Loop가 돌 때마다 current_turn 을 하나씩 증가시킨 뒤 run_single_turn() 을 호출합니다. max_turns 도 바로 이 카운터를 기준으로 검사하고 있습니다. while True: ... current_turn += 1 ... turn_result = await run_single_turn(...) Source: OpenAI Agents SDK · src/agents/run.py · AgentRunner.run() 내부 main loop · MIT · 2026-10-01 main 기준 코드 원문 — run.py 도식으로 그려볼까요? 여기서 중요한 함수가 run_single_turn() 입니다. 코드 주석을 보면 “Run a single non-streaming turn of the agent loop.” 라고 정의합니다. 한 Turn 안에서는 먼저 현재 Agent의 System Prompt와 Prompt 설정을 가져오고, 사용할 Handoff와 Tool 목록, Output Schema를 결정합니다. 이전 Turn까지 생성된 Item도 현재 입력과 합쳐 Model Input을 만듭니다. 그리고 실제 Model 호출은 get_new_response() 로 넘어갑니다. new_response = await get_new_response(...) ... return await get_single_step_result_from_response(...) Source: OpenAI Agents SDK · src/agents/run_internal/run_loop.py · run_single_turn() · MIT · 2026-10-01 main 기준 코드 원문 — run_loop.py 이 두 호출 사이가 OpenAI Agents SDK의 한 Turn을 이해하는 데 중요합니다. get_new_response() 는 말 그대로 현재 Turn의 Model 호출을 담당합니다. 현재 Agent와 RunConfig에서 Model과 Model Settings를 가져온 다음 model.get_response() 를 호출하는데, 이때 단순한 Prompt만 넘기는 것이 아니라 현재 사용할 Tool, Output Schema, Handoff 목록도 함께 전달하는 것입니다. 현재까지 단계를 요약해볼까요? 흥미로운 부분은 그 다음입니다. Model이 돌려준 ModelResponse 를 Runner가 곧바로 검사해서 “Tool을 실행할지, Handoff할지, 끝낼지”를 결정하지 않습니다. 먼저 get_single_step_result_from_response() 로 넘기고, 여기서 process_model_response() 를 호출해 Provider가 반환한 Raw Output을 SDK가 이해할 수 있는 실행 단위로 변환 합니다. processed_response = process_model_response(...) ... return await execute_tools_and_side_effects(...) Source: OpenAI Agents SDK · src/agents/run_internal/turn_resolution.py · get_single_step_result_from_response() · MIT · 2026-10-01 main 기준 코드 원문 — turn_resolution.py Run Item Lifecycle 이 중간 단계가 필요한 이유는 Model의 출력이 하나의 종류가 아니기 때문입니다. 한 번의 Model 호출에서는 일반적인 Assistant Message뿐 아니라 Function Tool Call, Handoff를 나타내는 Tool Call, Computer Action, Shell Call, Approval이 필요한 호출 등이 함께 나올 수 있습니다. OpenAI Agents SDK는 이 Provider Output을 두 종류의 내부 표현으로 나눕니다. 첫 번째는 RunItem 입니다. 이것은 해당 Turn에서 “무슨 일이 발생했는가”를 표현하는 공개적인 기록 같은 것입니다. 이후 RunResult , Streaming Event, Session History, Trace 등에 사용됩니다. 두 번째는 실제로 SDK가 실행해야 할 내부 Tool Run Record 입니다. 예를 들어 Function Tool Call이라면 SDK가 실제로 호출할 Python Tool 객체, Call ID, Routing 정보, Approval 상태 등을 함께 보존하는 것입니다. 공식 Runner 내부 문서를 보면 이 구분이 명시되어 있습니다. process_model_response() 는 ModelResponse.output 을 public RunItem 과 internal executable tool-run record로 변환해 ProcessedResponse 에 담는다 고 설명합니다. process_model_response() converts recognized output into public RunItem objects and internal executable tool-run records in ProcessedResponse . 출처: https://github.com/openai/openai-agents-python/blob/main/src/agents/run_internal/turn_resolution.py ProcessedResponse 는 이 둘을 한데 모아 현재 Turn에서 무엇이 생성됐고, 그중 실제로 무엇을 실행해야 하는지 를 정리한 중간 표현입니다. 예를 들어 모델이 다음과 같은 응답을 냈다고 가정해보겠습니다. - 일반 메시지 - get_weather() 호출 - transfer_to_support_agent() 호출 Provider 입장에서는 모두 하나의 Model Response 안에 들어 있는 Output Item입니다. 하지만 Runner 입장에서는 성격이 전혀 다릅니다. 일반 메시지는 결과와 History에 남길 RunItem get_weather() 는 실제 Function Tool을 찾아 실행해야 하는 작업 transfer_to_support_agent() 는 단순 Function Tool이 아니라 현재 Agent를 교체할 수 있는 Handoff 후보 그래서 process_model_response() 단계는 말하자면 “Model이 무엇을 출력했는가”를 “SDK가 무엇을 해야 하는가”로 번역 하는 것이지요. 공식 문서에서도 이 처리 흐름을 다음 순서로 설명합니다. Provider adapter가 ModelResponse.output 을 반환하면, process_model_response() 가 이를 ProcessedResponse 로 바꾸고, 이후 Tool 실행과 Handoff 처리가 Output Item을 추가한 뒤 SingleStepResult.next_step 을 결정합니다. "Tool execution and handoffs add output items and choose a SingleStepResult.next_step ." 출처: https://github.com/openai/openai-agents-python/blob/main/.agents/references/run-item-lifecycle.md 여기서 execute_tools_and_side_effects() 가 다음 단계입니다. process_model_response() 가 무엇을 실행해야 하는지 발견하고 구조화하는 단계 라면, 이 함수는 실제 Side Effect를 발생시키고 그 결과를 바탕으로 Control Flow를 결정합니다. 이 구분은 의도적으로 설계된 것입니다. OpenAI Agents SDK의 Tool Execution 문서는 process_model_response() 가 실행할 수 있는 작업을 찾아내는 단계와, 실제로 지금 실행해도 되는 작업을 결정하는 단계를 분리한다고 설명합니다. Approval이 필요한 호출인지, 이미 중단됐다가 재개된 실행인지, 동일한 Tool Call을 다시 실행하면 안 되는 상황인지 등을 Tool 실행 전에 별도로 판단하기 위해서입니다. “process_model_response() discovers executable work, but tool_planning.py decides which work may run now. ” 출처: https://github.com/openai/openai-agents-python/blob/main/.agents/references/tool-execution-lifecycle.md 같은 문서에서는 이 단계를 더 명확하게 다음처럼 설명합니다. “Keep discovery, approval partitioning, and invocation as separate phases.” 또 재개된 실행에서 이미 완료한 Tool Call을 다시 실행하지 않아야 한다는 점도 명시합니다. “A resumed interruption must execute unresolved or newly approved work without rediscovering or rerunning completed calls.” 예를 들어 Human Approval이 필요한 Tool이라면 Model이 Tool Call을 생성했다고 해서 바로 실행하면 안 됩니다. 이 구조 덕분에 Model Output의 해석과 실제 실행을 분리할 수 있습니다. 특히 이후에 등장하는 Approval, Tool deduplication, interruption/resume, Handoff 같은 기능은 이 분리가 있어야 안정적으로 처리할 수 있습니다. 여기까지 처리된 뒤에야 execute_tools_and_side_effects() 가 실제 Tool을 실행하고 Handoff나 Approval을 처리한 다음, 결과를 SingleStepResult.next_st
그녀의 목소리는 돈으로 가득차 있죠. 그 문장은 《위대한 개츠비》 전체를 관통하는 가장 아름다우면서도 잔인한 통찰을 담고 있는 문장 중 하나입니다. 단순히 "그녀가 부유하다"는 사실을 말하는 것이 아니라, 계급, 욕망, 그리고 개츠비가 도달하고자 하는 '그 무엇'의 본질 을 청각적인 이미지로 형상화한 것이기 때문입니다. 이 문장이 갖는 다층적인 의미를 세 가지 관점에서 분석해 보겠습니다. 1. '올드 머니(Old Money)'의 압도적인 아우라 이 소설에서 가장 중요한 갈등 중 하나는 '대물림된 부(Old Money)'와 '자수성가한 부(New Money)' 사이의 간극 입니다. 데이지의 부(Old Money): 데이지의 부는 노력해서 얻은 것이 아니라, 태생적으로 주어진 것입니다. 그녀의 목소리에 '돈이 가득 차 있다'는 것은, 그녀가 돈을 의식하거나 그것을 과시하려 애쓰지 않아도, 그녀의 존재 자체가 부(富) 그 자체와 하나로 녹아들어 있음 을 의미합니다. 그것은 여유로움, 세련됨, 그리고 타인과 구별되는 근본적인 귀족적 분위기로 나타납니다. 개츠비의 부(New Money): 반면 개츠비의 부는 필사적인 노력과 부정한 방법으로 쌓아 올린, '노력의 결과물'입니다. 그는 돈을 통해 데이지에게 도달하려 하지만, 데이지가 가진 그 '태생적인 여유(돈이 섞인 목소리)'는 돈을 아무리 많이 가져도 살 수 없는 영역입니다. 따라서 이 문장은 데이지가 가진 계급적 우월성과, 개츠비가 결코 넘볼 수 없는 사회적 벽 을 상징합니다. 2. 대상의 '객체화'와 '이상화' 개츠비의 시선에서 이 말은 매우 비극적입니다. 개츠비에게 데이지는 단순한 '사랑하는 여자'를 넘어, 자신의 꿈(Dream)이 형상화된 존재 입니다. 그는 데이지를 사랑하는 것이 아니라, 자신이 도달하고자 하는 '완벽한 삶의 상태'를 데이지라는 인물에 투사 하고 있습니다. 그녀의 목소리에서 돈의 소리를 듣는다는 것은, 그가 그녀를 한 인간으로 보는 것이 아니라 자신의 결핍을 채워줄 '가장 가치 있는 보석'이나 '성배(Holy Grail)'로 보고 있음 을 의미합니다. 즉, 그녀는 인격체가 아니라 그가 쫓는 '꿈의 음파(Sound of the Dream)'인 셈입니다. 3. 물질과 환상의 경계가 무너진 상태 이 문장은 물질(돈)이 어떻게 인간의 본질(목소리) 속으로 침투하여 환상(사랑)으로 변모하는가 를 보여줍니다. 보통 '돈이 섞였다'는 표현은 부정적(속물적)으로 쓰이기 쉽지만, 개츠비에게는 그것이 황홀한 매력 으로 다가옵니다. 그는 그녀의 목소리에서 들리는 그 부유함과 여유로움에 매료됩니다. 이는 현대 사회에서 우리가 어떤 화려한 겉모습이나 사회적 지위를 보고 "저 사람에게는 품격(혹은 아우라)이 느껴진다"고 말하는 것과 비슷하지만, 그 본질이 철저히 '자본'에 뿌리를 두고 있다는 점을 냉소적으로 보여주는 것이기도 합니다. 결론적으로, "그녀의 목소리는 돈으로 가득 차 있다"는 말은, 개츠비가 쫓는 '꿈'이 결국은 극도로 물질적인 것에 기반하고 있으며, 동시에 그 꿈이 결코 손에 잡을 수 없는 '계급적 본질'을 품고 있음 을 폭로하는 문장입니다. 이 문장을 읽으실 때, 단순히 "부자라서 부럽다"는 느낌을 넘어, "인간의 열망이 어디까지 물질과 결합할 수 있는가" 혹은 "우리가 사랑한다고 믿는 것이 사실은 그 사람이 가진 조건의 잔향(Resonance)은 아닌가" 하는 서늘한 질문을 던지셨을지도 모르겠습니다.
"소셜 로그인은 당연히 공급자(Google, Kakao, Naver)가 이메일을 인증해 줬겠지?" 이 안일한 가정 하나가 서비스 전체의 관리자 계정 탈취(Account Takeover) 취약점으로 이어질 수 있습니다. 실제 서비스 운영 중 코드 리뷰를 진행하다가 발견한 치명적인 OAuth 계정 선점 및 탈취 취약점 과, 이를 프로덕션 무중단 배포로 안전하게 방어해 낸 과정을 공유합니다. 🚨 1. 문제의 발단: 어떻게 계정이 털릴 수 있었을까? 기존 시스템의 소셜 로그인 로직은 일반적인 웹 서비스들과 비슷하게 구성되어 있었습니다: 사용자가 카카오/구글/네이버로 로그인 요청. IdP(공급자)가 인증 후 사용자 정보(User Info)를 넘겨줌. 백엔드의 OAuth2SuccessHandler 가 넘어온 email 문자열 을 읽음. DB에서 해당 이메일을 가진 회원이 있는지 조회: 있으면? ➡️ 기존 회원으로 판단하고 즉시 로그인(JWT 발급) 없으면? ➡️ 신규 소셜 회원가입 처리 [공격 시나리오] 1. 피해자(또는 관리자)의 이메일: admin@company.com (일반 이메일/비밀번호 가입 계정) 2. 공격자가 카카오나 타 플랫폼에서 admin@company.com을 임의로 입력하여 소셜 계정 생성 (이메일 인증 미완료 상태) 3. 공격자가 우리 서비스에서 "카카오 로그인" 클릭 4. 우리 서버는 넘어온 email이 "admin@company.com"이라는 이유만으로 기존 관리자 계정의 세션을 그대로 발급해버림! ➡️ 💥 비밀번호 없이 관리자 계정 로그인 성공 (Account Takeover) 더 심각했던 점은, 아직 가입하지 않은 피해자의 이메일로 공격자가 먼저 소셜 가입을 해두면, 나중에 피해자가 정식 가입을 하려고 해도 계정이 선점당하는 문제도 존재했습니다. 🔍 2. 근거와 원인 분석: 공급자(IdP)의 이메일을 무조건 믿으면 안 되는 이유 소셜 공급자마다 '이메일 인증 여부' 를 다루는 정책이 완전히 다릅니다: 공급자 이메일 인증 여부 필드 특징 Google email_verified (boolean) 대부분 true이지만 예외적인 경우(도메인 미인증 계정 등) false 가능 Kakao kakao_account.is_email_verified 이메일 인증을 안 거친 계정도 존재할 수 있음! (is_email_valid도 별도 존재) Naver ❌ 인증 여부 필드를 아예 안 줌 네이버는 응답 객체에 이메일 인증 여부 플래그 자체가 없음 문제의 코드 (Before) // 기존 OAuth2UserInfo - 공급자가 넘겨준 email 문자열만 그대로 추출 public class OAuth2UserInfo { private String email; public static OAuth2UserInfo of(String registrationId, Map<String, Object> attributes) { // ... 공급자별 파싱 로직 // ❌ 치명적 실수: 공급자가 '이 이메일의 소유권을 인증했는지' 확인하는 플래그를 전혀 읽지 않음! return new OAuth2UserInfo(email); } } 결국 "공급자가 넘겨준 이메일은 이미 검증된 이메일일 것이다"라는 암묵적 가정 이 보안 구멍을 만든 근본 원인이었습니다. 🛠️ 3. 해결 방안: 어떻게 방어했을까? 이 취약점을 해결하기 위해 3중 방어 체계를 구축했습니다. 1 공급자별 이메일 인증 여부(emailVerified) 필수 해석 public class OAuth2UserInfo { private String email; private boolean emailVerified; // 인증 여부 필드 추가 public static OAuth2UserInfo ofKakao(Map<String, Object> attributes) { Map<String, Object> account = (Map<String, Object>) attributes.get("kakao_account"); String email = (String) account.get("email"); // 카카오: 인증 여부(is_email_verified)와 유효성(is_email_valid) 모두 확인 boolean verified = Boolean.TRUE.equals(account.get("is_email_verified")) && Boolean.TRUE.equals(account.get("is_email_valid")); return new OAuth2UserInfo(email, verified); } public static OAuth2UserInfo ofNaver(Map<String, Object> attributes) { // 네이버: 인증 여부 필드를 제공하지 않으므로 기본적으로 '미확인(false)' 처리 return new OAuth2UserInfo(email, false); } } 2 OAuth2SuccessHandler 교차 로그인 보안 규칙 강화 구글 / 카카오 (미인증 이메일) : 기존 계정이 있더라도 로그인을 단호히 거절 하고 에러 페이지로 리다이렉트 ( /login?error=unverified_email ). 사용자에게 해당 소셜 플랫폼에서 이메일 인증을 완료하고 오도록 안내. 네이버 (인증 여부 미제공) : 기존 계정의 가입 방식(Provider)이 이미 NAVER일 때만 로그인 허용 . 기존 계정이 일반 이메일 가입자라면 자동으로 엮지 않고 명시적인 계정 연동을 요구 ( /login?error=link_required ). 3 후속 아키텍처 개선: 이메일이 아닌 '소셜 고유 식별자(ID)'로 1순위 식별 이메일은 변경될 수 있고 공급자 정책에 따라 위험이 따르므로, 장기적으로는 social_accounts 매핑 테이블을 두고 소셜 고유 ID로 식별하도록 개선했습니다: Google: sub Kakao: id Naver: response.id 최초 1회만 엄격한 인증 검증 후 연동을 맺어두면, 이후에는 소셜 쪽 이메일 상태와 무관하게 안전하고 빠른 로그인이 보장됩니다. 🧪 4. 검증 결과 단위 테스트 작성 : OAuth2SuccessHandlerTest (8개 시나리오 - 미인증 이메일 거절, 교차 연동 차단 등 100% 검증) 전체 회귀 테스트 336개 ALL PASS 운영 배포 : 다운타임 없이 안전하게 배포 완료, 기존 정상 인증 회원들의 로그인에는 영향 없이 비정상 접근만 완벽 차단. 💡 이번 트러블슈팅을 통해 얻은 교훈 외부 플랫폼(IdP)의 데이터를 맹신하지 말 것 : "구글/카카오니까 당연히 검증되었겠지"라는 생각이 가장 위험합니다. 이메일을 소셜 계정의 유일한 식별자로 쓰지 말 것 : 이메일 대신 공급자의 고유 회원 식별 번호( sub , id )를 Key로 사용하는 것이 정석입니다. 보안 검증은 반드시 서버에서 강제할 것 : 프론트엔드 화면에서 아무리 가드를 쳐도, 엔드포인트 직통 호출을 막으려면 백엔드 비즈니스 로직에서 인증 여부를 직접 검증해야 합니다.
4장 - 미국인들은 충성스러운 농노가 되고싶어도 소작농은 되지않겠다고 억지를 부리곤 했다. (출판사 - 더 스토리) "Americans, while willing, even eager, to be serfs, have always been obstinate about being peasantry." 다르게 번역하자면, "미국인들은 기꺼이, 아니 간절히 농노가 되고자 하면서도, 소작농이 되는 것만은 언제나 완강히 거부해 왔다." 이 문장은 미국인이 계급 자체는 받아들이면서도 "낮은 계급"이라는 딱지만은 거부하는 모순 을 꼬집는 말입니다. 농노 는 주인에게 충성하는 종속적 존재이고, 소작농 은 신분이 고정된 하층민입니다. 미국인은 부자나 권력자를 섬기고 그 밑에서 일하는 것은 기꺼이 하지만, 자신을 "아래 계급"으로 규정하는 것은 완강히 거부한다는 뜻입니다. 이는 아메리칸 드림 의 이면입니다. 누구나 올라갈 수 있다는 믿음 때문에 사람들은 지금 낮은 자리에 있어도 자신을 하층이 아니라 "성공하기 전 단계"로 여깁니다. 그래서 부와 성공의 상징 앞에서는 굽신거리면서도 계급 고착은 인정하지 않습니다. 소설 전체로 보면 개츠비를 둘러싼 사람들 이 이 모습을 보여줍니다. 그의 파티에 몰려드는 이들은 부에 매혹되어 그를 추종하지만, 자신이 사회적 위계 속에 있다는 사실은 의식하지 않습니다. 개츠비 자신도 가난한 출신을 지우고 새 신분을 만들어 낸 인물입니다. 작가 피츠제럴드는 평등을 내세우는 미국 사회가 실제로는 돈에 따른 위계가 뚜렷하며, 사람들이 그 사실을 인정하지 않을 뿐이라고 비꼬는 셈입니다. 원문은 다음과 같습니다. "Americans, while willing, even eager, to be serfs, have always been obstinate about being peasantry." 번역문과 비교하면 몇 가지 차이가 있습니다. willing, even eager : 번역문의 "충성스러운"이라는 표현은 원문에 없습니다. 원문은 농노가 되는 것을 마지못해 받아들이는 정도가 아니라 기꺼이, 심지어 간절히 원한다 는 뜻입니다. obstinate : 번역문의 "억지를 부리곤 했다"에 해당하며, 완고하게 고집을 부린다 는 뜻입니다. peasantry : 개인 소작농이 아니라 소작농이라는 계급 전체 를 가리키는 집합명사입니다. 그래서 "하층 계급이라는 신분 자체를 거부한다"는 해석이 더 잘 맞습니다. 원문 문장 하나만 보면 일반론처럼 읽히지만, 피츠제럴드는 이 문장을 냉소적인 논평으로 던져 넣은 것이므로, 앞뒤 문단과 함께 읽으면 뉘앙스가 더 분명해집니다.
작성일: 2026.09.30 (수) | 과목: Mini Project 1 지원UP 신청 전에 AI가 한 번 더 확인하는, 1인 소상공인을 위한 정부지원사업 AI 신청 도우미 1. 프로젝트 기본 정보 : Overview 항목 내용 프로젝트명 지원UP 한 줄 정의 내 사업정보로 지원사업 자격요건을 AI가 미리 검수해 주는 신청 전 도우미 진행 기간 2026.09.21 ~ 2026.09.30 (총 1주) 단계 기획 → 설계(화면 흐름·ERD·API) → 구현 1(CRUD) → 구현 2(AI·예외 처리·UI 보완) → 검증·발표 팀 구성 총 6명 — Frontend 2명, Backend·DB 3명, AI 2명 (1명 FE·AI 겸임) 역할 FE -> A 파트 + 발표자료 디자인 GitHub LGCNS-MiniProject-Group6 역할 & 기여도 : My Role & Contribution 프론트엔드 A 파트 계정 영역 담당 : 가입부터 공고 찾기까지의 화면 : 로그인, 회원가입, 휴대폰 인증, 인증 상태·토큰, 마이페이지, 프로필·사업정보 AUTH-01~07, 로고제작, footer제작, 관심공고 하트 토글 UI 공통 컴포넌트 제작 : Header, ProgramCard, SearchBar, Filter, SignupStepCard, ProgressBar API 연동 : /auth/signup , /auth/login , /users/me/business-info , /programs 발표 PPT 디자인 : Figma(Product Review 템플릿 기반)로 제작 사용 툴 & 기술 스택 : Tools & Tech 구분 사용 기술 Design Figma Frontend React, JavaScript, axios Backend·DB Spring Boot, Java, MariaDB, Redis, JWT AI Spring AI, OpenAI API Data 기업마당 Open API (공고 수집) Collab GitHub Projects, Notion, Discord, Figma 2. 문제 정의 및 배경 : Problem Statement & Background 지원사업은 많지만, 내가 신청 가능한지 판단하기는 어렵다. 지원UP은 '찾기'가 아니라 '신청 직전의 판단'에 집중했다. 시장/사용자 배경 : Context 기존 서비스는 지원사업을 검색·추천하는 데 집중한다. 하지만 1인 소상공인은 복잡한 신청 조건과 자격 요건 때문에 자신이 신청 가능한 사업인지 스스로 판단하기 어렵고, 이 단계에서 시행착오가 생긴다. 핵심 문제점 : Pain Point 3가지 단계 문제 사용자가 겪는 불편 찾기 흩어진 공고 여러 사이트를 돌며 같은 공고를 중복 확인해야 함 판단하기 복잡한 조건 공고마다 자격요건이 달라 충족 여부를 직접 해석해야 하고, 적합/부적합 결과만으로는 믿기 어려움 준비하기 늦게 알아챈 서류 공고마다 다른 준비서류·주의사항을 마감 직전에야 발견함 타겟 사용자 : Target Audience 항목 내용 페르소나 박운영, 38세, 온라인 쇼핑몰 대표(3년차) 상황 매출은 있지만 지원사업은 첫 신청 한마디 "공고 찾느라 본업할 시간이 없어요." 핵심 니즈 시간 절약 + 의사결정 신뢰 — "빠르고 정확하게 결정할 수 있다" P/G Pain → Gain 여러 사이트 중복 확인 → 한 곳에서 매칭·검수·저장 공고마다 다른 서류·조건 → 신청 전 확인사항으로 정리 적합/부적합만으론 못 믿음 → 내 사업정보 기반 판단 근거 탐색할 시간 없음 → 맞춤 공고 우선 노출 프로젝트 목표 사용자가 사업정보를 입력하고 지원사업을 고르면 → AI가 신청 가능 여부·판단 근거·필수 확인사항 을 한 화면에 보여주는 전체 흐름을 시연 가능하게 만드는 것. → 실제 접수, 사업계획서 자동 작성, 100% 자동 판정은 범위에서 제외. 3. UX 설계 및 해결 과정 : UX Process & Solution 사업정보 입력 → 탐색 → AI 검수 → 근거·서류 → 챗봇·저장 5단계로 끝나는 신청 전 검수 흐름을 설계 정보 구조 1depth 주요 화면 / 기능 계정 로그인, 회원가입(기본정보 → 세부 사업정보, 건너뛰기 가능), 휴대폰 인증 홈(메인) 맞춤 추천 카드(AI 검수결과 조회·상세보기), 키워드 검색, 필터, 카테고리 지원사업 공고 목록(지역·접수상태·마감임박순 필터), 공고 상세 AI 검수 조건별 충족 / 추가 확인 필요 / 미충족 판정 + 근거·준비서류·마감일 AI 챗봇 공고문·검수 결과 기반 질의응답 마이페이지 검수 기록, 관심공고·지원공고 모아보기, 프로필·사업정보 수정, 탈퇴 유저 플로우 : User Flow [로그인·회원가입] → [사업정보 등록] → [홈·맞춤 추천] → [공고 목록·상세] │ (건너뛰기 가능) ▲ │ └──────────────────────────────────┘ ▼ [마이페이지 저장] ← [AI 챗봇 질의응답] ← [근거·준비서류] ← [AI 신청 전 검수] 가입 시 사업정보를 건너뛸 수 있지만, 입력해 두면 추천·검수에 자동 반영되어 공고 상세에서 바로 AI 검수로 넘어간다. 핵심 UX 솔루션 : Key UX Features 1. AI 신청 전 검수 — '판단하기' 문제 해결 사업정보와 공고 자격요건을 AI가 조건별로 비교해 MATCHED · NEED_CHECK · UNMATCHED 3단계로 표시 판단 근거가 된 공고문 페이지·문장을 함께 보여줘, 결과만 던지는 대신 사용자가 근거를 직접 확인할 수 있게 함 2. 사업정보 1회 입력 + 맞춤 공고 목록 — '찾기' 문제 해결 지역·업종·개업일·사업자 유형·상시근로자 수·연 매출을 한 번 저장하면 추천·검수에 자동 반영 기업마당 공고를 한 곳에 모아 카드형 목록으로 보여주고, 지원가능성·마감일(D-day) 뱃지로 훑어보기 쉽게 구성 3. 근거·준비서류 정리 + AI 챗봇 — '준비하기' 문제 해결 조건별 준비서류·주의사항·신청 마감일을 한 화면에 정리 판정 사유 등 궁금한 점은 공고 기반 챗봇으로 바로 질문하고, 결과는 마이페이지에 저장 초기 와이어프레임 → 최종 UI 변천사 : Iteration _※ Develop 필요 _ 구분 Before After 계기 회원가입 (작성 필요) 기본정보 → 세부 사업정보 단계형 (ProgressBar, 건너뛰기 가능) (작성 필요) 공고 카드 (작성 필요) 카드형 목록 + 지원가능성/마감일 뱃지 (작성 필요) 4. UI 디자인 & 디자인 시스템 : UI & Design System 신뢰감을 주는 네이비·퍼플 계열에 따뜻한 크림 배경을 더해, '행정 서비스지만 어렵지 않은' 인상을 목표로 했다. 비주얼 컨셉 : Visual Concept 역할 색상 사용처 Primary 네이비 #3B3F88 타이틀, 로고, 주요 버튼 Secondary 퍼플 #573B88 로고 그라데이션, 강조 Tint 라벤더 #9AA0D5 배경 그라데이션, 보조 영역 Background 크림 #F4F0E9 카드·섹션 배경 상태 컬러: 검수 결과 3단계(충족 / 추가 확인 필요 / 미충족)를 색으로 구분 타이포그래피: (작성 필요) AI 신청 적합성 검수 결과 Page 지원사업 찾기 page 챗봇 page 디자인 시스템 : Design System Asset 컴포넌트 쓰인 화면 주요 상태/Variant Header 전체 로그인 전 / 후 (마이페이지·로그아웃 버튼) ProgramCard 홈, 공고 목록 지원가능성 뱃지, D-day 뱃지 SearchBar 홈, 공고 목록 키워드 입력 Filter 공고 목록 지역, 접수상태, 마감임박순, 카테고리 SignupStepCard 회원가입 기본정보 / 세부 사업정보 단계 ProgressBar 회원가입 단계 진행률 예외 처리 화면 : Edge Cases _※ Develop 필요 _ 상황 설계 내용 사업정보 미입력 회원가입 시 세부 사업정보 '건너뛰기' 허용 검색 결과 없음 (Empty State) (작성 필요) 데이터 로딩 중 (Loading/Skeleton) (작성 필요) 에러 팝업 (작성 필요) 5. 개발 협업 & 트러블슈팅 : Developer Collaboration 화면에 보이는 이름과 API 데이터 이름을 맞추고, CORS 문제를 백엔드와 함께 풀며 프론트 A 파트를 3일 안에 연결했다. 컴포넌트·데이터 매핑 : Figma to Code Mapping ProgramCard에 들어갈 필드를 백엔드 응답 필드명과 1:1로 맞춰 확정. 화면 요소 초기 이름 확정 필드명 처리 공고명 title title 유지 카테고리 category category 유지 지원 대상 target target 유지 주관 기관 agency organization 이름 변경 요약 description summary 이름 변경 D-day / 접수기간 dday , period applicationStartAt , applicationEndAt 프론트에서 계산해 표시 기술적 제약과의 타협 : Troubleshooting 케이스 문제 원인 해결 CORS (FE·BE) 회원가입·로그인 API 호출이 브라우저에서 차단됨 프론트와 백엔드 서버 주소가 달라 교차 출처 요청이 막힘 백엔드에 허용 출처 설정 추가, 프론트 axios baseURL을 백엔드 주소로 통일 HWP 추출 (BE·AI) 비정형 HWP 공고문에서 자격요건 추출 실패 HWP 포맷용 파싱 모듈 부재 HWP 파싱 오픈소스 도입, 텍스트 추출 로직 보완 AI 환각 (AI·FE) 공고 근거를 벗어난 모호·추정 답변 발생 초기 LLM의 문맥 제어·응답 범위 제한 부족 공고 기반 프롬프트 구조화, 응답 범위·생성 규칙 제한 직접 다룬 건 CORS 케이스, 프론트 A 파트(화면 4개, 컴포넌트 6개, API 4개)를 3일 안에 끝내야 했다. 팀원의 프로토타입 브랜치( feature/first-prototype )를 기준으로 Header·ProgramCard·Filter·목록 화면을 합쳐 일정을 맞췄다. 핸드오프 : Hand-off 문서화 GitHub FrontEnd 레포에서 기능별 브랜치( feature/home , program , review , chat , mypage , auth 등)로 작업을 나눔 프론트 A(가입 공고 찾기) / B(공고 상세 AI 검수·챗봇·마이페이지)로 화면 경계를 정해 2명이 충돌 없이 작업 6. 성과 및 회고 : Outcome & Retrospective 사업정보 입력부터 AI 검수·챗봇·저장까지 이어지는 시연 가능한 흐름을 완성하고, 일주일 뒤인 9.30 최종 발표 프로젝트 결과 : Result 구현 범위 : 회원가입·로그인(JWT), 사업정보 등록, 지원사업 목록·검색·필터, 맞춤 추천, AI 신청 전 검수(3단계 판정 + 근거), 공고 기반 AI 챗봇, 마이페이지 저장 API 6종 : 회원가입, 사업정보 등록, 지원사업 목록 조회, AI 신청 전 검수, AI 챗봇, 맞춤공고 조회 기대 효과 : 탐색 시간 단축, 리스크 사전 확인, 준비 부담 감소 — 행정 경험이 적은 1인 소상공인도 혼자 지원사업에 도전할 수 있게 함 수익 모델 구상 : Freemium 구독, 제휴 세무·행정, B2G 지자체·기관, 광고 배너 수익 모델 1 수익 모델 2 배운 점 : Key Takeaways 화면 요소 이름과 API 필드명을 먼저 맞춰 두면 연동 단계의 수정이 줄어든다 ( agency → organization , D-day는 날짜 필드로 계산) CORS처럼 프론트 혼자 해결할 수 없는 문제는 원인을 정리해 백엔드와 나눠 푸는 것이 빠르다 프론트엔드 입문 단계에서 3일 안에 화면 4개를 만들기 위해, 범위를 화면 단위로 나누고 기존 프로토타입을 재사용했다 아쉬운 점 및 추후 개선 계획 : Future Plan 개선 항목 현재 향후 사업정보 항목 확대 입력 항목이 제한적이라 정밀 매칭에 한계 사업 규모·업력·매출·관심 분야 등으로 확장해 매칭 정확도 향상 맞춤 공고 알림 사용자가 직접 목록을 확인해야 해 신규 공고를 놓칠 수 있음 신규 공고를 사업정보와 자동 비교해 적합도 높은 공고 알림 프롬프트 고도화 일부 질문에서 답변의 일관성·구체성 부족 공고 유형·질문 의도별 프롬프트 세분화, 출력 형식 구조화
AUROC (Area Under the Receiver Operating Characteristic curve) 는 분류 모델의 성능을 측정하는 지표 중 하나로, 모델이 양성(Positive) 클래스와 음성(Negative) 클래스를 얼마나 잘 구분하는지를 나타냅니다. 1. AUROC 계산 방법 AUROC를 이해하려면 먼저 ROC 곡선 을 이해해야 합니다. 1 ROC 곡선 (Receiver Operating Characteristic Curve) ROC 곡선은 임계값(Threshold)을 변화시킬 때, TPR(True Positive Rate) 과 FPR(False Positive Rate) 이 어떻게 변하는지를 나타낸 그래프입니다. TPR (재현율, Recall/Sensitivity): 실제 양성 중 양성으로 맞춘 비율 $$\text{TPR} = \frac{\text{TP}}{\text{TP} + \text{FN}}$$ FPR (1 - 특이도): 실제 음성 중 양성으로 틀린 비율 $$\text{FPR} = \frac{\text{FP}}{\text{FP} + \text{TN}}$$ 모델이 예측한 점수(Score)를 기준으로 임계값을 $1.0$에서 $0.0$까지 낮추어 가며, 매 단계마다 TPR과 FPR을 계산하여 그래프를 그립니다. 2 AUROC (Area Under the Curve) AUROC는 이 ROC 곡선 아래의 면적 을 의미합니다. AUROC = 1.0: 완벽한 분류기 (모든 양성을 맞히고, 모든 음성을 정확히 구분) AUROC = 0.5: 무작위 추측 (Random Guessing, 대각선 형태) AUROC < 0.5: 모델이 거꾸로 예측하고 있음 (클래스를 반대로 분류) 2. 이상 탐지(Anomaly Detection)에서 AUROC를 쓰는 이유 이상 탐지 문제에서는 일반적인 분류 문제와 다른 두 가지 큰 특징이 있으며, 이 때문에 AUROC가 매우 중요한 지표가 됩니다. 1 데이터 불균형(Imbalance) 문제 해결 이상 탐지 데이터는 대부분 정상(Normal) 이고 이상(Anomaly) 데이터는 극히 적습니다. Accuracy(정확도)의 한계: 만약 데이터의 99%가 정상이라면, 모델이 무조건 "정상"이라고만 답해도 정확도는 99%가 나옵니다. 이는 모델이 이상치를 전혀 못 찾아내도 성능이 좋아 보이는 착시 현상을 일으킵니다. AUROC의 강점: AUROC는 클래스의 비율(Imbalance)에 영향을 받지 않고, 모델이 '정상'과 '이상' 사이의 경계(Separation) 를 얼마나 잘 구분하는지를 측정합니다. 2 임계값(Threshold)에 대한 의존성 없음 (Threshold-Independent) 이상 탐지 시스템을 운영할 때, "어느 정도 점수 이상을 이상치로 간주할 것인가?"라는 임계값을 정하는 것은 매우 어렵습니다. 어떤 시스템은 오탐(False Positive)을 줄이는 것이 중요하고, 어떤 시스템은 미검(False Negative)을 줄이는 것이 중요합니다. AUROC는 특정 임계값을 지정하지 않고, 모든 가능한 임계값에서의 성능을 종합적으로 평가 합니다. 즉, 모델 자체가 가진 "구분 능력" 그 자체를 평가하므로, 나중에 최적의 임계값을 선택할 수 있는 잠재력을 측정하는 데 적합합니다. 3 순위 지정 능력(Ranking Ability) 평가 이상 탐지는 사실상 "이 데이터가 다른 데이터에 비해 얼마나 더 이상한가?" 라는 순위를 매기는 작업입니다. AUROC는 수학적으로 "무작위로 뽑은 이상치 데이터의 점수가 무작위로 뽑은 정상 데이터의 점수보다 높을 확률" 을 의미합니다. 따라서 모델이 이상치를 상위권에, 정상을 하위권에 잘 배치(Ranking)하는지를 평가하는 데 가장 직관적인 지표입니다. 요약 지표 특징 이상 탐지에서의 적합성 Accuracy 전체 중 맞춘 비율 낮음 (데이터 불균형 시 왜곡 심함) F1-Score 정밀도와 재현율의 조화 평균 높음 (특정 임계값에서의 성능 측정) AUROC 모든 임계값에서의 구분 능력 매우 높음 (불균형 데이터 및 모델의 잠재 성능 평가에 최적) 단순한 예제를 넘어, 실제 데이터 분석 환경과 유사하게 다음과 같은 요소를 포함하여 코드를 작성해 보겠습니다. 데이터 불균형(Imbalance) 구현 : 이상 탐지 데이터처럼 정상 데이터는 많고 이상 데이터는 아주 적은 상황을 시뮬레이션합니다. 다양한 모델 비교 : 아주 성능이 좋은 모델, 중간 성능의 모델, 그리고 무작위로 예측하는 모델을 비교합니다. ROC 곡선과 PR(Precision-Recall) 곡선 동시 시각화 : 이상 탐지에서는 AUROC만큼이나 AUPRC(Area Under Precision-Recall Curve) 가 중요하므로, 두 곡선을 모두 그려 비교하겠습니다. 복합 예제 코드 (Python) import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score, roc_curve, precision_recall_curve, average_precision_score # 1. 복잡한 데이터셋 생성 (Imbalanced Dataset) # 1000개의 샘플 중 오직 5%만이 이상치(1)인 상황 X, y = make_classification(n_samples=1000, n_features=20, n_clusters_per_class=1, weights=[0.95, 0.05], flip_y=0, n_classes=2, random_state=42) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) # 2. 세 가지 시나리오의 예측 확률(Score) 생성 # Scenario A: 매우 강력한 모델 (실제 학습된 모델) model_best = RandomForestClassifier(n_estimators=100, random_state=42) model_best.fit(X_train, y_train) y_probs_best = model_best.predict_proba(X_test)[:, 1] # Scenario B: 성능이 다소 낮은 모델 (노이즈가 섞인 예측) y_probs_mid = np.clip(y_probs_best + np.random.normal(0, 0.2, size=len(y_probs_best)), 0, 1) # Scenario C: 무작위 예측 (Random Guessing) y_probs_random = np.random.rand(len(y_test)) # 3. 지표 계산 함수 def get_metrics(y_true, y_probs): roc_auc = roc_auc_score(y_true, y_probs) pr_auc = average_precision_score(y_true, y_probs) # AUPRC return roc_auc, pr_auc # 4. 시각화 fig, ax = plt.subplots(1, 2, figsize=(16, 6)) models = [ (y_probs_best, 'High Performance', 'darkgreen'), (y_probs_mid, 'Mediocre Performance', 'orange'), (y_probs_random, 'Random Guessing', 'gray') ] for probs, label, color in models: auc_roc, auc_pr = get_metrics(y_test, probs) # ROC Curve 계산 fpr, tpr, _ = roc_curve(y_test, probs) ax[0].plot(fpr, tpr, color=color, lw=2, label=f'{label} (AUROC = {auc_roc:.2f})') # Precision-Recall Curve 계산 precision, recall, _ = precision_recall_curve(y_test, probs) ax[1].plot(recall, precision, color=color, lw=2, label=f'{label} (AUPRC = {auc_pr:.2f})') # 그래프 스타일 설정 (ROC) ax[0].plot([0, 1], [0, 1], 'k--', lw=1) ax[0].set_xlim([0.0, 1.0]) ax[0].set_ylim([0.0, 1.05]) ax[0].set_xlabel('False Positive Rate') ax[0].set_ylabel('True Positive Rate') ax[0].set_title('ROC Curve (Discriminative Power)') ax[0].legend(loc="lower right") ax[0].grid(alpha=0.3) # 그래프 스타일 설정 (PR) ax[1].set_xlim([0.0, 1.0]) ax[1].set_ylim([0.0, 1.05]) ax[1].set_xlabel('Recall') ax[1].set_ylabel('Precision') ax[1].set_title('Precision-Recall Curve (Imbalanced Data Performance)') ax[1].legend(loc="lower left") ax[1].grid(alpha=0.3) plt.tight_layout() plt.show() 💡 코드 핵심 설명 및 심화 학습 1. 왜 ROC와 PR 곡선을 같이 보나요? (매우 중요) 코드에서 두 개의 그래프를 그린 이유는 이상 탐지(Anomaly Detection)의 특수성 때문입니다. ROC Curve (왼쪽 그래프): 전체적인 모델의 구분 능력(Separation) 을 보여줍니다. 데이터가 불균형해도 (양성이 적어도) 비교적 완만하게 변화하므로, 모델의 전반적인 성능을 파악하기 좋습니다. 하지만 데이터가 극단적으로 불균형할 경우(예: 양성이 0.001%), 모델이 매우 우수함에도 불구하고 ROC 곡선이 실제보다 훨씬 좋아 보이는 낙관적 편향(Optimistic Bias) 이 발생할 수 있습니다. Precision-Recall Curve (오른쪽 그래프): 정밀도(Precision) 와 재현율(Recall) 의 관계를 보여줍니다. 이상 탐지처럼 "정상이 너무 많아서" 발생하는 오탐(False Positive)에 매우 민감하게 반응합니다. 실제 산업 현장(예: 카드 부정 결제 탐지, 불량품 검출)에서는 "정상을 이상으로 잘못 판단하는 비용"이 매우 크기 때문에, AUPRC(Area Under PR Curve) 를 더 엄격한 지표로 사용하는 경우가 많습니다. 2. 데이터 시나리오 분석 High Performance (초록색): ROC와 PR 곡선 모두 왼쪽 상단(1,1)으로 가깝게 붙습니다. 이는 양성을 아주 잘 찾아내면서도 동시에 오답을 거의 내지 않는다는 뜻입니다. Mediocre Performance (주황색): 모델의 점수에 노이즈가 섞이면 ROC는 완만하게 꺾이지만, PR 곡선은 급격하게 떨어지는 것을 볼 수 있습니다. 이는 모델이 이상치를 찾으려 할 때 정상을 이상으로 잘못 판단하는 비율이 급격히 늘어남을 의미합니다. Random Guessing (회색): ROC는 대각선($y=x$)을 따라가고, PR 곡선은 양성 클래스의 비율($\text{Positive Rate}$) 근처에서 수평선을 그립니다. 3. 결론: 어떤 지표를 믿어야 할까? 만약 당신이 "전반적으로 이 모델이 얼마나 잘 작동하는가?" 를 알고 싶다면 AUROC 를 보세요. 만약 당신이 "데이터가 매우 불균형한 상황에서, 오탐(False Positive)을 최소화하며 이상치를 얼마나 잘 잡아내는가?" 를 알고 싶다면 AUPRC 를 보세요. (이상 탐지에서는 이쪽이 훨씬 더 실무적입니다.)
카드 확장 애니메이션 시리즈 — 1편 에서 만든 걸 실제로 여러 화면에 붙여보다가 만난 버그 기록. 증상 목록 화면에서 스크롤을 한참 내린 뒤, 아래쪽에 있는 카드를 클릭해서 확대 애니메이션을 시작했다. 그런데 확대된 결과물이 스크롤로 옮겨놓은 그 위치가 아니라, 스크롤하지 않았을 때 카드가 있었을 "원래 자리"에서 튀어나왔다. 1차 가설 (틀림) 처음엔 position: fixed 가 transform 이 걸린 조상 요소 때문에 뷰포트가 아닌 엉뚱한 기준점을 잡은 게 아닐까 의심했다. 클릭 시점에 캡처한 좌표, URL로 넘겨받은 좌표, 실제로 렌더링된 위치 — 이 세 값을 디버그 로그로 찍어 비교했더니 셋 다 정확히 일치 했다. 좌표 계산 자체는 문제가 없었다는 뜻이다. 2차 가설 (맞음) 좌표 문제가 아니라면 남은 건 애니메이션 자체가 "어디서 시작하는지"를 다르게 알고 있다는 것. 여기서 원인을 찾았다. 카드에 붙인 layoutId 가 항목의 id만으로 만들어져 있었다. 즉 목록 페이지의 진짜 카드와, 상세 페이지로 넘어가면서 만든 애니메이션용 카드가 같은 layoutId 문자열 을 쓰고 있었던 것이다. Next.js 같은 프레임워크의 클라이언트 사이드 네비게이션은 브라우저를 새로고침하지 않고, 같은 JS 런타임 안에서 화면 트리만 바꾼다. 그런데 Motion(구 Framer Motion)의 layoutId 는 그 요소가 화면에서 사라진 뒤에도 한동안 "마지막으로 어디 있었는지"를 전역으로 기억 한다. 문제는 목록의 카드는 스크롤한다고 해서 다시 측정되지 않는다는 점이다(스크롤은 레이아웃 변경이 아니라서 Motion이 재계산할 이유가 없다). 그래서 Motion이 기억하고 있는 그 카드의 위치는 스크롤하기 전, 페이지를 처음 열었을 때의 자연스러운 리스트 순서상 위치 였다. 우리가 넘긴 좌표는 무시되고, Motion은 그 "오래된 기억" 위치에서부터 애니메이션을 시작해버린 것이다. 해결 카드 컴포넌트에 layoutId 를 조합할 때 접두어(prefix)를 옵션으로 받을 수 있게 만들고, 다른 화면에서 애니메이션을 시작할 땐 anchor-${item.id} 처럼 목록 페이지가 쓰는 layoutId 와 절대 겹치지 않는 문자열 을 붙였다. 이렇게 하면 Motion 입장에서는 완전히 별개의 요소로 인식해서, 엉뚱한 과거 위치를 기억해올 일이 없다. // 목록 페이지의 카드 <motion.div layoutId={`card-${item.id}`}>...</motion.div> // 다른 페이지에서 시작하는 애니메이션 (접두어로 완전히 분리) <motion.div layoutId={`anchor-${item.id}`}>...</motion.div> 왜 처음엔 안 보였나 같은 페이지 안에서만 카드를 펼쳤다 접었다 하는 시나리오에서는 이 문제가 전혀 드러나지 않는다. layoutId 가 겹치는 두 요소가 "같은 화면 안에서, 한쪽이 사라지고 다른 쪽이 나타나는" 상황에서는 오히려 그게 의도된 동작(모핑)이기 때문이다. 문제는 페이지 이동을 낀 채로 같은 layoutId 를 다시 쓸 때만 나타난다 — 그래서 단일 페이지 안의 테스트만으로는 잡히지 않고, 실제로 목록 → 상세를 오가며 확인해야 드러나는 종류의 버그였다. 일반화하면 layoutId 를 쓰는 shared layout 애니메이션을 여러 화면에 걸쳐 쓸 계획이라면, "이 id가 다른 화면의 다른 요소와 우연히 겹치지 않는가" 를 미리 점검해볼 가치가 있다. Motion 공식 문서도 비슷한 이유로 LayoutGroup 에 id 를 줘서 네임스페이스를 분리하는 방법을 안내하고 있다 — 같은 페이지 안에 같은 컴포넌트가 여러 번 렌더링될 때 layoutId 가 서로 충돌하지 않게 하기 위한 것인데, 여기서 겪은 "페이지를 넘나드는 충돌"도 결국 같은 종류의 문제(의도치 않은 layoutId 겹침)라는 점에서 원리는 같다. 출처 Motion 공식 문서, <motion /> 컴포넌트: https://motion.dev/docs/react-motion-component Motion 공식 문서, LayoutGroup (id로 layoutId 네임스페이스 분리하는 공식 패턴): https://framer.com/docs/layout-group/
ArrayList란? 패키지에 있는 크기가 자동으로 조절되는 배열 기반 리스트이다. ArrayList는 요소를 추가 시 자동으로 크기를 늘린다. ArrayList<Movie> movies = new ArrayLisr<>(); // Movie 객체를 여러개 담아두는 리스트 값 넣기 add() 값 꺼내기 get() 개수 확인 size() 삭제 remove()
지난 실습에서는 ROS 2 Service의 기본적인 개념과 명령어를 확인했다. 이번에는 앞서 확인한 Service를 실제 TurtleSim에서 호출하면서 거북이 생성, 삭제, 펜 설정 변경, 위치 이동 등의 기능을 직접 실습해 보았다. 1. /clear Service 호출 먼저 TurtleSim에 기존에 그려져 있던 선을 지우기 위해 /clear Service를 호출했다. ros2 service call /clear std_srvs/srv/Empty 실행 결과: requester: making request: std_srvs.srv.Empty_Request() response: std_srvs.srv.Empty_Response() Service 호출이 정상적으로 처리되면서 TurtleSim 화면에 그려져 있던 선이 모두 지워지는 것을 확인했다. 2. /spawn Service로 새로운 거북이 생성 다음으로 /spawn Service를 이용해 새로운 거북이를 생성했다. 이번 실습에서는 turtle3 이라는 이름의 거북이를 (8.0, 8.0) 위치에 생성했다. ros2 service call /spawn turtlesim/srv/Spawn "{x: 8.0, y: 8.0, theta: 0.0, name: 'turtle3'}" 실행 결과: requester: making request: turtlesim.srv.Spawn_Request(x=8.0, y=8.0, theta=0.0, name='turtle3') response: turtlesim.srv.Spawn_Response(name='turtle3') TurtleSim 화면에 turtle3 이 새롭게 생성되는 것을 확인할 수 있었다. 3. /kill Service로 거북이 삭제 생성한 turtle3 을 삭제하기 위해 /kill Service를 호출했다. ros2 service call /kill turtlesim/srv/Kill "{name: 'turtle3'}" Service 호출 후 TurtleSim 화면에서 turtle3 이 사라지는 것을 확인했다. 이를 통해 실행 중인 TurtleSim의 특정 거북이를 Service를 이용해 삭제할 수 있었다. 4. /turtle1/set_pen Service 실습 다음으로 turtle1 이 그리는 선의 색상과 굵기를 변경해 보았다. ros2 service call /turtle1/set_pen turtlesim/srv/SetPen "{r: 0, g: 0, b: 255, width: 8}" 여기서는 RGB 값을 이용해 펜 색상을 설정하고 width 값으로 선의 굵기를 지정했다. 이후 turtle1 을 움직여 변경된 펜 설정이 실제로 적용되는 것을 확인했다. 5. /turtle1/teleport_absolute Service 실습 이번에는 turtle1 의 위치를 직접 변경해 보았다. ros2 service call /turtle1/teleport_absolute turtlesim/srv/TeleportAbsolute "{x: 5.0, y: 5.0, theta: 0.0}" 명령어를 실행하면 turtle1 이 지정한 좌표로 바로 이동한다. 실습을 통해 거북이를 직접 조작하지 않고도 Service를 이용해 특정 위치와 방향으로 순간이동시킬 수 있다는 것을 확인했다. 6. 여러 Service를 이용한 TurtleSim 조작 이번 실습에서는 하나의 Service만 사용하는 것이 아니라 여러 Service를 직접 호출해 보았다. /clear ↓ 화면의 이동 경로 초기화 /spawn ↓ 새로운 거북이 생성 /kill ↓ 거북이 삭제 /turtle1/set_pen ↓ 펜 색상 및 굵기 변경 /turtle1/teleport_absolute ↓ 거북이 위치 및 방향 변경 각각의 Service를 호출할 때마다 TurtleSim의 상태가 실제로 변경되는 것을 확인할 수 있었다. 마무리 이번 실습에서는 ROS 2에서 제공되는 Service를 직접 호출하여 TurtleSim을 제어해 보았다. 특히 새로운 거북이를 생성하거나 삭제하고, 펜의 색상과 굵기를 변경하고, 거북이를 원하는 위치로 이동시키는 등 Service를 이용해 실행 중인 노드의 기능을 직접 요청하는 과정을 확인할 수 있었다. 이번 실습을 통해 앞서 학습한 Service가 실제 ROS 2 환경에서 어떻게 활용되는지 확인할 수 있었다.
React 왜 씀 single page application 을 편하게 만들기 위해 React 라는 자바스크립트 라이브러리 를 설치해 사용하는 것 이다. 비슷한 js라이브러리들이 많긴 하지만 가장 오래되었고 유저가 가장 많기에 리엑트를 쓴다(근-본). 장단점 장점 HTML 재사용의 편리함 컴포넌트 단위로 개발하기 때문에 여러 개발자들이 분업하기 좋음 React Native를 쓰면 리액트 비슷한 문법으로 모바일 앱개발도 가능 프론트엔드와 백엔드 파트를 완전히 분리해서 각각 개발가능 단점 많은 자바스크립트로 인해 웹페이지 크기가 커짐 웹페이지 로드시 HTML 내용물이 천천히 채워지기 때문에 SEO(Search Engine Optimization)에 악영향이 있을 수 있음 (hydration 작업 : html을 먼저보내고 자바스크립트를 연결한다) 간단한 사이트도 코드가 쓸데없이 복잡해짐 Single Page Application 말 그대로 서버에서 전체 페이지를 다시 불러오지 않고 html 한 페이지 만 써서 자바스크립트 로 현재 페이지를 동적으로 갱신한다. html 파일을 1개만 사용 다른 페이지를 보여주고 싶을 때 html 부분만 갈아치워서 보여준다 예) instagram 환경구성 nodejs 설치하면 npm 패키지 매니저가 같이 설치된다. => React 받기 편해짐 설치 후 프로젝트 경로에서 npm create vite@latest 하면 생성된다. npm run dev 개발 서버 실행한다. React 기초 문법 기초 jsx 문법 3개 프로젝트 생성시 주는 App.jsx에 작성해 보았다. function App(){ return ( <div className="App"> <div className="black-nav"> <h4>블로그임</h4> </div> </div> ) } 이런 식으로 div에 때려 넣는다 하지만 function App(){ return ( <div className="App"> <div className="black-nav"> <h4>블로그임</h4> </div> </div> <div> 안된다 </div> ) } 이건 안된다. return()안에는 하나의 div가 있어야 한다. 반환 할 때 div tag 하나만 반환하기 때문이다. import './App.css' 이런 식으로 css도 작성해서 적용이 가능하다. className 평소에는 class를 그냥 class로 넣었는데 className으로 넣어야 한다. => js에서 class를 써가지고 우리가 jsx에서 작성하고 있다는 사실을 망각하지 말자. 데이터 바인딩 문법 {} 거지같은 DOM에서 하나하나 queryseletor나 getElementById로 찾아서 수정하던 것은 잊어라 이건 jsx다. function App(){ let post = 'swap'; return ( <div className="App"> <div className="black-nav"> <div>swap</div> <div>{ post }</div> </div> </div> ) } 긴 말 필요없다. return() 밖에서 선언하고 안에 변수 명을 {}중괄호 안에 꼬라 박으면 값이 들어간다. function App(){ let post = 'swap'; return ( <div className="App"> <div className="{ post }"> <div>swap</div> <div>{ post }</div> </div> </div> ) } className에도 가능이 html에 style 속성 넣기 <div style="color : blue"> 이렇게 넣을 안일한 생각 하지마라 <div style={ {color : 'blue', fontSize : '30px'} }> 글씨 </div> 일단 - 이 기호도 못 쓰고 js에서 변수 선언할 때 마냥 대문자로 써서 붙여야 한다. 그리고 저렇게 오브젝트 형식으로 써줘야 하는데 그 이유는 JSX는 HTML이 아니라 JavaScript이기 때문이다. JSX는 빌드 시점에 결국 React.createElement() 같은 순수 JavaScript 코드로 변환된다. state 왜 씀 중요한 변수는 state에 담는다. state는 변동사항이 생기면 state쓰는 html도 자동으로 재렌더링해준다. function App(){ let post = 'swap' return ( <h4>{ post }</h4> ) } 이렇게 쓰면 코드를 더 짜야지 변경 내용이 보이는데 function App(){ let [글제목, b] = useState('swap'); return ( <h4>{ 글제목 }</h4> ) } state는 바뀌면 바로 적용 해준다. 선언 let [name, age] = ['swap', 18] array 안에 왼쪽 오른쪽 형식을 똑같이 하면 알아서 딱딱 들어가는 destructuring 문법을 사용해 선언한다. let [a,b] = useState('hello') useState()를 쓰면 [데이터1, 데이터2] 가 생기는데 데이터1 : 값 데이터2 : state변경을 도와주는 함수 가 남게 된다. state 변경 <div onclick="자바스크립트"> 일반 html 파일에선 이렇게 하면 된다. <div onClick={함수}> JSX에서는 이렇게 한다. function App(){ let [ 따봉, 따봉변경 ] = useState(0); return ( <h4> swap <span onClick={ ()=>{따봉변경(따봉+1)} } >👍</span> { 따봉 }</h4> ) } 이런 식으로 state를 변경 할 수 있다. 아까 데이터2에는 state 변경을 도와주는 함수가 들어간다 한 것을 기억한다면 이해가 어렵지 않을 것이다. function App(){ let [글제목, 글제목변경] = useState( ['남자코트 추천', '강남 우동맛집', '파이썬 독학'] ); return ( <button onClick={ ()=>{ let copy = [...글제목]; copy[0] = '여자코트 추천'; 글제목변경(copy) } }> 수정버튼 </button> ) } 또 다른 문자열 array를 갈아 끼우는 방식이다. state 자체를 변경하기에는 쓸모가 있을 수 있으니 보통 복사해서 많이 쓴다. 근데 잠깐 [...글제목] 너 뭐냐 let data1 = [1,2,3]; let data2 = data1; data2[0] = 1000; console.log(data2 === data1) true ??? 결론 부터 말하자면 data1과 data는 똑같은 값을 공유한다. 그래서 저런 괴랄한 결과가 나오는 것 이다. ... 이게 뭐냐 spread operator 라는 것 인데 ..[1,2,3] 이렇게 쓰면 그 자리에 1,2,3 이 남는다. 이렇게 하면 다른 복사본을 만들 수 있다. component 긴 HTML을 한 단어로 깔끔하게 치환해서 넣을 수 있는 문법 함수 만들듯, 변수 만들듯 HTML을 깔끔하게 한 단어로 치환해서 원하는 곳에 꽂아넣을 수 있다. 줄이는 법 function을 이용해서 함수를 하나 만들어주고 작명한다. 그 함수 안에 return () 안에 축약을 원하는 HTML을 담는다 그럼 원하는 곳에서 <함수명></함수명> 사용하면 아까 축약한 HTML이 나타난다. function App (){ return ( <div> (생략) <Modal></Modal> </div> ) } function Modal(){ return ( <div className="modal"> <h4>제목</h4> <p>날짜</p> <p>상세내용</p> </div> ) } 이런 식으로 function App (){ return ( <div> (생략) <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> <Modal></Modal> </div> ) } function Modal(){ return ( <div className="modal"> <h4>제목</h4> <p>날짜</p> <p>상세내용</p> </div> ) } 으하하 function Modal(){ return ( <div></div> ) } let Modal = () => { return ( <div></div>) } 이런 방법으로도 만들 수 있다. 주의사항 component 작명할 땐 영어대문자로 보통 작명 return () 안엔 html 태그들이 평행하게 여러개 들어갈 수 없다. function App(){} 내부에서 만들면 안된다. function App(){} 이것도 다시보니 컴포넌트 생성문법, component 안에 component 를 만들진 않는다. <컴포넌트></컴포넌트> 이렇게 써도 되고 <컴포넌트/> 이렇게 써도 된다 뭐 할때 씀 사이트에 반복해서 출현하는 HTML 덩어리들은 Component로 만들면 좋다. 내용이 매우 자주 변경될 것 같은 HTML 부분을 잘라서 Component로 만들면 좋다. 다른 페이지를 만들고 싶다면 그 페이지의 HTML 내용을 하나의 Component로 만드는게 좋다. 또는 다른 팀원과 협업할 때 웹페이지를 Component 단위로 나눠서 작업을 분배하기도 한다. 함수문법도 긴 코드 축약할 때 다른 곳에서 코드 재사용할 때 복잡한 코드를 작은 기능으로 나눌 때 컴포넌트는 그냥 함수 문법이랑 똑같아서 용도도 똑같다. 단점 관리가 힘들어질 수도 있고 안에 state 쓰기가 불편하니 필요할 때만 사용하자. 동적 UI 만들기 3step 예시로 글자 같은거 클릭시 글이 사라졌다 나타났다 하게 만들어 보자. 1. html css로 미리 UI 디자인 function Modal(){ return( <div className='modal'> <h4>제목</h4> <p>날짜</p> <p>상세내용</p> </div> ) } 예를 들어 이딴걸 만들었다 치자 2. UI의 현재 상태를 state로 저장 state 하나 만들고 거기에 현재 UI의 상태정보를 저장 let [modal, setModal] = useState(false); let [modal, setModal] = useState('닫힘'); let [modal, setModal] = useState(0); // 이런 식으로 아무렇게나 지정해도 상관없다. 3. state에 따라서 UI가 어떻게 보일지 조건문 등으로 작성 let [modal, setModal] = useState(false) <div className='list'> <h4 onClick={()=>{setModal(modal == true? false:true)}}>{글제목[2]}<span>👍</span>0</h4> <p>2월 17일 발행</p> </div> { modal == true ? <Modal/>:null } 나의 경우 조건식을 사용하여 true 일때 모달이 보이고 false라면 null로 비워두게 만들었다. map .map 이런 식으로 array 옆에 붙이는 js 기본 함수다 기능 array에 들어있는 자료갯수만큼 그 안에 있는 코드를 반복실행한다. var 어레이 = [2,3,4]; 어레이.map(function(){ console.log(1) }); 콜백함수에 파라미터 아무렇게나 작명하면 그 파라미터는 어레이 안에 있던 모든 자료를 하나씩 출력해준다. var 어레이 = [2,3,4]; 어레이.map(function(a){ console.log(a) }); return 오른쪽에 뭐 적으면 array로 담아주고 map() 쓴 자리에 남겨준다 var 어레이 = [2,3,4]; var newArray = 어레이.map(function(a){ return a * 10 }); console.log(newArray) html복사 이를 이용하면 이런 식으로 html 복사 가능하다 function App (){ return ( <div> { [1,2,3].map(function(){ return ( <div>말하면500만원</div> ) }) } </div> ) } map()의 기능들을 이용하면 다음과 같은 것들도 가능해진다 function App (){ return ( <div> (생략) { 글제목.map(function(a){ return ( <div className="list"> <h4>{ a }</h4> <p>2월 18일 발행</p> </div> ) }) } </div> ) } function App (){ return ( <div> (생략) { 글제목.map(function(a, i){ return ( <div className="list"> <h4>{ 글제목[i] }</h4> <p>2월 18일 발행</p> </div> ) }) } </div> ) } 이런 식으로 function에는 인자들을 넣어서 리스트 값들을 복사해와 하나씩 증가해 따로따로 바꿀 수도 있다. { 글제목.map(function(a, i){ return( <div className='list' key={i}> <h4>{글제목[i]}<span onClick={() => {엄지변경((이전엄지)=>{const 새엄지 = [...이전엄지];새엄지[i] += 1; return 새엄지;})}}>👍</span>{엄지[i]}</h4> <p>2월 17일 발행</p> </div> ) }) }
불변 객체 (Immutable Object) 말 그대로 한 번 만들어지면 절대 바뀌지 않는 객체이다 Primitive vs Reference Type Primityive Type(원시형) : 변수에 실제 값이 들어간다. -> 객체가 아니다 Reference Type(참조형) : 변수에 객체의 주소 (참조값)이 들어간다 레퍼런스가 레퍼런스를 불러오는경우에 값이 마음대로 바뀔 수 있음 final을추가하고 , setter를 없앰으로써 immutable 만일 final 속성을 setter로 건드리려고하면 에러가 발생한다 final 키워드 붙은걸 조작하려고 하기때문임.