블로그

양성빈

[인프런 워밍업 스터디 클럽] 0기 백엔드 미션 - 어노테이션(Day1)

어노테이션 서론드디어 '인프런 워밍업 스터디 클럽 0기' 첫 날이 밝아왔다. 강의를 듣고 미션을 보니 어노테이션에 관련한 내용이었다.나는 이 미션을 보고 오히려 기쁜 마음이 들었다. 😆 내가 강의를 들으면서 어노테이션 부분이 많이 궁금하였는데 이렇게 공부하게 될 계기가 생긴 것 같아서 미션도 완성시키고 나 스스로 깊게 공부도 할 겸 미션을 시작할려고 한다. 미션 내용은 아래와 같다.진도표 1일차와 연결됩니다우리는 최초로 API를 만들어 보았습니다. GET API를 만들기 위해 사용했던 어노테이션에 익숙하지 않다면 자바 어노테이션에 대해서 몇 가지 블로그 글을 찾아보세요! 다음 질문을 생각하며 공부해보면 좋습니다! 😊 [질문]어노테이션을 사용하는 이유 (효과) 는 무엇일까?나만의 어노테이션은 어떻게 만들 수 있을까?내가 알아본 어노테이션의 정의나는 강의의 실습을 통하여 스프링 부트 프로젝트를 생성하고, GET API를 만들어보고 포스트맨을 통하여 테스트 작업도 해보았다. 나는 여기서 다양한 어노테이션들을 볼 수 있었다. @SpringBootApplication, @RestController, @GetMapping 등 여러 어노테이션들을 볼 수 있었다. 여기서 나는 어노테이션이 무엇일까 고민을 해보았다. 단순히 어노테이션은 @ 붙인거라고만 알고 있었기에 이번 기회에 미션도 수행할 겸 깊게 알아보는 것도 좋다 생각하여 공부해보기로 하겠다.먼저 어노테이션이 대체 어떤 정의가 있는지 구글링을 해보기로 하였다. 구글링을 해보니, 다양한 블로그들이 나왔지만 정의가 수록된 위키백과를 먼저 참조해보기로 하였다. 위키백과는 다음과 같이 정의를 내렸다. 자바 어노테이션은 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종이다. 보통 @ 기호를 앞에 붙여서 사용한다. JDK 1.5 버전 이상에서 사용 가능하다. 자바 어노테이션은 클래스 파일에 임베디드되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동한다. 그리고 강의 중에 코치님께서도 어노테이션에 대해 아래와 같이 언급해주셨다. 어노테이션은 어노테이션마다 너무 다양한 역할을 한다. 또한 마법같은 일을 자동으로 해준다는 것이다.예를 들어서, @SpringBootApplication 어노테이션은 스프링을 실행시킬 때 다양한 설정이 필요한데 이 설정을 모두 자동으로 해준다. 또한 이런것이 가장 핵심적인 마법같은 일이다.위키사전, 코치님의 설명을 통해 어노테이션의 정의를 알 수 있었다. 좀 더 내가 설명한 식으로 풀어보자면 다음과 같다.자바의 어노테이션은 코드에 추가 정보를 제공하는 데 사용되며, 컴파일 시간, 배포 시간, 또는 실행 시간에 해당 정보를 활용할 수 있습니다. 이를 통해 개발자는 코드에 메타데이터를 추가하여 코드의 가독성, 유지 보수성을 향상시키고, 다양한 도구와 프레임워크에서 활용될 수 있는 정보를 제공할 수 있습니다.좀 더 자세히 풀어보자.어노테이션은 자바 5부터 도입된 기능으로, 코드에 대한 메타데이터를 제공하는 방법입니다. 어노테이션은 주석과 비슷하지만, 실제로 코드에 영향을 줄 수 있으며, 컴파일러에게 정보를 제공하거나 실행 시간에 특정 동작을 하도록 할 수 있습니다. 어노테이션은 선언적 형태로 코드 안에 포함되어, 클래스, 메소드, 변수 등 다양한 요소에 적용될 수 있습니다.이제 위의 내용을 좀 더 정리해보겠다. 어노테이션이란?자바를 개발한 사람들은 소스코드에 대한 문서를 따로 만들기보다 소스코드와 문서를 하나의 파일로 관리하는 것이 낫다고 생각했다. 그래서 소스코드의 주석에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML 문서를 생성해내는 프로그램(javadoc.exe)를 만들어 사용했다. 그런데 여기서 의문점이 하나 든다. 🙋🏻 왜 어노테이션이라는 것을 살펴보려 하는데 주석이라는 내용이 먼저 나올까? 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 어노테이션이다.어노테이션은 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다. 📚 어노테이션(annotation)의 뜻은 주석, 주해, 메모이다.package org.example; public @interface SampleAnnotation { }위의 코드는 인텔리제이로 나의 어노테이션을 만든 코드이다.그럼 인텔리제이로 어노테이션을 만드는 것도 끝났으니 이제 끝인가? 나는 여기서 더 나아가서 이 어노테이션 코드가 .class파일로 컴파일 되었을 때 어떻게 나오는지 보고 싶어서 터미널로 컴파일을 해보았다. 컴파일 결과는 다음과 같다.public interface org.example.SampleAnnotation extends java.lang.annotation.Annotation { }컴파일 시점에 extends 한적 없는 java.lang.annotation.Annotation 이 extends 되어 있다. 이제 좀 더 자세한 어노테이션의 내용과 활용법을 알아가보자. 어노테이션은 JDK에서 기본적으로 제공하는 것과 다른 프로그램에서 제공하는 것들이 있는데, 어느 것이든 그저 약속된 형식의 정보를 제공하기만 하면 될 뿐이다.JDK에서 제공하는 표준 어노테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다. 📚 JDK에서 제공하는 어노테이션은 'java.lang.annotation' 패키지에 포함되어 있다.어노테이션은 코드에 넣는 주석이다. 완전히 주석같지는 않지만 그 비슷한 부류이다.주석이기 때문에, 실행되는 코드라고 생각하면 안된다. 어노테이션은 기능을 가지고 있는 것이라 착각을 할 수 있지만 어노테이션은 마크, 표시 해놓는 주석이다. 어노테이션은 다이나믹하게 실행되는 코드는 들어가지 않는다.즉, 런타임에 알아내야 하는 것들은 못 들어간다.위의 내용을 좀 더 풀어쓰면 컴파일러 수준에서 해석이 되야 하거나, 완전히 정적이어야 한다는 말이다.이유를 아래 코드로 보여주겠다. package me.sungbin.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { private static final String hello = "/hello"; @GetMapping(hello) public String hello() { return "hello"; } }위와 같이 hello 변수는 정적 변수이므로 @GetMapping 어노테이션에 사용할 수 있다.하지만. hello 변수가 동적인 변수라면 컴파일 에러가 발생한다.아래의 코드를 보자. 컴파일 에러가 발생하는 것을 볼 수 있을 것이다. 간략한 어노테이션 정의 방법새로운 어노테이션을 정의하는 방법은 아래와 같다.'@'기호를 붙이는 것을 제외하면 인터페이스 정의와 동일하다. package me.sungbin; public @interface SampleAnnotation { 타입요소이름(); } 📚 타입요소등, 어노테이션 정의에 대한 자세한 정의방법과 내용들은 구체적인 내용들을 확인 후, 살펴보자.자바의 표준 어노테이션자바에서 기본적으로 제공하는 어노테이션들은 몇 개 없다.그나마 이들의 일부는 '메타 어노테이션(meta annotation)' 으로 어노테이션을 정의하는데 사용되는 어노테이션의 어노테이션이다. 표준 어노테이션과 메타 어노테이션@Override: 컴파일러에게 오바리이딩하는 메서드라는 것을 알린다.@Deprecated: 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.@SuppressWarnings: 컴파일러의 특정 경고메시지가 나타나지 않게 해준다.@SafeVarags: 제네릭스 타입의 가변인자에 사용한다. (JDK 1.7)@FunctionalInterface: 함수형 인터페이스라는 것을 알린다. (JDK 1.8)@Native: native 메서드에서 참조되는 상수 앞에 붙인다. (JDK 1.8)@Target*: 어노테이션이 적용가능한 대상을 지정하는데 사용한다.@Documented*: 어노테이션 정보가 javadoc으로 작성된 문서에 포함되게 한다.@Inherited*: 어노테이션이 자손 클래스에 상속되도록 한다.@Retention*: 어노테이션이 유지되는 범위를 지정하는데 사용한다.@Repeatable*: 어노테이션을 반복해서 사용할 수 있게 한다. (JDK 1.8)*이 붙은 것이 메타 어노네이션이다.📚 메타 어노테이션: 어노테이션을 정의하는데 사용하는 어노테이션의 어노테이션 @Override현재 메서드가 슈퍼 클래스의 메서드를 오버라이드한 것임을 컴파일러에게 명시해준다.메서드가 슈퍼클래스에 없다면 에러를 발생시기 때문에 오타와 같은 실수도 잡을 수 있다. @Deprecated마커 어노테이션으로 다음 버전에 지원되지 않을 수도 있기 때문에 앞으로 사용하지 말라고 경고를 알린다.@Deprecated를 붙인 메서드는 인텔리제이에서 아래의 사진과 같이 표시해준다. @SuppressWarning경고를 제거하는 어노테이션으로 개발자가 의도를 가지고 설계를 했는데 컴파일은 이를 알지 못하고 컴파일 경고를 띄울 수 있기 때문에 이를 제거하는 목적이다. @SafeVarargsJava 7이상에서 사용가능하고 제네릭같은 가변인자 매개변수 사용시 경고를 무시한다제네릭사용할 클래스,메서드 내부에서의 데이터타입을 외부에서 지정하는 기법 @FunctionalInterfaceJava 8이상에서 사용가능하고 컴파일러에게 함수형 인터페이스라는 것을 알리는 어노테이션이다.메타 어노테이션'어노테이션을 위한 어노테이션' 쯕, 어노테이션에 붙이는 어노테이션으로 어노테이션을 정의할 때 어노테이션의 적용대상(target) 이나 유지기간(retention)등을 지정하는데 사용된다. 📚 메타 어노테이션은 java.lang.annotation 패키지에 포함되어 있다. @Target어노테이션이 적용가능한 대상을 지정하는데 사용한다.아래 예제는 '@SuppressWarnings' 를 정의한 것인데, 이 어노테이션에 적용할 수 있는 대상을 '@Target' 으로 지정한다.여러 개의 값을 지정할 때는 배열처럼 괄호{} 를 이용하여 지정할 수 있다.package me.sungbin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); } @Target으로 지정할 수 있는 어노테이션 적용대상의 종류ANNOTATION_TYPE: 어노테이션CONSTRUCTOR: 생성자FIELD: 필드(멤버 변수, ENUM 상수)LOCAL_VARIABLE: 지역변수METHOD: 메서드PACKAGE: 패키지PARAMETER: 매개변수TYPE: 타입(클래스, 인터페이스, ENUM)TYPE_PARAMETER: 타입 매개변수(JDK1.8)TYPE_USE: 타입이 사용되는 모든 곳(JDK1.8)📚 java.lang.annotation.ElementType 이라는 열거형에 정의되어 있다. static import문을 사용하면 ElementType.TYPE 이 아니라 TYPE 과 같이 간략히 사용할 수 있다. TYPE은 타입을 선언할 때 어노테이션을 붙일 수 있다는 뜻TYPE_USE는 해당 타입의 변수를 선언할 때 붙일 수 있다는 뜻이다.FIELD 는 기본형에 사용할 수 있고, TYPE_USE는 참조형에 사용된다는 점을 주의한다.타입 선언부제네릭 타입, 변수 타입, 매개변수 타입, 예외 타입...타입에 사용할 수 있으려면TYPE_PARAMETER : 타입 변수에만 사용할 수 있다.TYPE_USE : 타입 변수를 포함해서 모든 타입 선언부에 사용할 수 있다.package me.sungbin; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({FIELD, TYPE, TYPE_USE}) public @interface MyAnnotation { } package me.sungbin; import me.sungbin.controller.HelloController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MyAnnotation public class AnnotationTestApplication { @MyAnnotation int i; @MyAnnotation HelloController helloController; public static void main(String[] args) { SpringApplication.run(AnnotationTestApplication.class, args); } } @Retention어노테이션 유지되는 기간을 지정하는데 사용한다. 어노테이션 유지정책의 종류SOURCE: 소스 파일에만 존재. 클래스파일에는 존재하지 않는다.CLASS: 클래스 파일에 존재. 실행 시에 사용 불가능하다. (기본값)RUNTIME: 클래스 파일에 존재하며 실행시에 사용 가능하다.SOURCE -> CLASS -> RUNTIMESOURCE는 소스코드만 유지하겠다.컴파일 시에만 사용하겠다는 것!컴파일하고 나면 어노테이션은 없어진다. -> 바이트코드에도 남아있지 않다.CLASS애노테이션에 대한 정보를 클래스 파일까지, 즉 바이트 코드에도 남겨 두겠다.클래스 정보를 읽어들이는 방법(바이트 코드를 읽어들이는)을 바탕으로 애노테이션 정보를 읽어와서 처리할 수 있다.예) BYTE BUDDY, ASM 활용바이트 코드엔 남아 있지만, 이 클래스파일을 JVM이 실행할 때 클래스에 대한 정보를 클래스로더가 읽어서 메모리에 적재하게되고, 이후 사용 시점에 메모리에서 읽어올 때 애노테이션 정보를 제외하고 읽어옴RUNTIME위 CLASS와 동일하지만, 메모리에 적재된 클래스 정보를 읽어올 때 애노테이션 정보를 그대로 포함하는 것이다.바이트코드에서 읽어오는게 빠를까?RetentionPolicy를 CLASS로 한 이후, 바이트코드를 읽어 처리하는 라이브러리를 활용?리플렉션으로 읽어오는게 빠를까?RetentionPolicy를 CLASS로 한 이후, 바이트코드를 읽어 처리하는 라이브러리를 활용? -> 리플렉션 자체가 부하가 존재한다.-> 바이트 코드의 양에 영향을 끼친다.-> 리플렉션은 메모리에 이미 올라와 있는 정보를 읽는다. 클래스 로더가 읽고 메모리에 적재시킨 후 읽어온다. 📚 커스텀하게 만든 애노테이션이 정말로 RUNTIME 까지 필요한 정보인가? RUNTIME 까지 사용할 필요가 없다면, CLASS 레벨로 내려가거나 SOURCE 레벨로 내려갈 수도 있을 것이다. 그냥, 의례적으로 RUNTIME으로 작성하는 경우가 있었다면? 그 역할을 다시 살펴보고 명확한 Retention Policy 를 정의하자. 표준 어노테이션 중 '@Override' 나 '@SuppressWarnings' 처럼 컴파일러가 사용하는 어노테이션은 유지 정책이 'SOURCE' 이다. -> 컴파일러를 직접 작성할 것이 아니면, SOURCE 이상의 유지정책을 가질 필요가 없다. 유지 정책을 RUNTIME 으로 한다면,실행 시에 리플렉션(Reflection) 을 통해 클래스 파일에 저장된 어노테이션의 정보를 읽어서 처리 할 수 있다.Retention 정책은 RUNTIME 으로 정의하고Target은 TYPE과 FIELD로 정의한다.package me.sungbin; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { } Target이 TYPE과 FIELD 임으로 클래스에도 애노테이션을 선언할 수 있고클래스 내부의 필드에도 애노테이션을 선언할 수 있다.package me.sungbin; @MyAnnotation public class TestClass { @MyAnnotation private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } TestClass 클래스에 선언된 Annotation을 리플렉션을 이용해 확인할 수 있다.package me.sungbin; import java.lang.reflect.Field; import java.util.Arrays; public class App { public static void main(String[] args) { Arrays.stream(TestClass.class.getAnnotations()).forEach(System.out::println); Field[] declaredFields = TestClass.class.getDeclaredFields(); for (Field declaredField : declaredFields) { Arrays.stream(declaredField.getAnnotations()).forEach(System.out::println); } } }  표준 어노테이션 중 '@FunctionalInterface' 는 '@Override' 처럼 컴파일러가 체크해주는 어노테이션이지만, 실행 시에도 사용되므로 유지 정책이 "RUNTIME"으로 되어 있다. @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FunctionalInterface {} 유지 정책을 "CLASS" 으로 한다면컴파일러가 어노테이션의 정보를 클래스 파일에 저장할 수 있게 하지만,클래스 파일이 JVM에 로딩 될 때는 어노테이션의 정보가 무시되어 실행 시에 어노테이션에 대한 정보를 얻을 수 없다.→ CLASS 가 유지정책의 기본값임에도 불구하고 잘 사용되지 않는 이유 지역 변수에 붙은 어노테이션은 컴파일러만 인식할 수 있으므로, 유지 정책이 RUNTIME인 어노테이션을 지역변수에 붙여도 실행 시에는 인식되지 않는다. @Documented어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다.표준 어노테이션 중 Override와 SuppressWarnings를 제외하고 모두 Documented 메타 어노테이션이 붙어 있다. @Documented애노테이션 정보가 javadoc으로 작성된 문서에 포함된다고 한다. 이것이 무슨말일까? 내 코드가 자바docs에 올라간다는 말일까?정확히 말하면 자바docs에 올라간다는 말이 아니라,직접 javadoc을 만들 수 있다는 뜻이다.이런식으로 만들 수 있는데, Local 지역입력 ko_KRother command line arguments : 한글깨짐 방지-encoding UTF-8 -charset UTF-8 -docencoding UTF-8적절하게 내용을 채운뒤 output directory에 경로를 입력해주면 끝이다.그러면 @Documented를 붙인거와 안 붙인것을 비교해보자. 코드public class Korea implements Great{ @Override @Make public String country() { return "한국"; } } 없는거 있는거JavaDoc애노테이션을 알기 전에 JavaDoc에 대해 알아보자.JavaDoc은 Java코드에서 API문서를 HTML 형식으로 생성해주는 도구이다.HTML 형식이기 때문에 다른 API를 하이퍼 링크를 통해 접근이 가능하다. JavaDoc TagsJavaDoc은 여러 Tag를 작성하여 문서를 완성한다.Java 코드에서 애노테이션으로 추가한다.IDE에서 /** 입력 후 엔터를 치면 자동으로 형식이 생성된다.Javadoc Tags의 종류들@author@deprecated@exception@param@return@see@serial@serialData@serialField@since@throws@since@throws@version@Inherited어노테이션이 자손 클래스에 상속되도록 한다.'@Inherited' 가 붙은 어노테이션을 조상 클래스에 붙이면, 자손 클래스도 이 어노테이션이 붙은 것과 같이 인식된다.MyAnnotation은 Inherited 애노테이션을 통해 자손 클래스에도 인식되도록 정의한다.package me.sungbin; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, FIELD}) @Inherited public @interface MyAnnotation { } 부모클래스인 Sungbin클래스에 MyAnnotation을 정의package me.sungbin; @MyAnnotation("hi") public class Sungbin { @MyAnnotation("yang sung bin") private String name; }  Sungbin 클래스의 자식 클래스인 Child 클래스에는 별도의 어노테이션 정의가 없다.package me.sungbin; public class Child extends Sungbin { } 리플렉션을 이용해 Child 클래스의 어노테이션을 확인해보자.→ ChildSson 클래스에는 정의한 애노테이션이 없지만,→ 부모 클래스인 Sson 클래스에 정의한 애노테이션이 확인됨을 볼 수 있다.→ Inherited 애노테이션을 통해 자식 클래스까지 전파될 수 있음을 확인할 수 있다. Inherited 애노테이션을 바탕으로 리플렉션을 활용해 자식클래스에서부모클래스에 정의되어 있는 Inherited 애노테이션을 확인할 수 있다. 📚 리플랙션의 getDeclaredFields(); 를 하면 클래스에 정의된(선언된) 것들을 가져와서 조작할 수 있다. public이던, private 이던,, Getter와 Setter에 대해 논의를 하며 큰 비용을 소모하는 것이 크게 가치가 없다.. 객체지향을 얘기하며 Getter, Setter의 정의 관련한 내용으로 얘기할 수 있겠지만, Getter와 Setter가 없더라도 리플랙션을 이용하면 충분히 가져오고 수정할 수 있기 때문이다. 중요한건 Getter, Setter 가 아닌것 같다. @Repeatable보통은 하나의 대상에 한 종류의 어노테이션을 붙이게 되는데,'@Repeatable'이 붙은 어노테이션은 여러 번 붙일 수 있다. 일반적인 어노테이션과 달리 같은 이름의 어노테이션이 어러 개가 하나의 대상에 적용될 수 있기 때문에, 이 어노테이션들을 하나로 묶어서 다룰 수 있는 어노테이션도 추가로 정의해야 한다. @Native네티이브 메서드(native method)에 의해 참조되는 '상수 필드(constant field)'에 붙이는 어노테이션이다.여기서, 네이티브 메서드는 JVM이 설치된 OS의 메서드를 말한다.네이티브 메서드는 보통 C언어로 작성되어 있는데, 자바에서는 메서드의 선언부만 정의하고 구현하지 않는다.그래서 추상 메서드처럼 선언부만 있고 구현부가 없다. 어노테이션 타입 정의어노테이션의 요소어노테이션 내에 선언된 메서드를 어노테이션의 요소라고 한다. 📚 어노테이션에도 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 있다. 어노테이션의 요소는 반환 값이 있고 매개변수는 없는 추상 메서드의 형태를 가진다.다만, 어노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다.각 요소들은 기본값을 가질 수 있으며, 기본값이 있는 요소들은 어노테이션을 적용할 때 값을 지정하지 않으면 기본값이 사용된다.어노테이션의 요소가 오직 하나 뿐이고 이름이 value 인 경우, 어노테이션을 적용할 때 요소의 이름을 생략하고 값만 적어도 된다.요소 타입이 배열인 경우, 괄호{} 를 사용해 여러 개의 값을 지정할 수 있다.하나인 경우는 괄호{} 를 생략할 수 있다.java.lang.annotation.Annotation모든 어노테이션의 조상은 Annotation이다.그러나 어노테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotation을 조상으로 지정할 수 없다. @interface TestInfo extends Annotation{ // 에러. 허용되지 않는 표현이다. int count(); String testedBy(); ... } Annotation 을 살펴보면Annotation은 어노테이션이 아니라 일반적인 인터페이스로 정의되어 있다. 모든 어노테이션의 조상인 Annotation 인터페이스가 위와 같이 정의되어 있기 때문에모든 어노테이션 객체에 대해 equals(), hashCode(), toString() 과 같은 메서드를 호출하는 것이 가능하다.리플랙션(Reflection)을 이용해 특정 클래스에 선언된 애노테이션들을 조회하여 equals, hashCode, toString 메서드를 호출해본다.어노테이션 요소의 규칙어노테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙요소 타입은 기본형, String, Enum, 어노테이션, Class 만 허용() 안에 매개변수를 선언할 수 없다.예외를 선언할 수 없다.요소를 타입 매개변수로 정의할 수 없다.마커 어노테이션 Marker Annotation값을 지정할 필요가 없는 경우,어노테이션의 요소를 하나도 정의하지 않을 수 있다.Serializable 이나 Cloneable 인터페이스처럼, 요소가 하나도 정의되지 않은 어노테이션을 마커 어노테이션이라 한다. 🙋🏻 이런 마커 어노테이션은 왜 사용될까? 글을 찾아보니 아래의 내용이 있었다.마커 어노테이션을 통해 코드 작성 시점, 컴파일 시점, 러타임 시점에 부가적인 작업을 추가할 수 있을 것이다.코드 작성 시점에 어노테이션 정보를 통해 부가적인 정보를 check 하여 컴파일에러를 발생시킬 수 있을 것이며컴파일하는 과정에서 어노테이션 정보를 바탕으로 부가적인 정보를 포함하여 컴파일된 결과를 내보낼 수도 있을 것이다.또한, 런타임 시점에 리플랙션을 이용하여 애노테이션 정보를 바탕으로 부가적인 작업을 할 수 있을 것이다.Java8 어노테이션 변화 애노테이션 관련 큰 변화 두가지자바 8 부터 애노테이션을 타입 선언부에도 사용할 수 있게 되었다.자바 8 부터 애노테이션을 중복해서 사용할 수 있게 되었다.타입 선언부제네릭 타입변수 타입매개변수 타입예외 타입...타입에 사용할 수 있으려면TYPE_PARAMETER : 타입 변수에만 사용할 수 있다.TYPE_USE : 타입 변수를 포함해서 모든 타입 선언부에 사용할 수 있다.중복 사용할 수 있는 애노테이션을 만들기@Repeatable애노테이션들을 감싸고 있을 컨테이너 애노테이션을 선언해야 한다.중복 사용할 애노테이션 만들기컨테이너 애노테이션은 중복 애노테이션과 @Retention 및 @Target 이 같거나 더 넓어야 한다.컨테이너이기 떄문에 , 이것은 접근 지시자의 범위와 유사한 개념이라고 볼 수 있다.@Retention : 애노테이션을 언제까지 유지할 것이냐?@Target : 애노테이션을 어디에 사용할 것이냐?애노테이션 프로세서애노테이션 프로세서는 소스코드 레벨에서 소스코드에 붙어있는애노테이션을 읽어서 컴파일러가 컴파일 하는 중에 새로은 소스코드를 생성하거나 기존 소스코드를 바꿀 수 있다.또는, 클래스(바이트코드) 도 생성할 수 있고 별개의 리소스파일을 생성할 수 있는 강력한 기능이다. 애노테이션 프로세서 사용 예롬복 (기존코드를 변경한다)AutoService (리소스 파일을 생성해준다.)java.util.ServiceLoader 용 파일 생성 유틸리티@Override애노테이션 프로세서 장점바이트코드에 대한 조작은 런타임에 발생되는 조작임으로 런타임에 대한 비용이 발생한다.but. 애노테이션 프로세서는 애플리케이션을 구동하는 런타임 시점이 아니라,컴파일 시점에 조작하여 사용함으로 런타임에 대한 비용이 제로가 된다.단점은 기존의 코드를 고치는 방법은 현재로써는 public 한 API 가 없다.롬복 같은 경우.. 기존 코드를 변경하는 방법이지만 public 한 api를 이용한 것이 아님으로 해킹이라고 할 수 도 있다.롬복(Lombok)의 동작원리Lombok@Getter @Setter, @Builder 등의 애노테이션과애노테이션 프로세서를 제공하여 표준적으로 작성해야 할 코드를 개발자 대신 생성해주는 라이브러리이다.사용하기의존성 추가IntelliJ Lombok 플로그인 설치Intellij Annotation Processing 옵션 활성화동작원리컴파일 시점에 "애노테이션 프로세서"를 사용하여 (자바가 제공하는 애노테이션 프로세서)소스코드의 AST(Abstract Syntax Tree) 를 조작한다.AST에 대한 참고 사이트 (아래 참조 참고)javax.annotation.processing || Interfaec Processor⇒ 소스코드의 AST를 원래는 참조만 할 수 있다. // 수정하지 못한다. 그리고 하면 안된다!⇒ 그러나 수정이 됬음을 알 수 있다.(컴파일 이후 바이트코드 확인)⇒ 참조만 해야 하는 것을 내부 클래스를 사용하여 기존 코드를 조작하는 것임으로 "해킹" 이라고 얘기하기도 한다. 논란 거리공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.특히 이클립스의 경우에는 Java Agent를 사용하여 컴파일러 클래스까지 조작하여 사용한다.해당 클래스들 역시 공개된 API가 아니다보니 버전 호환성에 문제가 생길 수도 있고 언제라도 그런 문제가 발생해도 이상하지 않다.그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며, 대안이 몇가지 있지만 롬복의 모든 기능과 편의성을 대체하지 못하는 상황이다.AutoValueImmutables기존 Getter, Setter, equals, hasCode 등의 메소드를 생성하는 순간?해당 클래스는 이미 방대해진 모습을 볼 수 있다.해당 클래스를 위한 메소드들이 선언이 되어 있더라도 위 메소드들 사이에 파묻혀 있다면?개발자 입장에서 놓칠 수도 있다. (그래서 boilerplat 코드라는 개념도 나온다.)⇒ 롬복을 이용하여 쉽게, 그리고 가독성 높게 클래스를 구현할 수 있다. package me.sungbin; import lombok.Getter; import lombok.Setter; @Getter @Setter public class Member { private String name; private int age; }  위의 롬복이 적용된 코드를 컴파일하면 아래와 같이 나온다. package me.sungbin; public class Member { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } 결론위에서 미션에 대해 다 애기한 듯 하다. 결론을 내보겠다.어노테이션을 사용하는 이유는 단순하다.코드의 가독성과 유지보수성 향상: 어노테이션을 사용하면 개발자가 코드의 의도를 더 명확하게 표현할 수 있습니다. 예를 들어, @Override 어노테이션은 메소드가 상위 클래스의 메소드를 오버라이드한다는 것을 명시합니다.컴파일 시간 검사: 어노테이션을 통해 코드에 대한 추가적인 검사를 수행할 수 있어, 잠재적인 오류를 컴파일 시간에 발견하고 수정할 수 있습니다.런타임 처리: 특정 어노테이션이 적용된 요소를 런타임에 검사하고 처리할 수 있어, 리플렉션을 사용한 동적 처리가 가능해집니다. 이는 프레임워크와 라이브러리에서 많이 활용됩니다.이런 이유로 사용이 되며 이로인하여 코드문서화, 컴파일러에 특정처리를 지시, 코드분석 도구 지원, 런타임처리등이 가능해지게 된다. 우리가 스프링의 의존성 주입을 할 때 @Autowired도 이런 기능처리를 해준다. 컴파일러에서의 처리:코드 검증: 컴파일러는 어노테이션을 사용하여 코드에 대한 추가적인 검증을 수행합니다. 예를 들어, @Override 어노테이션은 메서드가 실제로 상위 클래스나 인터페이스의 메서드를 오버라이드하는지 확인하는 데 사용됩니다. 만약 오버라이드하는 메서드가 없다면, 컴파일러는 에러를 발생시킵니다.정책 적용: 일부 어노테이션은 컴파일러에 특정 정책을 적용하도록 지시합니다. 예를 들어, @Deprecated 어노테이션이 적용된 요소를 사용하는 코드는 컴파일러 경고를 발생시키며, 이는 개발자에게 해당 요소가 더 이상 사용되지 않아야 함을 알립니다.소스 코드 변환: 어노테이션 프로세서를 사용하여 컴파일 시점에 소스 코드를 자동으로 생성하거나 수정할 수 있습니다. 이는 코드 생성 라이브러리나 프레임워크에서 흔히 사용되는 기법입니다.런타임에서의 처리:리플렉션을 통한 접근: 런타임에는 리플렉션 API를 사용하여 어노테이션 정보에 접근할 수 있습니다. 이를 통해 개발자는 실행 중인 프로그램에서 클래스, 메서드, 필드 등에 적용된 어노테이션을 검사하고, 해당 어노테이션에 지정된 정보를 바탕으로 동적인 처리를 수행할 수 있습니다.동적 처리: 런타임에 어노테이션을 기반으로 동적 처리를 하는 예로, Java EE와 Spring 프레임워크에서 의존성 주입을 구현하는 방법을 들 수 있습니다. 이러한 프레임워크는 특정 어노테이션(@EJB, @Autowired)이 붙은 필드나 메서드를 찾아, 런타임에 자동으로 의존성을 주입합니다.구성 관리: 어플리케이션의 구성 정보를 어노테이션을 통해 관리할 수 있습니다. 예를 들어, 웹 어플리케이션에서 서블릿이나 REST 엔드포인트를 정의할 때 사용되는 어노테이션들은 런타임에 웹 서버가 해당 구성 정보를 읽어들여 서비스를 구동하는 데 사용됩니다.이러한 방식으로 어노테이션은 컴파일 시점과 런타임에 다양한 목적으로 활용됩니다. 컴파일 시점에는 코드의 정확성을 보장하고, 런타임에는 코드의 동적인 행위를 제어하는 데 중요한 역할을 합니다.커스텀 어노테이션이것 또한 위에서 예제로 많이 보여드렸으므로 어노테이션 예제를 보여줌으로 이 글을 마치려고 한다. 정말 단순히 어노테이션부터 시작해서 리플렉션까지 갔는데 정말 험난한 여정이였지만 보람찬 공부가 되었다. package me.sungbin; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, FIELD}) @Inherited public @interface MyAnnotation { String value(); }  📚 참조https://b-programmer.tistory.com/264http://javaparser.org/inspecting-an-ast/

백엔드인프런워킹업스터디클럽백엔드미션어노테이션

이양구

[인프런 워밍업 클럽 FE 0기] 미션8 - 디즈니 플러스 앱

🎞 Disney Plus APP GitHub 🎞 Disney Plus APP DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 여덟 번째 미션인 '디즈니 플러스 앱' 입니다. 따라하며 배우는 리액트 섹션 4~5(리액트로 Netflix 앱 만들기) 목표swiper 라이브러리 커스텀해보기react-oauth/google 로 구글 로그인 연동해보기 구현swiper 라이브러리 커스텀해보기// LoginPage import "swiper/css/effect-fade"; <Swiper modules={[Autoplay, EffectFade, Pagination, A11y]} autoplay={auto} effect={"fade"} pagination={{ clickable: true, }} loop={true} fadeEffect={{ crossFade: true }} slidesPerView={1} speed={2000} > {...} </Swiper> // Row.tsx import "swiper/css/mousewheel"; <Swiper modules={[Navigation, Pagination, Scrollbar, A11y, Mousewheel]} navigation pagination={{ clickable: true }} mousewheel speed={1000} spaceBetween={10} > {...} </Swiper> 2024년 3월 10일의 디즈니 플러스 메인 페이지를 그대로 옮겨보고자 swiper 라이브러리를 커스텀해봤다.로그인 페이지에서는 좌우로 넘기는 슬라이드가 아닌 fade-in-out의 슬라이드를 구현하기 위해 swiper에 EffectFade 모듈을 추가하고 fadeEffect 속성을 추가했다.이 fadeEffect가 제대로 작동하기 위해선 반드시 해당 이펙트의 css를 추가해야 한다.다른 모듈이나 컴포넌트를 추가할 때처럼 자동으로 추가되지 않으니 주의해야 한다. (이걸 몰라서 한참을 찾았다. 😥)Row 컴포넌트는 마우스 휠에 따라 움직이는 슬라이드를 만들기 위해 Mousewheel 모듈과 속성을 이용했다.이렇게 슬라이드 속성을 정한 뒤에 swiper가 렌더링하는 요소의 class를 찾아 CSS에서 원하는 디자인으로 변경하면 된다.이때 라이브러리의 CSS와 겹치는 속성이 있을 수 있기 떄문에 '!important'를 붙이는 게 좋다. react-oauth/google 로 구글 로그인 연동해보기// index.js <GoogleOAuthProvider clientId={process.env.REACT_APP_CLIENT_ID}> <BrowserRouter> <App /> </BrowserRouter> </GoogleOAuthProvider> // App.jsx const navigate = useNavigate(); const [isLogin, setIsLogin] = useState( localStorage.getItem("user") ? true : false ); useEffect(() => { isLogin ? navigate("/") : navigate("/login"); }, [isLogin]); <Routes> {isLogin ? ( <Route path="/" element={<Layout setIsLogin={setIsLogin} />}> <Route index element={<MainPage />} /> <Route path=":movieId" element={<DetailPage />} /> <Route path="search" element={<SearchPage />} /> </Route> ) : ( <Route path="login" element={<LoginPage setIsLogin={setIsLogin} />} /> )} </Routes> react-oauth/google는 구글 로그인을 지원하는 라이브러리로, 사전에 구글의 Cloud에서 API 등록을 하고 Client ID를 발급받아야 사용할 수 있다.먼저 프로젝트의 최상위에 GoogleOAuthProvider로 감싸준다.그리고 사용자의 로그인 여부에 따라 페이지를 이동시키기 위해 라우터를 설정한 App 컴포넌트에서 관련 코드를 작성했다.페이지가 렌더링 될 때 로컬 스토리지에 저장된 유저 정보를 받아오고 만약 없다면 로그인 페이지로 보내도록 했다. // loginPage const googleLogin = async (credentialResponse) => { localStorage.setItem( "user", JSON.stringify(jwtDecode(credentialResponse.credential)) ); setIsLogin(true); }; <GoogleLogin onSuccess={(credentialResponse) => googleLogin(credentialResponse)} /> GoogleLogin 컴포넌트는 react-oauth/google 라이브러리에서 지원하는 버튼 컴포넌트로 디자인 및 로그인 관련 함수가 내장되어 있다.onSuccess는 사용자의 로그인이 성공했을 때 실행되는 콜백 함수이며, 인자로 로그인한 유저의 정보를 담은 데이터를 갖는다.여기서 credential이라는 값은 유저의 정보를 담고 있는 토큰으로 암호화되어 있기 때문에 jwt-decode 라이브러리를 이용해 디코딩하여 사용해야 한다.여기서 받은 picture는 사용자의 프로필 이미지 링크를 포함하고 있어서 Nav 컴포넌트에서 사용해 로그인한 유저의 프로필 이미지로 변경했다. 회고'Netflix 앱 만들기'를 하면서 사용했던 기술이 대부분이라 오래 걸리지 않을 것 같았지만...라이브러리 알아보고 문서 읽고 실행해보고... 하는 데 너무 오래 걸린 것 같다.배너 하단의 카테고리 부분은 이전에 같은 과제를 하셨던 분의 깃허브를 참고했다. (https://github.com/kimneighbor/clone-disney-plus-app)로그인 페이지는 따라하기 싫어서 현재 디즈니 플러스 홈페이지를 보고 참고했다.그대로 하면 얼마 안 걸릴 거라 생각했는데 생각보다 라이브러리 커스텀에서 좀 애를 먹었다. 😅with_networks: "2739" 2739는 TMDB에서 디즈니 플러스 방송사(networks) 코드라서 axios의 instance 기본 값에 추가했다.몇몇 요청은 해당 파라미터가 통하지 않거나 오류를 보내기도 해서 완벽하진 않다.디즈니 플러스에서 API를 제공했다면 더 알맞게 페이지를 구현할 수 있었을 텐데 하는 아쉬움이 남는다.한편 영화 정보 API를 제공해주는 TMDB(The Movie Database) 같은 곳이 있어 감사하고 다행이라는 생각이 들었다.프론트엔드 공부하는데 API를 제공해주는 곳이 아예 없었다면 혹은 매번 일정 비용을 지불해야 했다면 얼마나 힘들었을까로그인도 사실 좀 더 좋은 라우팅 구조나 상태 관리 라이브러리를 공부하고 사용해보고 싶었지만...계속 욕심만 커지는 것 같아 최대한 간단하게 구현하려 했다.(사실 과제 밀려서 조바심에 아무것도 못 했다... 😂) 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션7 - 예산 계산기 앱

💸 Budget Calculator APP GitHub 💸 Budget Calculator APP DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 일곱 번째 미션인 '예산 계산기 앱' 입니다. 따라하며 배우는 리액트 섹션 0~3(To-Do 앱) 목표의존성 배열(Dependency Array) 을 이용해 함수 실행하기state 를 전역 변수처럼(?) 사용해보기 구현구조|-- App | |-- Form | |-- Lists | | |-- List  의존성 배열(Dependency Array) 을 이용해 함수 실행하기// App.jsx const [budgetList, setBudgetList] = useState( JSON.parse(localStorage.getItem("budgetList")) || [] ); useEffect(() => { localStorage.setItem("budgetList", JSON.stringify(budgetList)); }, [budgetList]); const totalCost = useCallback(() => { return budgetList.reduce((acc, cur) => acc + cur.cost, 0); }, [budgetList]); 최상위 컴포넌트인 <App> 컴포넌트에서 만든 'budgetList'이라는 state를 useEffect와 useCallback의 의존성 배열에 추가했다.useEffect에서는 해당 state가 변경되면 로컬 스토리지의 budgetList를 최근의 리스트로 변경한다.이렇게 하면 일일이 setBudgetList가 호출되는 곳마다 함수를 사용하지 않아도 된다.다음은 예산의 총 금액을 반환하는 함수가 리스트가 변경될 때마다 실행되도록 useCallback으로 감싸고 의존성 배열에 state를 추가했다.// Form.jsx const budgetNameRef = useRef(); const [budgetName, setBudgetName] = useState(""); const [budgetCost, setBudgetCost] = useState(0); useEffect(() => { if (isEdit) { setBudgetName(budget.name); setBudgetCost(budget.cost); budgetNameRef.current.focus(); } }, [isEdit]); <Form> 컴포넌트에서는 useEffect에 'isEdit'이라는 state를 의존성 배열에 추가했다.사용자가 예산을 수정하기 위해 list의 Edit 버튼을 클릭하면 해당 budget의 name과 cost를 최근 state로 불러오고, useRef를 이용해 name을 입력하는 <input> 요소에 focus 상태가 되도록 했다.state 를 전역 변수처럼(?) 사용해보기// App.jsx const [currentBudget, setCurrentBudget] = useState({ isEdit: false, budget: {}, }); // List.jsx const handleEdit = () => { setCurrentBudget({ isEdit: true, budget: list, }); setHandleStatus({ type: "edit", message: "Editing..." }); }; // Form.jsx const handleBudgetSubmit = (e) => { const newBudget = { id: Date.now(), name: budgetName, cost: budgetCost, }; // isEdit의 값에 따라 새로 추가할지 수정할지 결정 if (isEdit) { setBudgetList((prevBudgetList) => { const newBudgetLists = [...prevBudgetList]; const index = newBudgetLists.findIndex(({ id }) => id === budget.id); newBudgetLists[index] = newBudget; return newBudgetLists; }); setCurrentBudget({ isEdit: false, budget: {} }); setHandleStatus({ type: "submit", message: "Edit Success!" }); } else { setBudgetList((prevBudgetLists) => [...prevBudgetLists, newBudget]); setHandleStatus({ type: "submit", message: "Submit Success!" }); } // submit 종료 시 input의 데이터를 초깃값으로 설정 setBudgetName(""); setBudgetCost(0); }; 배웠던 To Do 앱은 List의 Edit 버튼을 클릭했을 때 해당 List의 요소를 input 요소로 변경시키고 수정을 했다.하지만 과제는 클릭을 했을 때 List의 요소를 변경시키는 게 아니라 Form의 input에 해당 예산의 데이터를 전달해야 했다.그래서 마치 전역 변수처럼 사용할 'currentBudget'이라는 state를 생성하고 'isEdit'이라는 boolean 값과 수정할 예산의 데이터를 담을 'budget'이라는 값을 설정했다.'isEdit'의 상태 값이 true일 때 수정하기와 삭제하기 <button> 요소를 disabled로 변경한다.또한 submit 함수는 새로운 입력 값을 budgetList에 추가하지 않고 해당 예산의 index를 찾아 수정하고 리스트를 변경한다.이렇게 하니 onSubmit과 onEdit 처럼 비슷한 기능을 하는 함수를 여러 개 만들지 않아도 되었다. ⚠ setTimeout 렌더링const { type, message } = handleStatus; const handleStyle = useCallback(() => { if (type === "edit") { return "text-gray-500 block"; } else if (type === "none") { return "hidden"; } else { // 2초 뒤에 실행 --> App - Form - Status 1번 더 렌더링 setTimeout(() => { setHandleStatus({ type: "none", message: "" }); }, 2000); if (type === "submit") { return "text-green-400 block"; } else { return "text-red-400 block"; } } }, [type]); 추가, 삭제, 수정의 완료 및 진행 중 상태를 보여주는 <Status> 컴포넌트를 만들었다.App에서 만든 'handleStatus'라는 state를 전달하고 메세지가 나타난 뒤에 사라지게 만들고 싶어서 setTimeout() 메서드를 이용해 2초 뒤에 상태를 초기화했다.하지만 이 상태가 App과 Form 컴포넌트에서 참고하다 보니 나타나고 사라질 때마다 렌더링이 발생했다.CSS의 opacity로 처리하기엔 state의 값을 변경해야 했기에 알맞는 방법은 아니라 생각했다.뭔가 <Status> 컴포넌트 내부에서만 렌더링이 일어나게 하고 싶었는데 아직 다른 방법을 찾지 못했다.😢😢😢 회고다른 컴포넌트의 클릭 이벤트로 변경된 state를 이용하는 부분이 생각보다 오래 걸렸다.처음엔 콜백 함수처럼 App 컴포넌트에서 함수 만들고 prop으로 넘겨봤지만 List와 Form은 종속적인 관계가 아니라 힘들었다. 😢그래서 생각해낸 게 state를 이용해서 상태의 변경을 이벤트처럼 사용하는 것이었다.pub-sub 혹은 observer 패턴 같다는 생각도 했지만, 이렇게 최상위에서 선언한 state가 이곳저곳 돌아다니는 게 좋은 방법은 아닐 것 같다는 생각이 들었다.규모가 커지면 렌더링 관리도 힘들고 props를 쫓아다녀야 하기 때문이다.이래서 상태 관리 라이브러리가 나왔나 보다. 🤔 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션4-2 - GitHubFinder 앱

🔍 github-finder-app GitHub 🔍 github-finder-app 개요인프런 워밍업 클럽 FE 0기의 네 번째 미션인 'GitHubFinder 앱' 입니다.따라하며 배우는 자바스크립트 섹션 5(OOP), 섹션 6(비동기) 목표Fetch API 를 이용해 깃허브 유저 목록 불러오기Closure 를 이용해 Debounce Function 만들기 구현Fetch API 를 이용해 깃허브 유저 목록 불러오기async function loadUser(input) { prevInputValue = input; try { // const response = await fetch('./src/javascript/user.json'); const response = await fetch(`${url}/${input}`); if (!response.ok) { throw new Error('Failed to fetch user json'); } const json = await response.json(); setUserAvatar(json); setUserInfo(json); await loadUserRepos(json); } catch (error) { console.error(error); } }fetch() 메서드의 응답은 HTTP 응답 전체를 나타내는 'response' 객체를 반환한다.response의 ok 속성은 응답의 성공 여부를 불리언 값으로 가지고 있다.따라서 응답이 성공이 아닐 경우 오류 객체(new Error())를 반환하고 catch 문으로 Promise의 오류를 처리한다.응답에 성공한 response 객체를 JSON으로 사용하기 위해선 json() 메서드를 이용해 파싱해야 한다. Closure 를 이용해 Debounce Function 만들기// debounce debounceInput.addEventListener('input', debounce(loadUser, 1000)); // debounceInput.addEventListener('input', e => callback(e)); function debounce(callback, delay = 0) { // timer는 부모 함수에서 선언된 지역 변수 let timer = null; return (arg) => { // 여기서 arg는 input event if (timer) { // 이미 타이머가 있는데 또 실행되면 타이머 삭제 clearTimeout(timer); } // 변수 timer는 부모 함수에서 선언되었지만 내부 함수에서 사용(클로저) timer = setTimeout(() => { callback(arg.target.value); }, delay); }; }<input> 요소의 'input' 이벤트는 요소의 value가 변경될 때마다 발생한다.만약 사용자가 입력할 때마다 서버에 데이터를 요청한다면 서버의 부하가 커지기 때문에 좋은 방법은 아니다.이럴 때 사용자의 입력이 끝난 뒤 마지막 value를 이용해 서버로 요청하는 게 효율적인 방법이라 할 수 있다.함수의 실행 요청이 반복될 때 마지막 요청만으로 실행하는 걸 '디바운싱(debouncing)'이라고 부른다.debounce 함수는 인자로 실행할 함수를 받고 자식 함수를 반환한다.부모 함수인 debounce 함수에서 선언한 변수(timer)를 자식 함수에서 사용할 수 있는 클로저(Closure)를 이용해 자식 함수의 setTimeout() 메서드의 반환 값인 'timeoutID'를 할당한다.변수 'timer'에 할당한 timeoutID를 이용해 setTimeout() 메서드의 지연 시간(delay)이 종료되기 전에 요청이 들어왔다면 이전에 생성한 타이머를 clearTimeout() 메서드를 이용해 종료하고 다시 타이머를 할당한다. 이렇게 delay로 설정한 시간 이내에 사용자의 입력이 없을 경우 API 요청 함수를 실행하게 된다. 반복적인 함수의 실행을 다루는 방법으로 디바운싱(debouncing)와 쓰로틀링(throttling)이 있다.여러 변수를 고려해 'lodash' 라이브러리의 debounce를 많이 사용한다. 회고이번 미션은 debounce가 반환하는 자식 함수의 인자(argument)가 어떤 타입인지 알기 때문에 callback 함수에 전달하는 인자를 수정해서 미숙한 debounce 함수라고 볼 수 있다.늘 라이브러리를 통해 사용하던 함수를 만들려고 하니 모르는 것도 많고, 고려해야 할 부분이 많다는 걸 알게 됐다.자바스크립트의 기초를 잘 알아야 이런 라이브러리 메서드의 원리를 이해하기도 쉽고, 커스텀하기에 수월한 것 같다.(외의로 GitHub의 API 요청이 API key 없이도 되어서 신기했고, 그 덕에 조금은 수월했다. 아주 조금... 😵) DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션1 - 음식 메뉴 앱

🍝 food-recipe-app API from TheMealDBGitHub food-recipe-app 개요인프런 워밍업 클럽 FE 0기의 첫 번째 미션인 '음식 메뉴 앱' 만들기입니다.따라하며 배우는 자바스크립트의 섹션 1~3(자바스크립트 기초, Window 객체 및 DOM, Event)를 보고 자바스크립트의 DOM 요소를 조작하는 데 중점을 두었습니다.음식 데이터는 TheMealDB의 API를 이용했습니다. 사용한 API가 '음식 레시피'라서 이름을 변경했습니다. 목표문서 객체 모델(The Document Object Model, 이하 DOM)의 메소드(methods)를 이용해 요소(element)에 접근하고 생성하고 교체하기이벤트 리스너(Event Listener) 메소드를 이용해 요소에 이벤트를 등록하고 이벤트 객체 이용하기메뉴 데이터를 Fetch API를 사용해 불러오기 구현이벤트 위임(Event Delegation)을 이용한 이벤트 생성/* <nav id="food-navigation"> <div class="food-navigation-item"> <button id="Beef"> <figure> <img src="https://www.themealdb.com/images/category/beef.png"> <figcaption> Beef </figcaption> </figure> </button> </div> // ... </nav> */ // Not Event Delegation foodNavigation.querySelectorAll('button').forEach((button) => { button.addEventListener('click', async () => { const targetId = button.id; await setFoodList(targetId); }); }); // Event Delegation foodNavigation.addEventListener('click', async (event) => { const targetElement = event.target; // closest() 메서드는 주어진 CSS 선택자와 일치하는 요소를 찾을 때까지, // 자기 자신을 포함해 위쪽(부모 방향, 문서 루트까지)으로 문서 트리를 순회합니다. const targetDiv = targetElement.closest('.food-navigation-item'); if (!targetDiv) { return; } const targetButton = targetDiv.querySelector('button'); const targetId = targetButton.id; await setFoodList(targetId); });이벤트 위임이란 '상위 요소에서 하위 요소의 이벤트를 제어하는 것'을 의미합니다.이벤트를 위임하는 이유이벤트를 하나의 핸들러로 처리함으로써 메모리 사용량을 줄이고 성능을 향상시킬 수 있다.새로운 요소가 추가되거나 제거되는 경우 이벤트 리스너는 상위 요소에 연결되어 있어 재연결의 필요성이 줄어든다.저는 nav 태그에 이벤트를 등록하고 closest 메서드를 이용해 버튼의 id를 찾는 방법을 사용했습니다. 하위 요소를 제거하고 생성한 요소를 추가하기/* <div id="food-list"> <div class="food-list-item"> <figure> <img src="img src" /> </figure> <div class="food-list-item-desc"> <p>food name</p> <hr /> <div> food recipe </div> </div> </div> </div> */ const foodList = await getFoodList(strCategory); const foodListElement = document.getElementById('food-list'); const foodListItem = document.querySelectorAll('.food-list-item'); foodListItem.forEach((item) => item.remove()); // foodListElement.innerHTML = ''; foodList.map(async (food) => { // ... const foodElement = getFoodElement( idMeal, strMeal, strMealThumb, strInstructions ); foodListElement.appendChild(foodElement); });배웠던 removeChild()와 replaceChild() 메서드를 이용하고자 했으나...'만약 해당 카테고리의 음식 리스트의 개수가 다르다면 어떻게 하지?'라는 생각에 한번에 제거하기로 결정했습니다.처음엔 innerHTML을 이용해 하위 코드를 공백으로 만들었지만, 뭔가 이건 너무 이상하다는 생각(요소의 참조나 연결 같은 게 깨지진 않을까)이 들어 찾아보았습니다.stack overflow의 Remove child nodes (or elements) or set innerHTML=""?라는 글에서는 innerHTML은 하위 요소의 이벤트 핸들러가 완전히 제거되지 않을 수도 있다고 한다.또한 Why InnerHTML Is a Bad Idea and How to Avoid It?에서는 innerHTML이 보안상 좋지 않다는 점을 말하고 있다. Stack Overflow의 글을 자세히 읽어 보니 다음과 같은 글이 있었다.What is the best way to empty a node in JavaScript그리고 MDN 문서에도 이렇게 소개하고 있다.replaceChildren() provides a very convenient mechanism for emptying a node of all its children. You call it on the parent node without any argument specified:즉 replaceChildren()메서드를 빈 인자로 실행하면 하위 자식 노드를 모두 지워준다는 것...!😅 회고빈 폴더를 놓고 코드를 작성해본 게 너무 오랜만인 것 같다.자료를 찾기 귀찮다는 마음과 첫 미션이니까 API를 써볼까 하며 자만했던 순간도 있었다.미션의 목적보다 어느새 다른 부분을 신경 쓰느라 배보다 배꼽이 점점 커지는 것 같았다.딸랑 script 태그 한 줄 작성하고 js 파일을 제대로 못 불러와서 몇 시간을 해결 방법을 찾아서 해매기도 했다.😭이벤트 위임 코드를 작성할 때 이은재 님의 시나브로 자바스크립트에서 배웠던 부분을 참고했다.음식 레시피를 불러올 때 요소를 지우고 불러와서 그런지 해당 부분이 사라지고 나타나서 페이지가 늘었다 줄었다 하는 게 눈에 띈다.이래서 가상 돔을 쓰는걸까? 아니면 태그의 속성을 하나하나 수정하면 되는걸까?일단 진도를 따라잡고 배워서 발전시켜야겠다. DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽FE프론트프론트엔드과제미션발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션6 - 타이핑 테스트 앱

