안녕하세요 버려뒀던 벨로그를 이렇게 잡게된 이유는.. 기업프로젝트가 끝나고!! 뒤늦게.. 큐포터즈로 큐시즘 34기 디자인파트 합격 후기 를 써보려고 합니다. 우선 34기 합격 후기를 쓰기 전에 33기 불합격에 대해서도 이야기 해보고 싶습니다. 어떤 게 달랐는지 이야기해보면, 추후 35기 지원을 원하시는 분들께 도움이 될 거라고 생각합니다. 저도 면접을 준비하며 큐시즘 후기를 살펴보며 도움을 받았던 터라, 최선을 다해서 적어보겠습니다. 33기에 지원하다 당시 같은 진로인 프로덕트 디자이너를 희망하는 친구들이 여러 it 동아리들에서 개발자와 협업하며 실무 경험을 쌓는게 좋아 보였습니다. 그 중에서 큐시즘에 지원하게 된 계기는 생각보다 단순합니다. 체계적이어보였고, 브랜딩이 단단해보였습니다. 그래서 33기 모집이 나오기만을 기다렸다가, 지원을 합니다! 포트폴리오를 3개 넣어야했기에, 당시 사이드 프로젝트에서 서브 디자이너로 활동한 작업 한 개와 학교에서 한 결과물 두 개를 넣었습니다. 부족한 건 이미 알고 있었고, 다음 단계를 위한 도약이라는 생각으로 최대한 정리해서 지원합니다. 정렬도 안 맞는 부족한 포트폴리오로 냈고, 감사하게도 서류에 합격해 면접에 갔습니다. 함께 보는 면접자가 질문당 답변을 2-3분씩 하셨는데, 저도 그래야하는 줄 알고 비슷하게 답을 했습니다.. ᄒ 지금 생각하면 상당히 횡설수설 했습니다. 잘 기억나지 않고 아카이빙도 안해뒀습니다. (그래도 기억나는 받은 질문은 뒤에 살짝 언급 드리겠습니다) 이때 솔직히 눈물을 훔쳤습니다.. 그렇게 큐시즘과의 인연은 여기까진 줄 알았으나... 이후 멘탈이 나가 4개의 it동아리에 지원서를 냈고 잇타 디자인 파트로 활동을 합니다. 잇타 9기 디자인 파트로 활동하며, 팀 내 1인 디자이너로 팀내 디자인 전반을 담당했고 완성도가 훌륭하다고 할 순 없지만 좋은 프로덕트를 만들었습니다. 좋게 봐주신 잇타인이 공모전 팀원으로 저를 데려가 서브 기획 및 디자인 전반을 담당하였습니다. 34기 재지원하다 그래서 상반기 동안 결론적으로 총 2개의 협업경험이 들어간 작업을 만들었습니다. 그러는 사이에 34기 큐시즘 공고가 올라왔고, 생각이 없었으나 지인의 여러차례 권유에 따라 지원하게 됩니다. 고민한 이유는 미대생의 숙제인 졸업전시 때문입니다.. ᄒᄒ 그러나 무려 두번째의 초과학기, 마지막 기회라 결국 지원하게 됩니다. 34기에도 3개의 작업물을 실을 수 있었고 * 잇타 9기 프로덕트 * (WEARTRACK, AI 옷장관리 서비스) 관광데이터 활용 공모전 출품작 (Pozit, 위치정보 기반 여행 기록 서비스) DSUS 장려상 수상작 (오늘의 읽기, 수험생을 위한 독해력 향상 서비스)을 넣었습니다! 셋다 BTC 앱 프로젝트입니다. 1번 작은 IT 동아리 경험+디자인 역량을 잘 보여줄 수 있다고 판단해 넣었고, 2번 작은 기획 및 디자인을 담당하였으며 가장 완성도 높은 작으로 판단하였습니다. 3번 작은 33기에도 넣었는데, 학술대회 수상적으로 UX적으로 매우 고심하여 기획 및 디자인을 했기에 넣게 되었습니다. 이 중 최소 1개의 프로젝트를 피그마 파일을 공개했어야 했는데, 레이어 정리를 그동안 엉망으로 했던 저는 매우 당황했습니다.. (큐시즘에서 기프하면서 리드 디자이너 언니를 통해 고쳤습니다 ᅲ.ᅲ) 결국 2번 작을 넣었습니다. 지원서에 활동 경력 은 최소한으로 적었습니다. (두번의 학술대회 수상, 잇타 10기 부회장 및 9기 디자인파트, UX랩 학부생 연구원, PM 부트캠프 수료) 다음은 제가 적었던 지원서입니다. 1. 큐시즘 34기에 지원한 이유와 활동을 통해 이루고 싶은 목표를 작성해주세요. 성장을 중심으로 적었습니다. 그러나 큐시즘은 세미나, 스터디 세션 없이 바로 프로젝트가 진행하는 동아리라, 제가 가진 능력을 바탕으로 다른 큐밀리들과 성장하고 싶음을 강조하였습니다. * 2. 함께하는 사람들과 ‘LIMIT’을 넘어선 경험을 소개해 주세요. * 이건.. 밴드부에서 공연을 한 얘기를 적었습니다. 여담이지만 it 프로젝트를 완성하는 것과 밴드부 공연을 완성하는 점은 큰 유사점이 있습니다. 각자 맡은 바를 책임감 있게 하며, 데드라인에 맞춰서 최선의 역량을 내야하는 부분에서 동일하다고 생각해 가장 기억에 남는 공연에 대해 적었습니다. * 3. 아래 두 가지 내용을 모두 포함하여 본인의 디자인 가치관과 역량에 관해 구체적으로 설명해 주세요. * 프로덕트 디자인은 UX가 가장 중요한 디자인이라고 설명을 하였습니다. 그러나 비주얼적인 완성도를 빼놓을 수 없다는 이야기를 풀어냈습니다. 4. 본인의 포트폴리오 중 하나를 선택하여, 프로젝트를 다시 진행한다면 가장 먼저 개선하고 싶은 부분은 무엇인지 UX관점에서 작성해 주세요. 또한 그 이유와, 기획자 및 개발자와의 협업 과정에서 어떤 점을 고려하며 해당 개선을 진행할 것인지 함께 설명해 주세요. 잇타 9기 프로덕트의 아쉬운 점에 대해서 적었습니다. 현재 출시를 준비중이며, 출시 이후에 유저들의 반응을 통해 개선하겠다고 하였습니다. * 5. 다음 학기 일정 및 계획 (자유 형식) * 잇타 10기 부회장, 학부생연구원, 졸업작품(3학점수강)을 적었습니다! 면접 준비는 많이 하지는 않았습니다. 큐시즘 이전 기수의 프로젝트와 큐시즘의 인재상 등을 꼼꼼히 읽고, 제가 쓴 자소서와 포트폴리오를 정독했습니다. 조금 늦어서 정확히 정시에 들어갔습니다. ᅲᅲ * 자기소개 및 별명 * 별명 없다고 대답했습니다..(진짜입니다. 평소 다정하다는 말을 많이 듣는다 정도로 말씀드렸습니다..) 기획자랑 의견 다를 때 디자이너와 기획자라는 명확한 역할이 나눠져있을 때는 고집하기보다는 팀원으로서 의견 제시 정도만 한다고 솔직하게 답변하였습니다. 조직 운영 경험(팀 기준vs내 기준) 잇타 10기 부회장으로서 리크루팅 준비중인데 기획자만이라도 대면으로 보는걸로 리더로서 내 기준 고집해서 채택한 이야기를 했습니다. 사실 저는 리더 경험이 별로 없어서 살짝 흔들렸던 것 같습니다. 포짓은 사용자 ui나 기능을 삭제한 경험이있다고햇는데 그게 뭔지 컨셉 극대화를 위해 여행 큐레이션 홈에서 뺀 이야기를 했습니다. 웨어트랙 옷장 열기 버튼이 왜있는지 바로 있으면 안되냐 팀내 논의중이라 지금 심사중인데 출시이후 개선할 예정이라고 답했습니다. 웨어트랙 사용자의 불편함을 해소한 경험 사용자 입력데이터를 줄였다고 답했습니다. * 웨어트랙 패션소비 리포트가 왜 존재하는지? * 웨트 컨셉 설명해주고 불필요한 소비를 막고 주간회고를 통해 입은 옷을 어쩌고 카테고리별로 (여기 좀 횡설수설함) 등 대답했습니다.. 저번 기수에 질문당 대답을 너무 길게 했던 트라우마가 있어서 최대한 간결하게 하려고 노력했습니다. 또한, 33기에 불합격한 이후로 많이 노력하여 큐시즘에 재지원했다는 사실을 강조하려하였습니다. 사실 저는 면접 질문에서 합/불의 힌트를 얻을 수 있었는데요. 탈락을 했을 때는 큐시즘에서 기대되는 활동이 뭔지, 팀 내 디자이너와 취향이 다를 경우 어떻게 할건지 등 포괄적인 질문에 중점을 두고, 합격을 했을 때는 조금 더 프로젝트에 파고들어서 질문을 하셨습니다. 이후 타 it동아리에서 리크루팅을 하면서 느낀 점은, 부족한 포트폴리오는 물어볼 것도 없다는 점이었습니다. (..) 아마 불합격에서 합격 목걸이를 걸 수 있었던 이유는, 포트폴리오를 갈아엎었기 때문이라는 생각이 듭니다..ᄒᄒ 포트폴리오를 단순 수정하고 디벨롭한게 아니라 프로젝트를 싹 바꿨기에 가능하다는 생각이 듭니다! 큐시즘 수료 이후 또 후기로 찾아뵙겠습니다. 읽어주셔서 감사합니다!
claude -c --model google/gemma-4-26b-a4b-qat Claude Code 세션과 Context: 폴더를 오가며 작업할 때 알아야 할 것들 Claude Code를 쓰다 보면 이런 궁금증이 생깁니다. 프로젝트A에서 작업하다 종료하고 프로젝트B에서 작업한 뒤 다시 프로젝트A로 돌아오면, 이전 대화 맥락(context)은 남아 있을까요? 이 글에서는 세션이 어떻게 관리되는지 정리합니다. 1. 결론부터: 그냥 claude 를 실행하면 새 세션이다 프로젝트A에서 claude 를 실행해 작업하고 종료한 뒤, 프로젝트B를 거쳐 다시 프로젝트A에서 claude 를 실행하면 이전 대화 context는 이어지지 않습니다. 빈 새 세션이 열리고, Claude는 직전에 무엇을 했는지 알지 못합니다. 다만 이전 대화가 사라진 것은 아닙니다. 별도로 저장되어 있어서 필요할 때 불러올 수 있습니다. 2. 대화는 어디에 저장되는가 대화 기록은 홈 디렉터리의 ~/.claude/projects/ 아래에 프로젝트별로 저장됩니다. ~/.claude/projects/ ├── -Users-me-projectA/ │ ├── <세션ID>.jsonl │ └── <세션ID>.jsonl └── -Users-me-projectB/ └── <세션ID>.jsonl 프로젝트 폴더의 절대 경로에서 / 를 - 로 바꾼 이름이 하위 폴더명이 됩니다. 세션 하나가 .jsonl 파일 하나입니다. 대화와 도구 호출 기록이 한 줄씩 JSON 형태로 쌓입니다. C:\Users\사용자명\.claude\projects\ 아래에 같은 방식으로 저장됩니다. 이 구조 덕분에 프로젝트A와 B의 대화는 서로 섞이지 않습니다. B에서 작업했다고 해서 A의 기록이 지워지지도 않습니다. 3. 이전 대화를 이어가는 방법 방법 설명 claude -c ( --continue ) 현재 폴더의 가장 최근 대화를 바로 이어감 claude -r ( --resume ) 현재 폴더의 과거 세션 목록에서 골라서 이어감 /resume 이미 claude 를 실행한 상태에서 과거 세션을 불러옴 세션 조회는 현재 폴더 경로 기준 으로 이루어집니다. 그래서 프로젝트 폴더를 다른 경로로 옮기거나 이름을 바꾸면 이전 세션이 목록에 나타나지 않을 수 있습니다. 4. 새 세션에서도 유지되는 것과 사라지는 것 유지되는 것 CLAUDE.md : 세션을 시작할 때마다 자동으로 읽힙니다. 프로젝트 규칙, 코딩 컨벤션, 자주 쓰는 명령어를 여기에 적어두면 매번 다시 설명할 필요가 없습니다. 디스크의 파일 변경 사항 : 이미 저장된 코드 수정 결과는 그대로입니다. Claude가 파일을 직접 읽거나 git log , git diff 로 이전 작업 내역을 확인할 수 있습니다. 사라지는 것 이전 대화의 내용 자체, 즉 context입니다. "아까 하던 거 계속해줘"라고 해도 Claude는 맥락을 알 수 없습니다. 5. Ctrl+C 동작 작업 중 Ctrl+C를 한 번 누르면 진행 중인 작업이 중단되고, 연속으로 두 번 누르면 Claude Code가 종료됩니다. 6. 실무 팁 이어서 작업할 때는 claude -c 를 습관화합니다. 옵션 없이 실행하면 세션 파일이 계속 새로 쌓이므로 맥락이 끊깁니다. 중요한 결정은 파일로 남깁니다. 진행 상황, 설계 결정, 남은 할 일을 CLAUDE.md 나 NOTES.md 에 정리해 두면, 새 세션에서도 "NOTES.md 읽고 이어서 진행해줘" 한마디로 맥락을 복구할 수 있습니다. 저장 기록의 보안에 유의합니다. 대화가 평문으로 저장되므로, API 키나 개인정보 같은 민감한 내용을 다뤘다면 ~/.claude/ 폴더의 접근 권한을 관리하세요. 보관 기간을 확인합니다. 오래된 세션은 일정 기간(기본값은 약 30일로 알려져 있습니다)이 지나면 자동 정리될 수 있습니다. 장기 보관이 필요한 내용은 별도 파일로 남기는 편이 안전합니다. 마치며 핵심은 세 가지입니다. 세션은 폴더(프로젝트) 단위 로 저장된다. 옵션 없이 claude 를 실행하면 새 세션 이고, 이어가려면 -c , -r , /resume 을 쓴다. 새 세션에서도 CLAUDE.md 와 디스크의 파일 상태는 남으므로, 중요한 맥락은 문서로 남겨두는 것이 가장 확실하다. ※ 보관 기간, 저장 경로 같은 세부 사항은 버전에 따라 달라질 수 있으니 게시 전에 Claude Code 공식 문서(docs.claude.com)에서 최신 내용을 한 번 확인하시길 권합니다.
이번주에는 3주차에 작성했던 코드를 ORM을 이용해서 업드레이드 하는 실습을 진행했습니다. 1. 핵심 키워드 1-1. ORM ORM(Object-Relational Mapping)은 객체와 관계형 데이터베이스의 테이블을 연결해 주는 기술입니다. 개발자는 데이터베이스의 테이블을 Java 객체인 Entity로 표현하고, Repository를 통해 데이터를 조회하거나 저장할 수 있습니다. ORM을 사용하면 반복적인 CRUD 작업에서 SQL을 직접 작성하는 코드를 줄일 수 있습니다. 하지만 ORM을 사용하더라도 내부적으로는 SQL이 실행되기 때문에 SQL과 데이터베이스에 대한 이해가 필요합니다. 1-2. JPA와 Hibernate JPA(Java Persistence API)는 Java에서 ORM을 사용하기 위한 표준 명세입니다. Hibernate는 JPA의 표준을 실제로 구현한 대표적인 ORM 구현체입니다. 즉, JPA가 ORM을 사용하기 위한 규칙이라면 Hibernate는 그 규칙을 실제 코드로 동작하게 만들어 주는 구현체입니다. 1-3. Entity Entity는 데이터베이스의 테이블을 Java 클래스로 표현한 객체입니다. @Entity를 사용하면 해당 클래스가 JPA에서 관리하는 Entity임을 나타냅니다. @Id는 테이블의 기본 키를 나타내고, @Column을 사용하면 Java 필드와 데이터베이스의 컬럼을 연결할 수 있습니다. 예를 들어 book 테이블은 Book Entity로, category 테이블은 Category Entity로 표현할 수 있습니다. 1-4. Entity 관계 매핑 Entity 사이의 데이터베이스 관계를 Java 객체의 관계로 표현하는 것을 관계 매핑이라고 합니다. Book과 Category처럼 여러 개의 Book이 하나의 Category에 속하는 경우 @ManyToOne을 사용하여 다대일 관계를 표현할 수 있습니다. @JoinColumn은 관계를 연결하는 데이터베이스의 외래 키 컬럼을 지정합니다. 1-5. Persistence Context Persistence Context(영속성 컨텍스트)는 JPA가 Entity를 관리하는 공간입니다. Entity를 저장하거나 조회하면 JPA가 해당 Entity를 Persistence Context에서 관리하고, 변경 사항을 추적할 수 있습니다. 트랜잭션이 커밋되기 전까지 SQL 실행이 지연될 수 있으며, flush 시점에 변경 내용이 데이터베이스에 반영됩니다. 1-6. Repository Repository는 데이터베이스에 접근하는 역할을 담당하는 객체입니다. Spring Data JPA에서는 JpaRepository를 상속하면 기본적인 조회, 저장, 수정, 삭제 기능을 사용할 수 있습니다. 또한 메서드 이름을 이용하여 원하는 조건의 조회 기능을 만들 수 있습니다. 예를 들어 findAllByOrderByBookIdDesc()는 모든 Book을 bookId 기준 내림차순으로 조회합니다. 1-7. DTO DTO(Data Transfer Object)는 API를 통해 데이터를 주고받기 위한 객체입니다. Entity를 API에 그대로 사용하지 않고 DTO를 사용하면 데이터베이스 구조와 API의 데이터 형식을 분리할 수 있습니다. 요청 데이터를 받는 Request DTO와 응답 데이터를 전달하는 Response DTO를 별도로 만들 수 있습니다. 1-8. Validation Validation은 API로 들어오는 요청 데이터가 올바른 형식인지 확인하는 기능입니다. @Valid를 사용하면 DTO에 설정한 검증 조건을 요청 데이터에 적용할 수 있습니다. @NotNull은 값이 null인지 확인하고, @NotBlank는 문자열이 비어 있거나 공백인지 확인하며, @Size는 문자열 등의 크기를 제한할 때 사용합니다. 1-9. N+1 Query N+1 문제는 연관된 Entity를 조회하는 과정에서 예상보다 많은 SQL 쿼리가 실행되는 문제입니다. 예를 들어 게시글 10개를 한 번 조회한 후 각 게시글의 댓글을 조회하면서 10번의 추가 쿼리가 발생하면 총 11번의 쿼리가 실행됩니다. JOIN FETCH나 @BatchSize 등을 사용하여 불필요한 추가 쿼리를 줄일 수 있습니다. 1-10. Migration Migration은 데이터베이스의 테이블이나 컬럼 등의 구조 변경을 버전별로 관리하는 방법입니다. Entity의 변경 내용을 운영 데이터베이스에 자동으로 반영하는 방식은 예상하지 못한 스키마 변경이나 데이터 손실이 발생할 수 있기 때문에 주의해야 합니다. 운영 환경에서는 Flyway나 Liquibase와 같은 Migration 도구를 사용하여 스키마 변경을 명시적으로 관리할 수 있습니다. 2. 실습 2-1. 테이블을 Entity로 변환 기존 데이터베이스의 book과 category 테이블을 JPA에서 사용할 수 있도록 Entity 클래스로 변환했습니다. @Entity @Table(name = "category") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "category_id") private Long categoryId; @Column(nullable = false) private String name; } @Entity를 사용하여 Category 클래스를 JPA가 관리하는 Entity로 지정했습니다. @Table을 통해 실제 데이터베이스의 category 테이블과 연결하고, @Id와 @GeneratedValue를 사용하여 기본 키를 매핑했습니다. 다음으로 book 테이블을 Book Entity로 만들었습니다. @Entity @Table(name = "book") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "book_id") private Long bookId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id", nullable = false) private Category category; @Column(nullable = false, length = 100) private String title; @Column(columnDefinition = "TEXT") private String description; @Column(name = "is_available", nullable = false) private Boolean isAvailable = true; } 기존 book 테이블의 category_id가 category 테이블을 참조하고 있기 때문에 @ManyToOne과 @JoinColumn을 사용했습니다. 이를 통해 여러 개의 Book이 하나의 Category에 속하는 다대일 관계를 Entity에서도 표현했습니다. 2-2. ORM 연결과 Repository 등록 JPA를 사용하기 위해 build.gradle에 Spring Data JPA 의존성을 추가했습니다. implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 기존 3주차에서는 JdbcTemplate을 사용하여 직접 SQL을 작성했지만, 이번에는 Spring Data JPA의 JpaRepository를 사용했습니다. public interface BookRepository extends JpaRepository<Book, Long> { List<Book> findAllByOrderByBookIdDesc(); } JpaRepository<Book, Long>을 상속하면 Book Entity에 대한 기본적인 조회, 저장, 수정, 삭제 기능을 사용할 수 있습니다. 또한 findAllByOrderByBookIdDesc()와 같이 메서드 이름을 작성하면 별도의 SQL을 직접 작성하지 않아도 bookId를 기준으로 내림차순 조회할 수 있습니다. 2-3. DTO로 API의 약속 만들기 Entity를 API의 요청과 응답에 그대로 사용하지 않고 DTO를 만들었습니다. 먼저 도서를 등록할 때 사용할 요청 DTO를 작성했습니다. public record CreateBookRequest( @NotNull Long categoryId, @NotBlank @Size(max = 100) String title, String description ) { } categoryId는 반드시 입력되어야 하고, title은 비어 있으면 안 되도록 검증 조건을 추가했습니다. 다음으로 GET 요청에서 사용할 응답 DTO를 작성했습니다. public record BookResponse( Long bookId, String title, String description, String categoryName, Boolean isAvailable ) { } API에서 필요한 값만 응답하도록 BookResponse를 만들었기 때문에 Entity의 전체 구조와 API 응답 형식을 분리할 수 있었습니다. 2-4. GET /books 만들기 Entity, Repository, DTO를 만든 후 실제 GET API를 구현했습니다. 먼저 Service에서 Repository를 통해 도서를 조회하고 BookResponse로 변환했습니다. @Transactional(readOnly = true) public List<BookResponse> getBooks() { return bookRepository.findAllByOrderByBookIdDesc() .stream() .map(BookResponse::from) .toList(); } findAllByOrderByBookIdDesc()를 통해 도서를 최신순으로 조회하고, 조회된 Book Entity를 BookResponse로 변환했습니다. 그다음 Controller에서 GET 요청을 받을 수 있도록 작성했습니다. @GetMapping public List<BookResponse> getBooks() { return bookService.getBooks(); } 이렇게 하면 다음과 같은 흐름으로 요청이 처리됩니다. GET /books ↓ BookController ↓ BookService ↓ BookRepository ↓ Database ↓ BookResponse 마지막으로 Postman에서 GET http://localhost:8080/books 를 요청하여 도서 ID, 제목, 설명, 카테고리 이름, 대여 가능 여부가 최신순으로 반환되는 것을 확인했습니다. 마무리 이번 주차 워크북에서는 3주차에 작성했던 기존 코드를 발전시키는 과정이라 과제 자체는 크게 어렵지 않았지만, DTO와 Entity 같은 새로운 개념과 파일들이 추가되면서 각각의 파일이 어떤 역할을 하고 서로 어떻게 연결되는지 조금 헷갈렸습니다. 특히 DB 테이블을 Entity로 변환할 때 어노테이션을 사용해 컬럼 이름, null 가능 여부, 기본키, 관계 등을 하나씩 설정해야 해서 확인해야 할 부분이 많아 어려움을 조금 느꼈습니다. 단순히 테이블을 코드로 옮기는 것뿐만 아니라 데이터베이스와 객체의 관계까지 생각해야 한다는 점에서 ORM의 특징을 조금 더 이해할 수 있었습니다. 반면 Repository를 사용하면서 편리함을 크게 느꼈습니다. 특히 findAllByOrderByBookIdDesc()처럼 메서드 이름만으로 원하는 정렬 조건의 데이터를 조회할 수 있다는 점이 인상적이었습니다. 직접 SQL을 작성하지 않아도 필요한 데이터를 조회할 수 있다는 점에서 ORM을 사용하는 이유를 체감할 수 있었습니다. 아직 Entity, DTO, Repository, Controller 등의 관계가 완전히 익숙하지는 않지만, 각각의 역할을 구분하면서 코드를 작성해보니 앞으로 API 구조를 이해하는 데 도움이 될 것 같습니다.
이번 4주차에서는 Flutter에서 비동기 작업을 처리하는 방법과 비동기 작업의 결과에 따라 화면을 다르게 보여주는 방법을 학습했다. 특히 Future , async/await , FutureBuilder 를 이용하여 영화 데이터를 불러오는 과정을 구현하고, Loading / Empty / Error / Success 상태를 각각 처리했다. 또한 mounted 를 이용해 비동기 작업이 끝난 후 Widget의 상태를 안전하게 변경하는 방법과 shared_preferences 를 이용해 마지막으로 선택한 장르를 저장하고 앱을 다시 실행했을 때 복원하는 기능까지 구현했다. 1. Future Flutter에서 서버 요청이나 파일 읽기처럼 시간이 걸릴 수 있는 작업은 바로 결과를 반환하지 않을 수 있다. 이처럼 나중에 결과가 완료되는 작업을 표현할 때 Future 를 사용한다. Future<T> 에서 T 는 비동기 작업이 완료되었을 때 반환되는 값의 타입을 의미한다. 예를 들어 문자열을 나중에 반환하는 함수라면 다음과 같이 작성할 수 있다. Future<String> getMessage() async { return 'Hello Flutter'; } 여기서 Future<String> 은 지금 당장 String을 반환하는 것이 아니라, 나중에 String이 완료될 예정이라는 의미이다. 숫자를 반환한다면 Future<int> , 영화 목록을 반환한다면 Future<List<Movie>> 처럼 사용할 수 있다. Future<int> getNumber() async { return 10; } Future<List<Movie>> getMovies() async { return movies; } 2. async와 await async 와 await 는 비동기 작업을 처리할 때 함께 사용한다. 2-1. async 함수 뒤에 async 를 붙이면 해당 함수가 비동기 함수가 된다. Future<String> getMessage() async { return 'Hello'; } async 가 붙은 함수는 기본적으로 Future 를 반환한다. 2-2. await await 는 비동기 작업이 완료될 때까지 결과를 기다릴 때 사용한다. Future<String> getMessage() async { final message = await getMessageFromServer(); return message; } await 를 사용하면 비동기 작업이 완료된 이후의 결과를 받아서 다음 코드를 실행할 수 있다. 중요한 점은 await 를 사용하는 함수에는 async 가 필요하다는 것이다. Future<void> loadData() async { final data = await getData(); } 3. try-catch-finally try { // 예외가 발생할 수 있는 코드 } catch (e) { // 예외가 발생했을 때 실행 } finally { // 성공 여부와 관계없이 실행 } 각각의 역할은 다음과 같다. try → 실행할 코드를 작성 catch → 예외가 발생했을 때 처리 finally → 예외 발생 여부와 관계없이 마지막에 실행 예를 들어 데이터를 가져오는 과정에서 오류가 발생할 수 있다면 다음과 같이 작성할 수 있다. try { final data = await getData(); } catch (e) { print('데이터를 가져오는 중 오류가 발생했습니다.'); } finally { print('작업이 종료되었습니다.'); } 4. FutureBuilder FutureBuilder<T> 는 Future 의 결과에 따라 화면을 구성할 때 사용하는 Widget이다. 비동기 작업은 시간이 걸리기 때문에 작업이 진행되는 동안에는 Loading 화면을 보여주고, 작업이 완료되면 결과에 따라 화면을 보여줘야 한다. 이때 FutureBuilder 를 사용할 수 있다. FutureBuilder<String>( future: getMessage(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } return Text(snapshot.data ?? ''); }, ) 여기서 FutureBuilder<String> 의 String 은 Future가 최종적으로 반환하는 데이터의 타입이다. 즉, Future<String> 을 사용한다면 FutureBuilder<String> 처럼 작성할 수 있다. 4-1. AsyncSnapshot FutureBuilder 의 builder 에서는 snapshot 을 통해 비동기 작업의 상태와 결과를 확인할 수 있다. 대표적으로 다음과 같은 값을 확인할 수 있다. snapshot.connectionState snapshot.data snapshot.hasError 예를 들어 Loading 상태인지 확인하려면 다음과 같이 작성할 수 있다. if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } 오류가 발생했는지는 다음과 같이 확인할 수 있다. if (snapshot.hasError) { return const Text('오류가 발생했습니다.'); } 그리고 정상적으로 데이터가 전달되었다면 snapshot.data 를 이용해 결과를 가져올 수 있다. 5. mounted Flutter에서는 비동기 작업이 진행되는 동안 Widget이 화면에서 사라질 수 있다. 예를 들어 화면에서 데이터를 불러오는 작업을 시작했는데 사용자가 그 전에 다른 화면으로 이동할 수도 있다. 이 상태에서 비동기 작업이 끝난 후 setState() 를 호출하면 문제가 발생할 수 있다. 따라서 비동기 작업이 끝난 후 현재 Widget이 아직 화면에 존재하는지 확인할 수 있다. 이때 사용하는 것이 mounted 이다. Future<void> loadData() async { final data = await getData(); if (!mounted) { return; } setState(() { // 상태 변경 }); } mounted 가 false 라면 해당 State가 더 이상 Widget tree에 연결되어 있지 않다는 의미이다. 따라서 mounted 를 확인한 후 setState() 를 호출하면 안전하게 상태를 변경할 수 있다. 6. shared_preferences 앱을 종료했다가 다시 실행하면 일반적인 변수에 저장한 값은 사라진다. 예를 들어 다음과 같은 변수에 장르를 저장했다면 String selectedGenre = '드라마'; 앱을 종료하고 다시 실행했을 때 다시 초기값으로 돌아간다. 앱을 다시 실행해도 유지해야 하는 간단한 설정값은 로컬 저장소에 저장할 수 있다. Flutter에서는 shared_preferences 패키지를 사용할 수 있다. flutter pub add shared_preferences shared_preferences 는 문자열, 숫자, boolean과 같은 간단한 데이터를 로컬에 저장할 때 사용할 수 있다. 이번 실습에서는 새로운 코드에서 비동기 방식으로 동작하는 SharedPreferencesAsync 를 사용했다. final preferences = SharedPreferencesAsync(); 값을 저장할 때는 다음과 같이 사용할 수 있다. await preferences.setString( 'selected_genre', '드라마', ); 저장된 값을 가져올 때는 다음과 같이 작성한다. final genre = await preferences.getString( 'selected_genre', ); 여기서 selected_genre는 저장할 데이터를 구분하기 위한 Key이다. 마무리 이번 4주차에서는 Flutter에서 비동기 작업을 처리하는 방법과 그 결과에 따라 화면의 상태를 나누는 방법을 학습했다. 특히 Future<String> 과 같은 Future<T> 의 개념부터 async/await , FutureBuilder , AsyncSnapshot 까지 비동기 작업이 어떤 흐름으로 처리되는지 이해할 수 있었다. 실습에서는 FakeMovieService 를 이용해 실제 API 없이도 영화 데이터를 비동기로 불러오는 상황을 만들었고, FutureBuilder 를 이용하여 Loading, Empty, Error, Success 상태를 각각 구현했다. 또한 Error 상태에서는 다시 시도 버튼을 눌렀을 때만 새로운 Future를 생성하도록 하면서 비동기 작업을 다시 실행하는 방법도 익혔다. mounted 를 이용해서 비동기 작업이 끝난 후 Widget이 아직 존재하는지 확인하는 방법도 배웠고, SharedPreferencesAsync 를 이용해서 사용자가 마지막으로 선택한 장르를 저장하고 앱을 다시 실행했을 때 복원하는 기능도 구현했다. 3주차에서는 화면을 연결하고 데이터를 전달하는 과정에 집중했다면, 4주차에서는 데이터를 불러오는 과정에서 발생할 수 있는 여러 상태를 처리하는 방법까지 확장해서 학습할 수 있었다. 처음에는 Future와 FutureBuilder가 낯설었지만 실제로 Loading부터 Error, Empty, Success까지 직접 구현해보면서 비동기 데이터가 화면에 표시되기까지의 흐름을 조금씩 이해할 수 있었다.