⌨ Typing Test APP GitHub ⌨ Typing Test APP DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 여섯 번째 미션인 '타이핑 테스트 앱' 입니다. 따라하며 배우는 자바스크립트 섹션 9(오브젝트 만들기)  목표setInterval() 메서드를 이용해 타이머 만들기split() 메서드를 이용해 문자열 관리(는 위험하다) 구현setInterval() 메서드를 이용해 타이머 만들기textarea.addEventListener('focus', gameStart); let timer = null; function gameStart() { // 만약 마우스를 다른 곳에 뒀다가 다시 focus 할 때 막기 if (timer) { return; } makeExample(); timer = setInterval(() => { if (remainTime > 0) { remainTime--; passedTime++; timeCount.textContent = remainTime; } if (remainTime <= 0) { finishGame(); } }, 1000); } function finishGame() { clearInterval(timer); // ... }'timer'라는 변수를 gameStart() 함수 외부에 생성하고(다른 함수에서도 사용하기 위해) 함수 내부에서 setInterval() 메서드를 할당한다.setInterval(func, delay) 메서드는 실행할 함수(func)와 지연 시간(delay)을 인자로 받는다.setInterval() 메서드는 setTimeout() 메서드의 반환 값처럼 'intervalID'을 반환하고, 이를 이용해 생성한 interval을 취소할 수 있다.1초마다 화면에 보여주는 시간 값을 줄여서 보여주고, 남은 시간(remainTime)이 0이 되면 finishGame() 함수에서 clearInterval(intervalID) 메서드를 호출한다.예제는 textarea에 focus 이벤트가 발생하면 gameStart() 함수를 호출하고 있어서 이미 타이머가 생성된 이후라면 하위 코드가 실행되지 않도록 조건을 만들었다. split() 메서드를 이용해 문자열 관리(는 위험하다)(정리글을 쓰면서 알아보다가 발견...)function makeExample() { exampleContainer.replaceChildren(); // String.split('') better than [...String] or Array.from(String) // better than grapheme-splitter library Array.from(examples[exampleIndex]).forEach((char) => { const span = document.createElement('span'); span.textContent = char; exampleContainer.appendChild(span); }); exampleIndex++; if (exampleIndex >= examples.length) { exampleIndex = 0; } }split(separator, limit) 메서드는 구분자(separator)와 문자열 최대 개수(limit)을 인자로 받는다.구분자는 문자열에서 일치하는 부분을 기준으로 나누는데, 빈 문자열('')을 할당하면 공백이므로 각 문자로 나눈다.처음엔 문자열의 각각 문자를 배열의 요소처럼 다루기 위해서 split() 메서드를 이용했다.⚠ split('')은 문자 단위로 나누지 않는다!하지만 이 split() 메서드에서 공백을 구분자로 하는 게 큰 위험이 있다는 걸 MDN 문서를 통해 확인했다.console.log('𝟘𝟙𝟚𝟛'.split('')); // ["�","�","�","�","�","�","�","�"] console.log('😎😜🙃'.split('')); // ["�", "�", "�", "�", "�", "�"] console.log('अनुच्छेद'.split('')); // ["अ", "न", "ु", "च", "्", "छ", "े", "द"] console.log('Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘!͖̬̰̙̗̿̋ͥͥ̂ͣ̐́́͜͞'.split('')); // ["Z","͑", "ͫ", "̓", ... ]이런 현상이 발생하는 이유는 split() 메서드의 구분자가 빈 문자열인 경우, 전체 문자열을 UTF-16으로 인코딩하기 때문이라고 한다.위와 같은 특수문자는 UTF-16로 표현할 수 없어서 2바이트 인코딩의 값을 쌍으로 묶어 표현했고, 이를 '써로게이트 페어'라고 부른다.즉 split('')은 이 써로게이트 페어가 망가지면서 원하는 결과를 얻지 못하게 된다.이런 다양한 특수문자와 흰두어 같은 다른 나라의 언어를 제대로 나누기 위한 'grapheme-splitter' 라이브러리도 있다.소스를 보면 생각보다 고려해야 하는 게 많아서 엄청난 코드 양을 볼 수 있다.예제는 비교적 간단하고 미리 정해진 문자열을 다루고 있기에 Array.from() 메서드로 변경했다. 회고사용자가 입력한 문자와 문제의 문자를 비교해서 CSS와 스크립트를 작성하는 게 중요한 과제였다.처음엔 늘 하던대로 String.split('')으로 문자열을 나누고 관리했지만 글을 작성하기 위해 알아보다가 이 방식이 문제가 있다는 걸 알게 됐다.MDN 문서를 보면서 문서화가 정말 중요하다는 걸 느끼기도 했고, Stack Overflow 같은 커뮤니티에 문제를 공유하고 함께 고민하는 게 개발 문화의 큰 힘이자 감사한 일이라 생각했다. (감사... 또 감사...)이런 다양한 경우의 수 때문에 테스트 코드를 중요하게 여기고 작성하는 게 아닐까!?훌륭한 개발자의 머리 속에는 if가 가득할 것 같다. 🙄 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션5 - 비밀번호 생성 앱

🔐 Password Genrator GitHub 🔐 Password Genrator DemoRecord by ScreenToGif  개요인프런 워밍업 클럽 FE 0기의 다섯 번째 미션인 '비밀번호 생성 앱' 입니다.따라하며 배우는 자바스크립트 섹션 7~8(Iterator, Generator, Design Pattern) 목표Array.from, fromCharCode() 메서드를 이용해 숫자, 소문자, 대문자 배열 생성동적 변수를 이용해 DOM 요소 조작 및 비밀번호 생성하기 구현Array.from, fromCharCode() 메서드를 이용해 숫자, 소문자, 대문자 배열 생성// index를 이용한 0~9 배열 [0, 1, 2, ...] const numbersArray = Array.from({ length: 10 }, (_, index) => index); // 유니코드를 이용한 소문자 배열 [a, b, c, ...] const smallLettersArray = Array.from({ length: 26 }, (_, index) => String.fromCharCode(97 + index) ); // 유니코드를 이용한 대문자 배열 [A, B, C, ...] const capitalLettersArray = Array.from({ length: 26 }, (_, index) => String.fromCharCode(65 + index) ); const symbolsArray = ['@', '!', '#', '$', '%'];Array.from(arrayLike, mapFn, thisArg) 메서드는 세 개의 인자를 받는다.첫 번째 인자는 순회가 가능한 유사 배열 객체(arrayLike), 두 번째 인자는 배열의 각 요소에 호출할 함수(mapFn), 세 번째 인자로 mapFn 함수 실행 시에 사용할 this(thisArg) 값을 받는다.여기서 유사 배열 객체의 속성인 'length'를 이용해 원하는 길이를 지정하고 두 번째 함수에서 인덱스를 이용해 각 요소의 값을 인덱스로 지정하면 0부터 length-1의 값을 갖는 배열을 만들 수 있다.String.fromCharCode(num1[, ...[, numN]]) 메서드는 UTF-16 코드 유닛의 시퀀스로부터 문자열을 생성해 반환한다.알파벳은 총 25개이며, 알파벳 소문자의 유니코드는 97부터 122까지, 대문자는 65부터 90까지이므로 길이와 index를 각각 알맞게 설정해주면 된다. 동적 변수를 이용해 DOM 요소 조작 및 비밀번호 생성하기function isChecked() { const checkboxes = form.querySelectorAll('input[type="checkbox"]'); let anyCheck = false; checkboxes.forEach((checkbox) => { if (checkbox.checked) { anyCheck = true; } }); return anyCheck; }<form> 요소 하위의 'checkbox' 타입인 <input> 요소를 모두 선택하고, 'checked' 속성을 확인해 하나라도 true라면 'anyCheck'라는 동적 변수에 true를 할당한다.form.addEventListener('click', (e) => { isChecked() ? passwordLength.removeAttribute('disabled') : passwordLength.setAttribute('disabled', ''); });이렇게 isChecked() 함수를 실행해서 체크 여부를 확인하고 입력 <input> 요소의 속성을 변경한다.const resultArray = []; let password = ''; let requiredNumbers = false; // ... if (checkNumbers.checked) { resultArray.push(...numbersArray); requiredNumbers = true; } // ...생성할 비밀번호에서 반드시 포함해야 하는 속성을 확인하는 required 변수를 만들고 'input[type="checkbox"]' 요소의 각 checked 값을 확인해 true로 변경한다.do { for (let i = 0; i < passwordLength.value; i++) { const randomIndex = Math.floor(Math.random() * resultArray.length); password += resultArray[randomIndex]; } } while ( (requiredNumbers && !password.split('').some((char) => numbersArray.includes(Number(char)))) || (requiredSmallLetters && !password.split('').some((char) => smallLettersArray.includes(char))) || (requiredCapitalLetters && !password.split('').some((char) => capitalLettersArray.includes(char))) || (requiredSymbols && !password.split('').some((char) => symbolsArray.includes(char))) // 정규 표현식으로 검사 // (requiredNumbers && !/[0-9]/.test(password)) || // (requiredSmallLetters && !/[a-z]/.test(password)) || // (requiredCapitalLetters && !/[A-Z]/.test(password)) || // (requiredSymbols && !/[!@#$%]/.test(password)) );for문을 이용해 사용자가 입력한 비밀번호의 자릿수(passwordLength.value)만큼 비밀번호를 생성한다.생성한 비밀번호를 split() 메서드를 이용해 문자마다 나눈 뒤 배열로 만들고, 문자를 하나하나 기존에 생성한 배열의 요소와 비교한다.만약 생성한 비밀번호에 반드시 포함해야 하는 속성이 없다면(required가 true인데 다음 조건식을 만족하지 않는 경우) 다시 생성한다.이를 좀 더 쉽게 검사하기 위한 정규 표현식도 있지만, 모른다는 가정 하에(잘 모르기도 했지만 😂) 구현을 해보려 했다. 회고다양한 플랫폼에서 회원가입과 로그인은 필수적인 사항일 텐데, 여기서 사용하는 검사식은 플랫폼만큼이나 다양하다고 들었다.왜냐하면 이 부분은 보안과 연결되어 있어서 꽤 민감하기 때문이다.코드를 작성하다 보니 비밀번호를 무작위로 생성하는 것보다 조건을 만들고 통과시키는 게 더 어려웠다.특히 input의 checked 여부에 따라 결과에 반드시 포함시켜야 한다고 작성하는 부분이 꽤 오래 걸렸다.처음엔 전체가 아니라 하나하나 만들 때마다 검사했지만, while의 조건에서 OR( || ) 연산자로 검사하다 보니 무한 루프에 빠져버렸다.결국 전체를 비교하고 다시 만드는 코드로 변경했지만 뭔가 좋은 방법이 아닌 것 같아서 찜찜하다. 🤔물론 보통은 생성이 아니라 사용자가 입력한 값을 비교하겠지만...여러 변수와 다양한 조건을 고려해서 효율적인 코드를 작성할 수 있는 개발자가 되고 싶다! 

프론트엔드워밍업워밍업클럽프론트엔드프론트FE미션과제발자국

이양구

[인프런 워밍업 클럽 FE 0기] 미션4-1 - 책 리스트 앱

📚 book-list-app GitHub 📚 book-list-app 개요인프런 워밍업 클럽 FE 0기의 네 번째 미션인 '책 리스트 앱' 입니다.따라하며 배우는 자바스크립트 섹션 5(OOP), 섹션 6(비동기) 목표class 를 이용해 책 리스트 객체 활용하기setTimeout 을 이용해 DOM 요소 관리하기배열 메서드(some(), splice())를 이용해 조건에 맞는 코드 실행 및 배열 관리 구현class 를 이용해 책 리스트 객체 활용하기class Book { constructor(name, author) { this.name = name; this.author = author; } } submitBtn.addEventListener('click', () => { const bookName = document.getElementById('book-name').value; const bookAuthor = document.getElementById('book-author').value; const book = new Book(bookName, bookAuthor); addBookToList(book); }); 'Book'이라는 클래스(class)는 'name'과 'author'이라는 속성을 갖는 객체를 생성합니다.책의 이름과 저자는 버튼의 클릭 이벤트로 input 요소의 value를 가져옵니다. setTimeout 을 이용해 DOM 요소 관리하기function addSubmitInfo(state, message) { const submitInfo = document.createElement('p'); submitInfo.classList.add('info'); submitInfo.textContent = message; switch (state) { case 'success': submitInfo.classList.add('success'); break; case 'error': submitInfo.classList.add('error'); break; } requestAnimationFrame(() => { submitInfo.style.opacity = '1'; }); infoWrap.appendChild(submitInfo); setTimeout(() => { requestAnimationFrame(() => { submitInfo.style.opacity = '0'; }); setTimeout(() => { submitInfo.remove(); }, 500); }, 1000); }버튼의 이벤트의 성공 유무에 따라 사용자에게 보여줄 정보를 담은 p 태그를 생성하는 코드입니다.성공 혹은 실패에 대한 상태(state)를 받고 보여줄 메시지를 받습니다.requestAnimationFrame() 메서드는 브라우저에 애니메이션을 업데이트할 때 사용하는 메서드입니다. 생성한 요소가 등장하고 사라질 때 서서히 나타나는 효과를 주기 위해 사용했습니다.setTimeout(callback, delay)은 첫 번째 인자로 콜백 함수를 받고, 두 번째로 밀리초 시간 단위를 받아서 해당 시간이 경과하면 콜백 함수를 실행하는 메서드 입니다.먼저 1초 뒤에 requestAnimationFrame() 메서드를 실행하고, 0.5초 뒤에 요소를 지우게 됩니다. 배열 메서드(some(), splice())를 이용해 조건에 맞는 코드 실행 및 배열 관리const bookList = []; function addBookToList(book) { if ( !bookList.some( (bookOfBookList) => bookOfBookList.name === book.name && bookOfBookList.author === book.author ) ) { bookList.push(book); // ... bookDeleteBtn.addEventListener('click', () => { addSubmitInfo('success', '✅ Book deleted to list successfully'); bookList.splice(bookList.indexOf(book), 1); bookTr.remove(); }); // ... addSubmitInfo('success', '✅ Book added to list successfully'); } else { addSubmitInfo('error', '❗ Book already in list'); } }some(callback) 메서드는 배열의 요소가 인자로 받은 함수를 통과하는지 확인하고 불리언 값을 반환합니다.만약 'bookList' 배열에 이미 똑같은 이름과 저자의 책을 등록하는 경우 else 문을 실행하게 됩니다.splice(start, deleteCount, item) 메서드는 변경할 인덱스(start), 제거할 요소의 수(deleteCount), 추가할 요소(item)를 인자로 받습니다. 만약 item이 없다면 start index부터 deleteCount의 수만큼 제거합니다.indexOf(searchElement, fromIndex) 메서드는 찾을 요소(searchElement)와 검색 시작 인덱스(fromIndex)를 인자로 받습니다. 만약 fromIndex가 없다면 0부터 시작합니다. 또한 찾을 수 없는 경우 -1을 반환합니다.즉, bookList에 indexOf를 이용해 해당 book의 index를 찾아서 splice로 제거하는 방식입니다. 객체를 indexOf로 비교하면 객체의 속성 값이 동일하더라도 참조가 다르다면 음수를 반환하는 경우도 있기에 정확한 속성 값을 비교하는 게 안전하다.또한 배열의 데이터가 객체인 경우는 findIndex()를 이용하는 게 좋다고 한다.findIndex()는 주어진 판별 함수를 이용해 비교하기 때문!bookDeleteBtn.addEventListener('click', () => { addSubmitInfo('success', '✅ Book deleted to list successfully'); // bookList.splice(bookList.indexOf(book), 1); const index = bookList.findIndex( (item) => item.name === book.name && item.author === book.author ); if (index !== -1) { bookList.splice(index, 1); } bookTr.remove(); console.log(bookList); }); 회고원래는 class로 생성한 Book 객체에 delete 이벤트도 함께 넣으려고 했었다.그런데 생성한 tr 요소와 bookList라는 array도 참고해서 지워야 했기 때문에 코드가 복잡해져 사용하진 못했다.(애초에 설계를 잘못하지 않았나...😂)아마 좀 더 복잡한 모듈을 만든다면 다르게 작성하지 않았을까...!물론 설계하는 데 충분한 시간을 두고 해야겠지만 🙄 DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽프론트엔드프론트미션과제발자국FE

이양구

[인프런 워밍업 클럽 FE 0기] 미션3 - 퀴즈 앱

🤔 quiz-app GitHub 🤔 quiz-app 개요인프런 워밍업 클럽 FE 0기의 세 번째 미션인 '퀴즈 앱' 만들기입니다.따라하며 배우는 자바스크립트 섹션 4(9~17) 목표Math.random() 메소드를 이용해 무작위 문제와 보기 만들기do while 문으로 중복된 데이터를 처리하고 오답 생성배열 메소드(includes(), sort())를 이용해 보기 관리  구현Math.random() 메소드를 이용해 무작위 문제와 보기 만들기const operatorArray = ['+', '-', '*']; function makeQuiz() { btnWrap.replaceChildren(); nextBtnWrap.replaceChildren(); emoji.innerText = '🤔'; const number1 = Math.floor(Math.random() * 10); const number2 = Math.floor(Math.random() * 10); const operator = operatorArray[Math.floor(Math.random() * operatorArray.length)]; quiz.innerText = `${number1} ${operator} ${number2}`; // 정답 저장 const correct = calculator(number1, number2, operator); // 정답 + 오답 3개 const optionArray = makeOptions(correct); for (let i = 0; i < 4; i++) { const btn = document.createElement('button'); btn.innerText = optionArray[i]; btnWrap.appendChild(btn); } } function calculator(num1, num2, operator) { switch (operator) { case '+': return num1 + num2; case '-': return num1 - num2; case '*': return num1 * num2; } }Math.random() 메소드는 0과 1 사이에서 무작위 난수를 생성합니다.정확한 계산을 위해 Math.floor() 메소드로 정수 이하의 소수점을 버립니다.무작위로 생성한 난수에 배열의 길이를 곱해서 무작위 인덱스(index)를 생성한 뒤에, 연산자(operater)에 따라 switch문을 이용해 정답을 반환합니다.또한 오답도 만들어야 하기에 범위가 정답에서 -10 ~ +10의 무작위 값을 생성하고 배열에 담습니다. do while 문으로 중복된 데이터를 처리하고 오답 생성 && 배열 메소드(includes(), sort())를 이용해 보기 관리function makeOptions(correct) { const optionArray = []; const range = 10; // 정답 범위 for (let i = 0; i < 3; i++) { let wrongAnswer; do { // 정답 주변의 랜덤한 오답 생성 wrongAnswer = correct + (Math.random() > 0.5 ? 1 : -1) * Math.floor(Math.random() * range); } while (optionArray.includes(wrongAnswer) || wrongAnswer === correct); // 이미 포함된 오답이거나 정답과 같은 경우 다시 생성 optionArray.push(wrongAnswer); } optionArray.push(correct); // 보기 섞기 optionArray.sort(() => Math.random() - 0.5); return optionArray; }do while문은 while의 특정 조건을 만족한다면 다시 do 구문을 실행하는 루프를 뜻합니다.includes() 메소드는 해당 배열에 일치하는 값이 있는지 검사하고, 있다면 true를 없다면 false를 반환합니다.따라서 생성한 wrongAnswer라는 값이 해당 배열에 존재하거나, 정답과 중복된 경우 다시 do 구문을 실행해 새로운 값을 생성합니다.3개의 오답을 모두 생성했다면 마지막에 정답을 추가합니다.이렇게 된다면 항상 마지막에 정답이 위치하므로 sort() 메소드를 이용해 배열을 섞습니다.sort() 메소드는 매개변수로 정렬 순서를 정의하는 함수를 받습니다.만약 해당 매개변수가 제공되지 않으면 요소를 문자열로 변환하고 유니 코드 코드 포인트 순서로 문자열을 비교하여 정렬됩니다.저는 Math.random()을 이용해 음수를 반환하면 false, 양수를 반환하면 true가 되도록 해서 배열의 순서를 변경했습니다. 회고강의에서 배웠던 커링(Curry Function)과 IIFE(Immediately Invoked Function Expression)를 사용하고자 했지만...평소에 보던 코드에서 마땅한 예시가 없던 탓인지 바로 적용할 좋은 예시가 생각나진 않았다. 😥IIFE는 라이브러리나 오픈 소스에서 사용하는데, 변수의 전역 선언을 피하고 다른 라이브러리나 오픈 소스와의 충돌을 피하기 위해서 라고 한다. (IIFE 내부 안으로 다른 변수가 접근할 수 없고, 전역 스코프에 불필요한 변수가 추가되지 않기 때문에 그렇다!)main.js 파일의 시작을 IIFE로 변경하긴 했지만... 나중에 제대로 된 예시를 참고해보거나, 라이브러리 제작 혹은 오픈 소스 제작 및 기여를 하는 수준까지 갔을 때 잊지 말고 참고해봐야겠다!Math.random()으로 여러 무작위 숫자를 생성해서 문제를 만들면서 생각보다 숫자와 문자열 타입이 난무하고 있다는 걸 많이 깨달을 수 있었다.내가 쓰는 메소드의 반환(return) 타입을 잘 이해하고 있어야겠다는 생각과 함수를 만들 때도 return 값을 잘 의도해서 만들어야겠다는 생각이 함께 들었다. 🙂 DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽프론트프론트엔드미션과제발자국FE

이양구

[인프런 워밍업 클럽 FE 0기] 미션2 - 가위 바위 보 앱

👊Rock-🖐Paper-✌Scissors-app GitHub 👊Rock-🖐Paper-✌Scissors-app 개요인프런 워밍업 클럽 FE 0기의 두 번째 미션인 '가위 바위 보 앱' 만들기입니다.따라하며 배우는 자바스크립트 섹션 4(1~8) 목표생성자 함수(Function() constructor) 를 이용해 객체를 생성하기this 를 이용해 객체의 프로퍼티(Property)와 메소드(Method) 관리하기 구현생성자 함수(Function() constructor)를 이용한 객체 생성function Player() { this.win = 0; this.weapon = ''; } function Game() { this.count = 0; this.play = function (player, robot) { // ...play function this.count++; if (this.count === TOTAL_CHANCE) { // ... } } this.reset = function (player, robot) { this.count = 0; player.win = 0; robot.win = 0; player.weapon = ''; robot.weapon = ''; // ... } } const game = new Game(); const player = new Player(); const robot = new Player();생성자 함수는 객체를 만드는 함수이며 두 가지 규칙이 있다.함수명은 '대문자'로 시작한다.'new' 연산자를 붙여서 실행해야 한다.new 연산자와 함께 생성자 함수를 호출하게 되면 다음과 같은 과정을 통해 this를 만들고 반환한다.this = {} this를 빈 객체로 생성한다.함수 내부의 프로퍼티와 메소드를 this에 바인딩한다.return this this를 반환한다.저는 'Player'라는 생성자 함수를 만들고 이기면 1씩 증가하는 'win' 프로퍼티와 가위, 바위, 보 중에 하나가 들어갈 'weapon'이라는 프로퍼티를 만들었습니다.그리고 게임 진행 상태를 기록할 'Game'이라는 생성자 함수도 만들었습니다.'count'는 몇 회의 게임이 진행됐는지 기록하는 프로퍼티이며, 'play'와 'reset'은 게임을 실행하고 초기화하는 메소드입니다. 버튼 클릭 이벤트에 생성한 객체의 메소드 실행하기const WEAPON_LIST = { rock: '👊', paper: '🖐', scissors: '✌', }; btnWarp.addEventListener('click', function (e) { const target = e.target; if (target.classList.contains('btn')) { if (target.id === 'retry') { return game.reset(player, robot); } player.weapon = target.textContent; // Object.values() --> object의 value를 array로 반환해준다. robot.weapon = Object.values(WEAPON_LIST)[Math.floor(Math.random() * 3)]; playerWeaponElement.innerText = player.weapon; robotWeaponElement.innerText = robot.weapon; game.play(player, robot); } });버튼 요소를 감싸고 있는 부모 요소인 btnWarp에 이벤트를 등록했습니다.game 객체의 play 메소드를 실행하기 전에 robot의 weapon에 Math.random() 메소드를 이용해 무작위의 weapon을 할당합니다.재시작 버튼 이벤트를 함께 처리하기 위해서 game의 play 함수에서 this.count를 이용해 게임이 마지막까지 진행됐다면 id가 'retry'인 버튼을 추가합니다. 회고뭔가 'player'라고 부르니 게임 캐릭터를 만드는 느낌이 들어서 신기하면서도 게임 개발자는 어떻게 캐릭터를 구성하는지 궁금해졌다.프로퍼티나 메소드를 객체에 바인딩해서 반환하는 게 class 문법과 닮아있다고 생각했다.Class and Object Constructor Function in JavaScript궁금해서 찾아보니 class가 좀 더 편리한 문법을 제공하며, 상속이 큰 차이점이라고 한다. (아직은 잘 모르겠다)생성자 함수나 class를 제대로 이해하고 사용한 적이 없어서 그런지 감이 잘 안 온다.😢라이브러리와 오픈 소스의 코드를 보고 class로 도배가 되어 있어 놀랐던 기억이 새록새록 난다.얼른 배우고 싶다. 습관적으로 window.onload나 body 태그에 onload를 사용했었는데, 다른 스터디 러너 분께서 'DOMContentLoaded' 이벤트를 쓰시는걸 보고 슬쩍 배워보았다.- DOMContentLoaded : 이벤트는 HTML의 파싱이 완료되면 발생하며, 스타일시트, 이미지 및 하위 프레임 로드를 기다리지 않습니다.- onload : 속성은 페이지의 모든 자원(이미지, 스크립트, 스타일시트 등)이 다운로드되고 웹 페이지가 완전히 로드되었을 때 발생합니다. DemoRecord by ScreenToGif

프론트엔드워밍업워밍업클럽발자국thisconstructor프론트프론트엔드미션과제FE

joyjoy312

[인프런 워밍업클럽 0기 BE] 첫 번째 발자국 (1주차 회고)

✔1주차 커리큘럼 ✔1주차 학습 요약1일차스프링 프로젝트를 시작하는 방법과 서버를 만들기 전 네트워크 및 관련된 지식과, API의 진입 지점인 Controller를 통해 GET API를 개발하는 것을 학습하였다.1일차 미션 - Annotation 개념과 특징 / Custom Annotation2일차GET API에 이어 POST API를 학습하고 여러가지 API를 개발해보았다. POST API의 경 쿼리가 아닌 바디(body)를 사용하므로 POSTMAN에서 JSON언어로 데이터를 요청하고 응답 받는 것에 대해서도 학습하였다.2일차 미션 - API 개발하기13일차Database의 필요성에 대해 학습하고 MySQL에서 데이터베이스를 만들어 여러가지 쿼리를 작성해보았다. 메모리에 저장하여 서버가 내려갈 시 데이터가 사라지는 문제점을 JdbcTemplate을 이용하여 데이터를 데이터데이스에 저장하는 방법에 대해 학습하였다.3일차 미션 - 익명클래스와 람다식4일차데이터베이스 테이블에 저장된 데이터를 수정, 삭제하는 PUT API, DELETE API를 개발하는 것에 관해서 학습하였다. 또한 삭제 요청을 할 시 존재하는 유저가 없을 경우 응답으로 200OK가 아닌 500 ERROR 가 발생하도록 에러 처리를 해주었다. 4일차 미션 - API 개발하기2 / 에러 처리 해보기5일차클린코드의 개념과 필요성에 대해 학습하고, 또 기존에 Controller에서 모든 역할을 수행했던 것을 Service, Repository를 생성하여 이를 3개의 역할로 분리하는 과정에 대하여 배우고 기존에 개발한 코드에 대해 리팩토링을 진행하였다.5일차 미션 - Clean Code(클린코드)만들기 (리팩토링)   ✔회고강의 소감🧩컴퓨터를 전공하였지만 대학 때는 이론에 비중이 큰 학습을 하였고 스프링은 아예 배운 적이 없고 git, aws 등 실무에 필요한 기술들은 전혀 배울 수 없었다. 백엔드 개발자가 되기로 하고 필요한 기술을 배우기 위해 부트캠프를 수강하였는데 대학 때 안 배운 기술들이 대부분이었다.부트캠프를 수강하며 그리고 인강을 통해 접하였던 서버 지식 및 이론 지식들과 Spring에 관한 내용들이 이번 스터디를 통해 퍼즐이 맞추어지는 느낌이다. 전에는 알지만 제대로 아는 것이 아니었다면, 강사님의 친절한 설명 덕에 이해가 잘 되고 기존에 알고 있던 지식이 정리가 되어 합쳐지는 느낌이었다.. (강사님 감사합니다)한 단계씩 올라갈 때마다 이해하기 쉽게 설명이나 예제 등을 준비해주신게 느껴지고, 호흡이 빠르지 않은 거 같은데 필요한 내용은 다 들어있는 느낌이다. 강의 끝날 때마다도 수강생들을 존중한다고 늘 말씀해주신다 멋지심👍 강의와 미션강의의 분량도 부담되는 수준이 아니고, 오히려 아주 많지 않기 때문에 쫓기듯 하지 않아도 되서 강의와 미션을 부담없이 매일 할 수 있었던 것 같다. 생각보다 분량이 금방 끝날 듯 싶지만 막상 해보면 미션을 하면서 나의 부족한 점이 계속 보이고 생각지 못한 변수가 생기기 때문에 초기에 생각한 시간보다 더 걸리곤 했다.개발을 할 때는 늘 넉넉한 시간을 두고 하는 것이 좋은 것 같다. 그래야 다시 내 코드가 괜찮은지 돌아보게 되고 리팩토링을 거칠 수 있고, 또 모르는 이론적인 내용도 찾아볼 여유가 생긴다. 학습에 대해배우는 큰 줄기에 대해서 잘 학습하고, 직접 코딩을 해보며 많은 연습을 해야겠다고 느꼈다. 수학 문제도 여러 번 풀어야 내 것이 되어 시험에서 문제를 풀어나갈 수 있는 것처럼, 코딩도 같은 내용에 대한 것도 여러 번 연습하여야 내 것이 되는 것 같다. 

웹 개발미션발자국백엔드