Develop/Web

Thrift 뽀개기 | Cracking Thrift

보통 서버 통신을 할 때 http 를 떠올리기 쉽다. 좀 더 가볍고, 다른 언어간의 호환이 편했으면 하는 이슈와, 당시 facebook에서 만들어서 인기가 많았다는 이유로 thrift를 서버간의 통신에서 사용하기도한다. 1. Thrift 정의하는 방법https://thrift.apache.org/ Apache Thrift - HomeThe Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++,..

Thrift 뽀개기 | Cracking Thrift

728x90

보통 서버 통신을 할 때 http 를 떠올리기 쉽다.
좀 더 가볍고, 다른 언어간의 호환이 편했으면 하는 이슈와, 당시 facebook에서 만들어서 인기가 많았다는 이유로 
thrift를 서버간의 통신에서 사용하기도한다.

 

1. Thrift 정의하는 방법

https://thrift.apache.org/

 

Apache Thrift - Home

The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haske

thrift.apache.org

먼저 thrift사용을 위해 thrift를 설치한다

$ brew update
$ brew install thrift

thrift 파일을 작성한다 (IDL). 해당 파일에 작성된 대로 해당 서버에서 thrift로 통신할 수 있는 요청 응답 포맷이 정의가 되게 된다.
IDL 작성법과 예시는 공식문서에 나와있는 example이 가장 좋은 것 같다.

https://thrift.apache.org/docs/types

 

Apache Thrift - Thrift Type system

Thrift Types The Thrift type system is intended to allow programmers to use native types as much as possible, no matter what programming language they are working in. This information is based on, and supersedes, the information in the Thrift Whitepaper. T

thrift.apache.org

 

기본 타입리스트

  • 기본타입 : bool, byte, i16, i32, i64, double, string (숫자는 전부 signed 타입이다.)
  • 특별 타입 : binary - 인코딩되지 않은 바이트 시퀀스
  • struct : oop 언어의 클래스와 동일하나 상속은 없다.
  • 컨테이너 : list, set, map<type1, type2>

위에서 말한 타입을 기반으로 https://thrift.apache.org/docs/idl 을 추가로 정의할 수 있다.

서비스

위에서 정의한 type들을 사용하여 정의된다. 인터페이스를 정의하는 것과 유사하고, 이렇게 정의된 service를 thrift 컴파일러로 컴파일을 하게되면, thrift가 정의한 서비스대로 요청 응답을 할 수 있는 클라이언트 코드 및 서버 코드를 작성해준다.

네임스페이스

작성한 thrift IDL 을 컴파일 했을 때 어떤언어로 내보낼지 설정한다. 문서를 보면 내보내기 가능한 언어 리스트가 적혀져있다. 다양한 언어로 thrift가 생성이 가능하기 때문에, 서로 다른 언어로 짜여진 서버간의 통신에 사용할 때 이점이 있다고 보는 것 같다.

 

사용법 예시를 위해 thrift DML을 아래와 같이 작성을 해보았다.

namespace java com.jyami.java.thrift.service
namespace py com.jyami.python.thrift.service

struct CommonResponse {
  1:required i32 status,
  2:optional binary bsonBody
}

struct RequestWithFlag {
  1:required binary bsonData,
  2:required bool flag
}

service PostService {

  void ping()

  CommonResponse read(1:RequestWithFlag request),

  CommonResponse save(1:binary bsonData),

  CommonResponse remove(1:binary bsonData)
}

 

2. thrift 컴파일로 코드 생성

thrift --gen <language> <Thrift filename>

 

위 명령어를 java로 실행했을 때 ( thrift --gen java app.thrift ) 위와 같은 java 파일이 자동생성되었다.

namespace에 python 도 짜두었으니, python 도 마찬가지로 생성이 가능하다.

 

3. Client, Server 코드 생성

https://thrift.apache.org/tutorial/java.html

 

Apache Thrift - Java

Java Tutorial Introduction All Apache Thrift tutorials require that you have: The Apache Thrift Compiler and Libraries, see Download and Building from Source for more details. Generated the tutorial.thrift and shared.thrift files: thrift -r --gen java tuto

thrift.apache.org

사실 공식문서에 너무 잘되어있다. java로 Client, Server 전부 작성해보았다. 
결국은 위에서 thrift gen으로 생성한 java 파일안의 클래스와 메서드들을 사용하는게 포인트이며,
Thrift 통신을 하기위한 기본적인 구현체인 TSocket, Ttransport 등등은 아래와 같이 thrift 라이브러리를 사용하면 받을 수 있다.

implementation("org.apache.thrift:libthrift:0.18.0")

3-0. 세팅

편의를 위해 client와 server를 한 디렉토리에 두었다.

하지만 핵심은 thrift 폴더 아래에 있는 app.thrift에서 생성된 java 코드 안에 있는 각종 클래스나 메서드와 같은 구현체를

비즈니스 로직이 있는 실제 클라이언트 혹은 서버의 main 코드에서 직접 사용하여 thrift로 통신을 할 수 있다는 것이다.

intellij gradle project 기준에서, 위와같이 main폴더와 thrift폴더 사이의 코드들이 서로 import 해서 사용하고, 이를 개발환경에서도 intellij 상에서 indexing을 진행한 후 사용하기 위해, 여러가지 세팅을 해주었다.

## build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.7.10"
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
    implementation("org.apache.thrift:libthrift:0.18.0")
    implementation("javax.annotation:javax.annotation-api:1.3.2")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1")
    implementation("de.undercouch:bson4jackson:2.13.1")
    implementation("org.slf4j:slf4j-api:1.7.25")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}


sourceSets {
    this.getByName("main"){
        this.java.srcDir("src/thrift/gen-java")
    }
}

 

intellij 로 실행할 때 thrift 폴더도 sourceDirectory로 인지하고 해당 폴더 안에 있는 자바 코드를 사용하기 위해 build.gradle에 sourceSets 를 설정해주었다. 또한 intellij 상에서 해당 폴더를 실제 sources로 인식할 수 있게 project structure(command + ;)에서 modules 안에 source로 잘 들어갔는지도 확인해준다.

 

3-1. Client 코드 작성

thrift gen으로 생성된 파일들의 실제 내용을 보면 알겠지만 RequestWithFlag, CommonRespones와 같은 파일은 요청 응답을 위해 정의한 구조체의 클래스만 들어있다. 즉 app.thrift에서 struct로 정의한 객체들의 파일이다.
실제로 서버로 호출을 하기위한 주된 역할을 하는 java파일은 PostService이며, Client와 Server 각각의 구현체 작성을 위한 코드는 전부 여기있다.

가이드에 따라 Client 코드를 짜고, 내 나름대로 리팩토링을 한다.

package com.jyami.sample.client

import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import org.apache.thrift.protocol.TBinaryProtocol
import org.apache.thrift.transport.TSocket
import org.apache.thrift.transport.TTransport
import java.nio.ByteBuffer

fun main() {

    val sampleByte = ByteBuffer.wrap("hello".toByteArray())

    connectWithServer { transport ->
        val client = makeClient(transport)
        client.ping()
        val readResponse: CommonResponse = client.read(RequestWithFlag(sampleByte, false))
        println("read response $readResponse")
        val saveResponse: CommonResponse = client.save(sampleByte)
        println("save response $saveResponse")
        val removeResponse: CommonResponse = client.remove(sampleByte)
        println("remove response $removeResponse")
    }

}

fun connectWithServer(process: (transport: TTransport) -> Unit) {
    val transport = TSocket("localhost", 9090)
    transport.open()
    process.invoke(transport)
    transport.close()
}

fun makeClient(transport: TTransport): PostService.Client = PostService.Client(TBinaryProtocol(transport))

build.gradle에 넣어준 thrift 라이브러리에 들어있는 것들 TBinaryProtocol, TSocket, TTransport 와 같은 객체들은 전부
thrift를 이용한 socket통신을 하기위해 apache에서 제공하는 추상화된 코드들이다. 해당 객체들을 사용하여 서버와 연결지을 커넥션을 생성하고, thrift.gen으로 만들어진 java 파일들 CommonResponse, PostService, RequestWithFlag를 사용하면 쉽게 클라이언트 코드를 작성할 수 있다.

connectWithServer(), makeClient() 메서드들 각각을 각각 thrift에서 실제 socket 커넥션을 맺는 역할, 호출을 위한 client 객체를 만드는 역할로 각각 코드를 작성해주었다. 

connectWithServer로 서버와 connection을 맺고, 해당 서버에 요청을 보낼 내용들은 makeClient로 생성된 PostService.Client객체에 정의된 메서드들을 사용한다. 해당 메서드들은 app.thrift에서 정의한 service들의 메서드임을 알 수 있고, 서버에서 해당 service들에 대한 비즈니스 로직을 채워서 client에 응답을 주면 client는 해당하는 응답에 따라 클라이언트 자체 뷰를 그리기도 하고 print를 하기도하는 등 클라이언트의 요구사항을 채워준다.

 

3-2. Server 코드 작성

package com.jyami.sample.server

import com.jyami.java.thrift.service.PostService
import org.apache.thrift.server.TServer.Args
import org.apache.thrift.server.TSimpleServer
import org.apache.thrift.transport.TServerSocket

fun main(){
    val serverTransport = TServerSocket(9090)
    val postServiceImpl = PostServiceImpl()
    val server = TSimpleServer(Args(serverTransport).processor(PostService.Processor(postServiceImpl)))

    println("Starting the simple server")
    server.serve()
}

 

서버 코드도 클라이언트와 마찬가지로 thrift 라이브러리를 사용하여 ServerSocket을 열어준다. TServerSocket, TSimpleServer(http2와 같은 secure 연결이 되어있지 않은 서버) 를 활용하여 thrift 통신을 하는 서버를 구동시킨다. 

이때 해당 서버에 요청이 들어오면 어떤 처리를 할 것 인지를 지정하는 것을 .processor() 메서드로 지정하는데.
이때 처리할 Processor 구현체는 아까 app.thrift에서 생성한 PostService안에 생성이 되어있다. 

 

 

package com.jyami.sample.server

import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import java.nio.ByteBuffer

class PostServiceImpl : PostService.Iface{

    override fun ping() {
        println("ping")
    }

    override fun read(request: RequestWithFlag): CommonResponse {
        println("server : read request")
        return CommonResponse()
    }

    override fun save(bsonData: ByteBuffer?): CommonResponse {
        println("server : save request")
        return CommonResponse()
    }

    override fun remove(bsonData: ByteBuffer?): CommonResponse {
        println("server : remove request")
        return CommonResponse()
    }

}

 

 

 

실제 해당 서버가 어떤 요청응답을 받는지에 대한 인터페이스는 PostService.Iface 를 이용하면 되기 때문에, 실제 서버에 작성한 프로토콜 명세에 대한 요청을 어떻게 처리할지만 정해주면 된다.
PostService.Iface를 구현한 구현체에서 각각의 프로토콜에 대한 비즈니스 로직을 길게 넣어주고 thrift파일에 정의한 프로토콜 명세에 맞게 요청값 응답값을 잘 넣어준다. 
PostService.Iface의 구현체 즉, 각 프로토콜의 동작이 정의된 비즈니스 로직을 담은 PostServiceImpl을 processor() 메서드 안에 넣어주면, 해당 thrift 서버의 동작을 정의할 수 있는 것이다.

 

3-3.  실행

위 코드대로 실제로 서버를 구동시키고 클라이언트가 2개 있다는 가정하에 Client 코드를 2번 가동시키면

서버 로그

클라이언트로부터 ping, read, save, remove, ping, read, save, remove 요청을 받아 처리하였다.

클라이언트 로그

서버에 read, save, remove 요청을 보내고 해당하는 응답을 받아 해당 응답을 직접 출력하였다.

 

3-4. 추가 잡담 - 코드 의도

thrift.app 파일에 int, long, string과 같은 다양한 타입을 정의할 수 있지만, struct에 굳이 binary를 사용한 이유는 thrift로 생성된 객체를 직접 바로 사용하고 싶지 않아서이다. 

만약 thrift로 생성된 요청 응답 객체를 직접 사용한다면, 요구사항에 따라 request에 param이 하나가 추가가 되야할 경우 그때마다 thrift.app 파일을 변경하고 그에따라 thrift gen을 이용해서 java 파일을 생성하고 생성된 java 파일에서 변경된 클래스명이나 파라이터 명에 따라 main 코드를 고쳐야한다. 

그러나 내가 원하는 요청응답 객체가 있을 때 (Post(title:String, username: String)) 위 객체를 jackson을 이용하여 byinary로 파싱하여 담으면 Post 객체 자체에 대한 요구사항을 훨씬 유연하게 처리할 수 있으면서, 가장 바깥에서 통신하는 thrift 객체는 body가 binary이기 때문에 거의 변화가 없어 위에서 말한 thrift 파일 변경 > thrift gen으로 파일생성 > 이에따른 main 코드 변경이 잦지 않게 되었기 때문에 위와 같은 코드 예시를 선택하였다.

샘플코드라서 오히려 간단해서 구분이 안갈 수 있지만, 해당 샘플 코드를 기반으로 나의 의도를 담아 비즈니스로직을 구현한 예시 소스코드가 보고싶다면 아래 링크에 넣어놨다. thrift 뽀개기 끝 ~_~

https://github.com/jyami-kim/Jyami-Java-Lab/tree/master/thrift-sample

 

GitHub - jyami-kim/Jyami-Java-Lab: 💻 Jyami의 Spring boot 및 Java 실험소 💻

💻 Jyami의 Spring boot 및 Java 실험소 💻. Contribute to jyami-kim/Jyami-Java-Lab development by creating an account on GitHub.

github.com

 

When it comes to server communication, HTTP is usually the first thing that comes to mind.
However, due to the need for something lighter and better cross-language compatibility, and partly because Facebook created it and it gained a lot of popularity, 
Thrift is also used for server-to-server communication.

 

1. How to Define Thrift

https://thrift.apache.org/

 

Apache Thrift - Home

The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haske

thrift.apache.org

First, install Thrift to use it.

$ brew update
$ brew install thrift

Write a Thrift file (IDL). Based on what's written in this file, the request and response formats for Thrift communication on that server will be defined.
For IDL syntax and examples, the official documentation's examples are probably the best resource.

https://thrift.apache.org/docs/types

 

Apache Thrift - Thrift Type system

Thrift Types The Thrift type system is intended to allow programmers to use native types as much as possible, no matter what programming language they are working in. This information is based on, and supersedes, the information in the Thrift Whitepaper. T

thrift.apache.org

 

Basic Type List

  • Basic types: bool, byte, i16, i32, i64, double, string (all numeric types are signed.)
  • Special type: binary - a sequence of unencoded bytes
  • struct: Same as a class in OOP languages, but without inheritance.
  • Containers: list, set, map<type1, type2>

Based on the types mentioned above, you can additionally define elements described at https://thrift.apache.org/docs/idl.

Services

Services are defined using the types described above. It's similar to defining an interface. Once a defined service is compiled with the Thrift compiler, Thrift generates both client and server code that can handle requests and responses according to the defined service.

Namespaces

This specifies which language the compiled Thrift IDL should be exported to. The documentation lists all the supported export languages. Since Thrift can generate code for a wide variety of languages, it seems to be particularly advantageous when used for communication between servers written in different languages.

 

As an example, I wrote a Thrift IDL file like below.

namespace java com.jyami.java.thrift.service
namespace py com.jyami.python.thrift.service

struct CommonResponse {
  1:required i32 status,
  2:optional binary bsonBody
}

struct RequestWithFlag {
  1:required binary bsonData,
  2:required bool flag
}

service PostService {

  void ping()

  CommonResponse read(1:RequestWithFlag request),

  CommonResponse save(1:binary bsonData),

  CommonResponse remove(1:binary bsonData)
}

 

2. Generating Code with Thrift Compiler

thrift --gen <language> <Thrift filename>

 

When running the above command for Java ( thrift --gen java app.thrift ), the Java files shown above were auto-generated.

Since we also set up Python in the namespace, Python files can be generated in the same way.

 

3. Creating Client and Server Code

https://thrift.apache.org/tutorial/java.html

 

Apache Thrift - Java

Java Tutorial Introduction All Apache Thrift tutorials require that you have: The Apache Thrift Compiler and Libraries, see Download and Building from Source for more details. Generated the tutorial.thrift and shared.thrift files: thrift -r --gen java tuto

thrift.apache.org

Honestly, the official documentation is really well done. I wrote both Client and Server in Java. 
The key point is using the classes and methods inside the Java files generated by thrift gen,
and the basic implementations for Thrift communication like TSocket, TTransport, etc. can be obtained by using the Thrift library as shown below.

implementation("org.apache.thrift:libthrift:0.18.0")

3-0. Setup

For convenience, I placed the client and server in the same directory.

But the key takeaway is that the various classes and methods — the implementations generated from app.thrift inside the thrift folder —

can be directly used in the actual client or server main code where the business logic resides, enabling Thrift communication.

In an IntelliJ Gradle project setup, I configured several settings so that the code between the main folder and the thrift folder can import and use each other, and also so that IntelliJ can index and recognize them properly in the development environment. I checked in Project Structure (Command + ;) under Modules to make sure the thrift folder was properly registered as a source.

## build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.7.10"
}

group = "org.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
    implementation("org.apache.thrift:libthrift:0.18.0")
    implementation("javax.annotation:javax.annotation-api:1.3.2")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1")
    implementation("de.undercouch:bson4jackson:2.13.1")
    implementation("org.slf4j:slf4j-api:1.7.25")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}


sourceSets {
    this.getByName("main"){
        this.java.srcDir("src/thrift/gen-java")
    }
}

 

I configured sourceSets in build.gradle so that IntelliJ recognizes the thrift folder as a sourceDirectory and can use the Java code inside it when running. I also verified in Project Structure (Command + ;) under Modules that the folder was properly registered as a source.

 

3-1. Writing the Client Code

If you look at the actual contents of the files generated by thrift gen, you'll see that files like RequestWithFlag and CommonResponse only contain the struct classes defined for requests and responses. In other words, they are the files for the objects defined as structs in app.thrift.
The main Java file responsible for making actual server calls is PostService, and all the code needed to write both Client and Server implementations is in there.

I wrote the Client code following the guide, and then refactored it in my own way.

package com.jyami.sample.client

import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import org.apache.thrift.protocol.TBinaryProtocol
import org.apache.thrift.transport.TSocket
import org.apache.thrift.transport.TTransport
import java.nio.ByteBuffer

fun main() {

    val sampleByte = ByteBuffer.wrap("hello".toByteArray())

    connectWithServer { transport ->
        val client = makeClient(transport)
        client.ping()
        val readResponse: CommonResponse = client.read(RequestWithFlag(sampleByte, false))
        println("read response $readResponse")
        val saveResponse: CommonResponse = client.save(sampleByte)
        println("save response $saveResponse")
        val removeResponse: CommonResponse = client.remove(sampleByte)
        println("remove response $removeResponse")
    }

}

fun connectWithServer(process: (transport: TTransport) -> Unit) {
    val transport = TSocket("localhost", 9090)
    transport.open()
    process.invoke(transport)
    transport.close()
}

fun makeClient(transport: TTransport): PostService.Client = PostService.Client(TBinaryProtocol(transport))

Objects like TBinaryProtocol, TSocket, and TTransport that come from the Thrift library added in build.gradle are all
abstracted code provided by Apache for socket communication using Thrift. You create a connection to the server using these objects, and then use the Java files generated by thrift gen — CommonResponse, PostService, RequestWithFlag — to easily write client code.

I wrote the connectWithServer() and makeClient() methods to handle establishing the actual socket connection with Thrift and creating the client object for making calls, respectively. 

connectWithServer establishes the connection with the server, and the actual requests sent to the server use the methods defined on the PostService.Client object created by makeClient. You can see that these methods correspond to the service methods defined in app.thrift. When the server fills in the business logic for those services and sends responses back to the client, the client can then render its own views, print output, or fulfill whatever client-side requirements it has.

 

3-2. Writing the Server Code

package com.jyami.sample.server

import com.jyami.java.thrift.service.PostService
import org.apache.thrift.server.TServer.Args
import org.apache.thrift.server.TSimpleServer
import org.apache.thrift.transport.TServerSocket

fun main(){
    val serverTransport = TServerSocket(9090)
    val postServiceImpl = PostServiceImpl()
    val server = TSimpleServer(Args(serverTransport).processor(PostService.Processor(postServiceImpl)))

    println("Starting the simple server")
    server.serve()
}

 

Just like the client, the server code also uses the Thrift library to open a ServerSocket. It uses TServerSocket and TSimpleServer (a server without secure connections like HTTP/2) to start a server that communicates via Thrift. 

To specify what processing should happen when a request comes into the server, you use the .processor() method.
The Processor implementation used here is already generated inside PostService, which was created from app.thrift. 

 

 

package com.jyami.sample.server

import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import java.nio.ByteBuffer

class PostServiceImpl : PostService.Iface{

    override fun ping() {
        println("ping")
    }

    override fun read(request: RequestWithFlag): CommonResponse {
        println("server : read request")
        return CommonResponse()
    }

    override fun save(bsonData: ByteBuffer?): CommonResponse {
        println("server : save request")
        return CommonResponse()
    }

    override fun remove(bsonData: ByteBuffer?): CommonResponse {
        println("server : remove request")
        return CommonResponse()
    }

}

 

 

 

Since the interface for what requests and responses the server handles can be obtained through PostService.Iface, all you need to do is define how to process the requests for the protocol specs written on the server.
In the implementation of PostService.Iface, you add the business logic for each protocol, and make sure the request and response values match the protocol specs defined in the Thrift file. 
The implementation of PostService.Iface — that is, the business logic where each protocol's behavior is defined — PostServiceImpl, is passed into the processor() method, which defines the behavior of the Thrift server.

 

3-3.  Running It

Following the code above, after actually starting the server and running the Client code twice (assuming there are 2 clients):

Server Log

It received and processed ping, read, save, remove, ping, read, save, remove requests from the clients.

Client Log

It sent read, save, and remove requests to the server, received the corresponding responses, and printed them out.

 

3-4. Extra Thoughts - Code Intent

While you can define various types like int, long, string in the thrift.app file, the reason I deliberately used binary in the struct is that I didn't want to directly use the objects generated by Thrift. 

If you directly use the request/response objects generated by Thrift, every time a new param needs to be added to a request due to changing requirements, you'd have to modify the thrift.app file, regenerate the Java files with thrift gen, and then update the main code to reflect any changed class names or parameter names. 

However, when I have my own desired request/response object (e.g., Post(title:String, username: String)), I can parse it into binary using Jackson and pass it along. This way, I can handle changes to the Post object itself much more flexibly, and since the outermost Thrift object's body is binary, it rarely changes. This means the cycle of modifying the Thrift file → regenerating files with thrift gen → updating main code accordingly happens much less frequently. That's why I chose this code approach.

Since it's sample code, it might actually be too simple to see the distinction clearly, but if you'd like to see the source code where I implemented business logic with my intent based on this sample code, I've put it at the link below. And that's a wrap on cracking Thrift! ~_~

https://github.com/jyami-kim/Jyami-Java-Lab/tree/master/thrift-sample

 

GitHub - jyami-kim/Jyami-Java-Lab: 💻 Jyami의 Spring boot 및 Java 실험소 💻

💻 Jyami의 Spring boot 및 Java 실험소 💻. Contribute to jyami-kim/Jyami-Java-Lab development by creating an account on GitHub.

github.com

 

댓글

Comments

Develop/Web

Image Styling with Web Components - 웹 컴포넌트를 사용한 이미지 스타일링 | Image Styling with Web Components

코드랩 세미나를 준비하기 위해 한글로 정리한 자료 입니다.https://codelabs.developers.google.com/codelabs/image-styling-web-components/#0 Image Styling with Web ComponentsYour Second Custom Element Let's now create a second custom element, codelab-effects. This element will render our image and possibly apply interesting visual effects to it. To start with, this is pretty much the same as the last element—with one extra ..

Image Styling with Web Components - 웹 컴포넌트를 사용한 이미지 스타일링 | Image Styling with Web Components

728x90

코드랩 세미나를 준비하기 위해 한글로 정리한 자료 입니다.

https://codelabs.developers.google.com/codelabs/image-styling-web-components/#0

 

Image Styling with Web Components

Your Second Custom Element Let's now create a second custom element, codelab-effects. This element will render our image and possibly apply interesting visual effects to it. To start with, this is pretty much the same as the last element—with one extra det

codelabs.developers.google.com

0. 소개

Web Component란?

HTML 페이지에 재사용 가능한 요소들을 작성할 수 있는 새로운 기술
새로 사용자가 정의한 이름을 갖는다 : 내가 원하는 태그들을 모아서 캡슐화 할 수 있다.

Custom Elements(codelab-dragdrop)와 shadow DOM(codelab-effects)을 사용해서 WebComponent를 만드는 과정이다. 이것들을 결합하여 페이지로 드래그되는 이미지를 조작 할 수있는 웹 사이트를 만든다.

  • Custom Elements를 선언하는 방법
  • Component에 리스너와 핸들러를 추가하는 방법
  • Custom Design을 캡슐화하기위한 Shadow Root를 만드는 방법
  • 여러 응용 프로그램을 구성하여 작은 응용 프로그램을 만드는 방법

깃허브 저장소 : https://github.com/googlecodelabs/image-styling-web-components

 

googlecodelabs/image-styling-web-components

Image Styling with Web Components. Contribute to googlecodelabs/image-styling-web-components development by creating an account on GitHub.

github.com

1. Custom Elements 만들기

1-1. 기본 HTML 틀 잡기

<!DOCTYPE html>
<html>
<head>
<script>
/* code will go here */
</script>
</head>
<body>

<h1>Image Styling with Web Components</h1>

<!-- elements will go here -->

</body>
</html>

1-2. 나의 첫번째 Custom Element 만들기

이미지를 이 페이지로 드래그앤 드롭을 하기 위한 코드를 작성해 보자.
<codelab-dragdrop></codelab-dragdrop> 태그를 생성할 예정이며, 이 태그는 파일이 드롭되는 위치를 표시하는 곳을 나타낼 것이다.

이에 대한 로직은 Javascript를 이용하여 구현할 예정이다.
1. 새로운 element를 정의
2. element를 사용(인스턴스 화)

<codelab-dragdrop></codelab-dragdrop>

1-3. Element 정의

Custom Element는 HTMLElement라는 ES6의 클래스를 상속받은 것 이다.

ES6는 ECMAScript6의 줄임말으로
ECMAScript6는 자바스크립트 표준 단체인 ECMA가 제정하는 자바스크립트 표준이다.

자바스크립트는 프로토타입 기반(prototype-based) 객체지향 언어다. 프로토타입 기반 프로그래밍은 클래스가 필요없는(class-free) 객체지향 프로그래밍 스타일로 프로토타입 체인과 클로저 등으로 객체 지향 언어의 상속, 캡슐화(정보 은닉) 등의 개념을 구현할 수 있다.

ES6의 클래스는 기존 프로토타입 기반 객체지향 프로그래밍보다 클래스 기반 언어에 익숙한 프로그래머가 보다 빠르게 학습할 수 있는 단순명료한 새로운 문법을 제시하고 있다. 그렇다고 ES6의 클래스가 기존의 프로토타입 기반 객체지향 모델을 폐지하고 새로운 객체지향 모델을 제공하는 것은 아니다. 
[출처] https://poiemaweb.com/es6-class

<script> 태그안에 Javascript 문법을 이용해서 나의 Custom Element를 정의한다.

/* code will go here */
class CodelabDragdrop extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    // we'll do stuff here later
    console.info('Element connected!');
  }
}
customElements.define('codelab-dragdrop', CodelabDragdrop);

connectedCallback() : document의 DOM에 정의한 custom element가 맨 처음 호출되었을 때 실행된다.
customElements.defind(DOMString, class, { extends: '[tag-name]' })
- DOMString : 사용자가 element에 전달하려는 이름 (즉, 태그 네임). 이때 커스텀 엘리먼트의 이름들은 dash('-')가 포함된 이름을 사용해야하므로 주의해야한다!
- class : element의 행위가 정의된 object이다.
- extends (optional) : 상속받을 태그를 지정할 수 있다. 만약 { extends: 'p'} 이렇게 지정한다면, p 태그의 inline 성질을 갖고있는 객체가 되는 듯

개발자도구( option+command+i / F12 )를 이용해서 확인해보면 connection이 완료되었다는 메세지를 확인 할 수 있다.

 

2. Drag and Drop

2-1. Target 생성하기

<!-- elements will go here -->
<codelab-dragdrop>
  <div style="width: 200px; height: 200px; background: red;">
  </div>
</codelab-dragdrop>

페이지를 새로 고침하면 큰 빨간색 상자가 나타난다. 
더 중요한 것은 페이지를 개발자 도구로 확인하면 codelab-dragdrop 내부에 빨간색 사각형을 보유하고 있기 때문에 현재 200 x 200 픽셀의 크기를 갖고있음을 알 수 있다.

2-2. Handler 추가하기

codelab-dragdrop에 파일을 끌어다 놓을 수 있도록 확장 해보자.

Web component가 가진 기능중에 하나는 캡슐화이다.
이를 수행할 수 있는 방법은 element defind 안에 코드를 추가하는 것이다.
이전에 작성한 ES6 클래스의 constructor element 자체에 리스너를 추가하여 메소드를 업데이트 할 것이다.

 constructor() {
    super();  // you always need super

    this.addEventListener('dragover', (ev) => {
      ev.preventDefault();
    });
    this.addEventListener('drop', (ev) => {
      ev.preventDefault();
      const file = ev.dataTransfer.files[0] || null;
      file && this._gotFile(file);
    });
  }

super()는 항상 필요하다. Web Element와 관련한 클래스인 HTMLElement 클래스를 상속받고,  그 기능을 온전히 사용하기 위해서.

ev.preventDefault() : 이벤트를 취소할 수 있는 경우, 이벤트의 전파를 막지않고 그 이벤트를 취소한다.
이전에 되어있던 이벤트 내역을 취소해 두는 느낌 인 것 같다
https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault

https://developer.mozilla.org/samples/domref/dispatchEvent.html

여기서 드래그 앤 드롭과 관련된 두 가지 이벤트를 처리합니다. 
여기서 중요한 것은 drop 핸들러이다. 첫 번째 파일 (있는 경우)을 드래그하여  _getFile() 이라는 메소드를 호출하게 되어있다.

2-3. Event 방출하기

이 코드랩의 목표는 Image를 조작할 수 있는 것(조작기 - manipulator)을 만드는 것이다. 따라서 우리는 이미지를 만들고, 해당하는 조작기에 이미지를 넘겨주는 것을 목적으로 한다.

이 작업을 수행할 일반 인터페이스를 제공하는 가장 좋은 방법은 HTML 자체를 이용해서 우리가 사용 가능할 수 있게 하는 것이다.
이전 단계의 drop과 같이 event를 생성해 볼 것이다. drop된 파일에서 하나의 유효한 이미지를 생성하는 것 같이 이 이벤트는 우리의 목표에 맞게 구체적으로 설정되어있다.
(추가하면 더 많은 작업을 할 수 있다 - 예 : 이미지의 크기를 먼저 조정한 다음 보낼 수 있다.)

이 코드에서 보는 것처럼 _getFile() 이라는 메소드를 작성한다. 이것은 File을 하나의 Image로 Load하며, custom한 Image로 방출한다.

  _gotFile(file) {
    const image = new Image();
    const reader = new FileReader();
    reader.onload = (event) => {
      // when the reader is ready
      image.src = event.target.result;
      image.onload = () => {
        // when the image is ready
        const params = {
          detail: image,
          bubbles: true,
        };
        const ev = new CustomEvent('image', params);
        this.dispatchEvent(ev);
      };
    };
    reader.readAsDataURL(file);
  }

reader가 준비가 된 경우, image객체의 src(경로) 속성에 drop 이벤트를 받은 타겟인 image의 결과값을 저장한다.
그렇게 해서 image가 준비가 잘 되었을 경우에 param이라는 변수를 정의하는데 이때 detail과 bubbles라는 속성을 지정하며.
우리가 image가 잘 들어왔는지 확인을 하기 위해서 CustomEvent라는 객체를 새로 생성한다.
this.dispatchEvent(ev) : 적어도 하나의 이벤트 핸들러가 해당하는 이벤트를 처리하면서 이 메소드를 호출했다면 false를 반환하고, 그렇지 않으면 true를 반환한다.

2-4. 시도하기

개발자 도구를 열고 다음을 붙여 넣는다.

document.querySelector('codelab-dragdrop').addEventListener('image',
    (ev) => console.info('got image', ev.detail));

해당하는 곳에 image가 drop 된다면 console에 해당하는 image에 대한 정보를 보여주는 것이다.
위에서 설정한 CustomEvent가 설정되는 것.

 

3. Connecting Element

3-1. 나의 두번째 Custom Element 만들기

codelab-effects라는 두번째 Custom Element를 만들어 보자. 
이 요소는 이미지를 렌더링하고 흥미로운 시각적 효과를 적용 할 수 있다.

class CodelabEffects extends HTMLElement {
  constructor() {
    super();
    this.root = this.attachShadow({mode: 'open'});
  }
}
customElements.define('codelab-effects', CodelabEffects);

3-2. Shadow Root 생성하기

Shadow DOM을 사용하면 실제로는 페이지에 없는 요소인 custom HTML을 추가하는 것을 허용해준다.
이때 Shadow DOM을 사용하기 위한 method가 attachShadow()인 것이다.

attachShadow()의 모드에 따라서 개발자 도구에서 해당 element의 내부에 있는 html 코드를 볼 수 있는지 없는지 여부를 판단할 수 있다.

실제 개발자 도구에서는 볼 수 없는 Element 이며, 이 것은 일반적으로 document.querySelector() 또는 getElementById()를 이용해서 호출을 할 수 있다.

모든 Custom Element에 Shadow Root가 필요한 것은 아니다.
실제로 <codelab-dragdrop> element는 새롭게 하나를 생성하지 않고도 drop된 파일을 조작하는 복잡한 로직을 수행한다. 그러나 이것은 정말 강력한 API이다.

Shadow DOM 내부의 HTML을 몇개의 코드를 추가해서 템플릿을 정의할 수 있다.

    this.root = this.attachShadow({mode: 'open'});
    this.root.innerHTML = `
<style>
:host {
    background: #fff;
    border: 1px solid black;
    display: inline-block;
}
</style>
<canvas id="canvas" width="512" height="512"></canvas>
<table>
  <tr>
    <td>AMOUNT</td>
    <td><input id="amount" type="range" min="3" max="40" value="10"></td>
  </tr>
</table>
`;

 

마지막으로 이 element를 codelab-dragdrop element 안에 넣는다. 
이때 이전에 있던 빨간색 상자는 제거한다. 
이제 codelab-effect element가 target image를 제공하는 데 도움이 된다.

<!-- elements will go here -->
<codelab-dragdrop>
  <codelab-effects></codelab-effects>
</codelab-dragdrop>

3-3. Putting It Together

이전 단계에 개발자 도구를 이용해서 event를 확인하려고 넣었던 스크립트를 기억하는가?
이 스크립트를 이용해서 이벤트를 연결해 보자!

<!-- elements will go here -->
<codelab-dragdrop id="dragdrop">
  <codelab-effects id="effects"></codelab-effects>
</codelab-dragdrop>
<script>
dragdrop.addEventListener('image', (ev) => {
  effects.image = ev.detail;  // set the image that we got in dragdrop
});
</script>

이제 image 이벤트가 발생하면(즉 image를 drop 했다는 이벤트가 발생하면) codelab-effects 요소에 있는 image 속성을 설정하도록 코드가 장성되어있다. 이제 이렇게 제공한 이미지의 픽셀 데이터를 가져오는 것을 코드에 추가해 보자.

 constructor() {
    super();
    this.root = this.attachShadow({mode: 'open'});
    // Leave the root.innerHTML part alone
  }

  // Add this method
  set image(image) {
    const canvas = this.root.getElementById('canvas');

    // resize image to something reasonable
    canvas.width = Math.min(1024, Math.max(256, image.width));
    canvas.height = (image.height * (canvas.width / image.width));

    // clone buffer to get one of same size
    const buf = canvas.cloneNode(true);
    const ctx = buf.getContext('2d');
    ctx.drawImage(image, 0, 0, buf.width, buf.height);
    this.data = ctx.getImageData(0, 0, buf.width, buf.height).data;
    console.info(this.data);
  }

setter는 클래스 필드에 값을 할당할 때마다 클래스 필드의 값을 조작하는 행위가 필요할 때 사용한다. setter는 메소드 이름 앞에 
set 키워드를 사용해 정의한다. 이때 메소드 이름은 클래스 필드 이름처럼 사용된다.
다시 말해 setter는 호출하는 것이 아니라 프로퍼티처럼 값을 할당하는 형식으로 사용하며 할당 시에 메소드가 호출된다.

이제 이 곳에 이미지를 Drag and Drop 하면 해당하는 이미지의 픽셀 데이터를 console에서 확인 할 수 있다.

3-4. 기본 스타일링

console.log()를 이용해서 데이터 숫자값만 보기 보다는 데이터를 가져와서 실제 캔버스로 그리는 작업을 해보자.
set image 메서드 내부에서 만든 이미지 데이터를 보고 캔버스에 그린다.
이 코드랩은 캔버스 사용 및 이미지 데이터 작업에 관한 것이 아니라 웹 구성 요소를 시연하는데 도움이 되며 흥미로운 효과를 기대할 수 있다.

    // Replace console.info with:
    this.draw();
  }

  // And add this method
  draw() {
    const canvas = this.root.getElementById('canvas');
    canvas.width = canvas.width;  // clear canvas
    const context = canvas.getContext('2d');

    const amount = +this.root.getElementById('amount').value;
    const size = amount * .8;

    for (let y = amount; y < canvas.height; y += amount * 2) {
      for (let x = amount; x < canvas.width; x += amount * 2) {
        const index = ((y * canvas.width) + x) * 4;
        const [r,g,b] = this.data.slice(index, index+3);
        const color = `rgb(${r},${g},${b})`;

        context.beginPath();
        context.arc(x, y, size, 0, 360, false);
        context.fillStyle = color;
        context.fill();
      }
    }
  }

페이지를 새로 고침하고 좋아하는 이미지를 페이지로 드래그하면 점묘 효과가 나타난다.

4. Saving Images

4-1. Click To Download

방금 만든 점묘화의 이미지를 공유하거나 사용하기 위해서는 마우스 오른쪽 버튼을 클릭해서 이미지를 다운로드 해야한다.
대신 canvas 아래에 링크를 추가하여 이미지를 자동으로 다운로드를 할 수 있도록 환경을 만들어 볼 예정이다.

    this.root.innerHTML = `
...
<canvas id="canvas" width="512" height="512"></canvas>
<br /><a href="#" id="link">Download</a>
...
`;

그리고 이 link라는 id값을 받아서 download 할 수 있도록 이벤트 핸들러를 설정한다.

    // And add this handler
    const link = this.root.getElementById('link');
    link.addEventListener('click', (ev) => {
      link.href = this.root.getElementById('canvas').toDataURL();
      link.download = 'pointify.png';
    });

다운로드 완료!!

 

5. Control 추가

5-1. 응답하기

"AMOUNT" 슬라이더를 활용해보자! 이 것을 활용해서 우리가 그리는 점묘화의 점의 크기를 제어할 수 있다. 그러나 현재는 이미지 자체가 드롭될 때 한번만 발생하기 때문에 코드를 수정해보자

CodeLabEffects 클래스의 생성자 안에 리스너를 추가해 보자.
다시 리마인드 하면 이것은 this.root는 AMOUNT 슬라이더를 포함한 모든 Shadow DOM이 있는 곳이다.
이것은 Shadow DOM element에 의해 생긴 모든 변경에 응답하고 draw 메소드를 호출한다.

...
      link.download = 'pointify.png';
    });

    //add these two new listeners
    this.root.addEventListener('input', (ev) => this.draw());
    this.root.addEventListener('change', (ev) => this.draw());

  }

input이나 change와 관련한 이벤트가 들어왔을 경우에 draw()를 다시 실행한다.

5-2. 고급 컨트롤

shadow DOM에 컨트롤을 추가한다.

    this.root.innerHTML = `
... <!-- add some new <tr>'s at the bottom -->
  <tr>
    <td>SIZE</td>
    <td><input id="size" type="range" min="0" max="4" step="0.01" value="1"></td>
  </tr>
  <tr>
    <td>OPACITY</td>
    <td><input id="opacity" type="range" min="0" max="1" step="0.01" value="1"></td>
  </tr>
  <tr>
    <td>ATTENUATION</td>
    <td><input id="attenuation" type="checkbox"></td>
  </tr>

</table>
`;

렌더링 코드인 draw() 역시 컨트롤에 따라 조금 수정해준다.

draw() {
    const canvas = this.root.getElementById('canvas');
    canvas.width = canvas.width;  // clear canvas
    const context = canvas.getContext('2d');

    const attenuation = this.root.getElementById('attenuation').checked;
    const amount = +this.root.getElementById('amount').value;
    const size = this.root.getElementById('size').value * amount;
    const opacity = this.root.getElementById('opacity').value;

    for (let y = amount; y < canvas.height; y += amount * 2) {
      for (let x = amount; x < canvas.width; x += amount * 2) {
        const index = ((y * canvas.width) + x) * 4;
        const [r,g,b] = this.data.slice(index, index+3);
        const color = `rgba(${r},${g},${b},${opacity})`;

        const weight = 1 - ( this.data[ index ] / 255 );
        const radius = (attenuation ? size * weight : size);

        context.beginPath();
        context.arc(x, y, radius, 0, 360, false);
        context.fillStyle = color;
        context.fill();
      }
    }
  }

size : 원의 기본 반경을 제어한다
opacity : 원의 투명도를 제어한다.
attenuation : 각 원의 어두운 정도에 따라 원의 크기를 조정한다.

5-3. 추가 기능

이미지 전체를 색조로 만드는 컬러 필터
다른 모양을 사용
정렬되지 않은 배치 등

이 component element 에는 많은 가능성이 있다!

Polymer와 같은 다양한 Web Component Element 라이브러리도 있다.
이 라이브러리에는 지금 렌더링 수준의 그림 하나하나 생각했던 것과 같은 Low Level의 Component Element보다 좀더 High Level 계층의 추상화된 라이브러리를 제공한다.

This is a summary I put together in Korean to prepare for a codelab seminar.

https://codelabs.developers.google.com/codelabs/image-styling-web-components/#0

 

Image Styling with Web Components

Your Second Custom Element Let's now create a second custom element, codelab-effects. This element will render our image and possibly apply interesting visual effects to it. To start with, this is pretty much the same as the last element—with one extra det

codelabs.developers.google.com

0. Introduction

What is a Web Component?

A new technology that lets you create reusable elements for HTML pages.
They have custom user-defined names: you can bundle the tags you want and encapsulate them.

This is the process of creating a WebComponent using Custom Elements (codelab-dragdrop) and shadow DOM (codelab-effects). By combining these, we'll build a website that can manipulate images dragged onto the page.

  • How to declare Custom Elements
  • How to add listeners and handlers to a Component
  • How to create a Shadow Root to encapsulate Custom Design
  • How to compose multiple components to build a small application

GitHub repository: https://github.com/googlecodelabs/image-styling-web-components

 

googlecodelabs/image-styling-web-components

Image Styling with Web Components. Contribute to googlecodelabs/image-styling-web-components development by creating an account on GitHub.

github.com

1. Creating Custom Elements

1-1. Setting Up the Basic HTML Structure

<!DOCTYPE html>
<html>
<head>
<script>
/* code will go here */
</script>
</head>
<body>

<h1>Image Styling with Web Components</h1>

<!-- elements will go here -->

</body>
</html>

1-2. Creating My First Custom Element

Let's write the code for dragging and dropping an image onto this page.
<codelab-dragdrop></codelab-dragdrop> — we're going to create this tag, and it will indicate the area where files can be dropped.

The logic for this will be implemented using Javascript.
1. Define a new element
2. Use the element (instantiate it)

<codelab-dragdrop></codelab-dragdrop>

1-3. Defining the Element

A Custom Element is an ES6 class that extends HTMLElement.

ES6 stands for ECMAScript 6.
ECMAScript 6 is a JavaScript standard established by ECMA, the JavaScript standards body.

JavaScript is a prototype-based object-oriented language. Prototype-based programming is a class-free object-oriented programming style that can implement concepts like inheritance and encapsulation (information hiding) through prototype chains and closures.

ES6 classes provide a cleaner, simpler syntax that makes it easier for programmers familiar with class-based languages to learn quickly, compared to the traditional prototype-based object-oriented programming. That said, ES6 classes don't replace the existing prototype-based object-oriented model with a new one. 
[Source] https://poiemaweb.com/es6-class

We define our Custom Element using Javascript syntax inside the <script> tag.

/* code will go here */
class CodelabDragdrop extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    // we'll do stuff here later
    console.info('Element connected!');
  }
}
customElements.define('codelab-dragdrop', CodelabDragdrop);

connectedCallback(): This is called when the custom element is first inserted into the document's DOM.
customElements.define(DOMString, class, { extends: '[tag-name]' })
- DOMString: The name you want to give the element (i.e., the tag name). Note that custom element names must include a dash ('-'), so keep that in mind!
- class: An object that defines the element's behavior.
- extends (optional): You can specify a tag to inherit from. For example, if you set { extends: 'p'}, the resulting object seems to take on the inline properties of a p tag.

If you check using the developer tools (option+command+i / F12), you can confirm the connection completed message.

 

2. Drag and Drop

2-1. Creating the Target

<!-- elements will go here -->
<codelab-dragdrop>
  <div style="width: 200px; height: 200px; background: red;">
  </div>
</codelab-dragdrop>

When you refresh the page, a big red box appears. 
More importantly, if you inspect the page with developer tools, you can see that the red square is contained inside codelab-dragdrop, so it currently has a size of 200 x 200 pixels.

2-2. Adding Handlers

Let's extend codelab-dragdrop so that files can be dragged and dropped onto it.

One of the features of Web Components is encapsulation.
The way to achieve this is by adding code inside the element definition.
We'll update the constructor of the ES6 class we wrote earlier by adding listeners to the element itself.

 constructor() {
    super();  // you always need super

    this.addEventListener('dragover', (ev) => {
      ev.preventDefault();
    });
    this.addEventListener('drop', (ev) => {
      ev.preventDefault();
      const file = ev.dataTransfer.files[0] || null;
      file && this._gotFile(file);
    });
  }

super() is always required — to inherit from HTMLElement, the class related to Web Elements, and to fully use its features.

ev.preventDefault(): If the event is cancelable, it cancels the event without stopping its propagation.
It feels like it clears out any previously set event behavior.
https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault

https://developer.mozilla.org/samples/domref/dispatchEvent.html

Here we handle two events related to drag and drop. 
The important one here is the drop handler. It takes the first file (if there is one) from the drag and  calls a method called _getFile().

2-3. Emitting Events

The goal of this codelab is to build something that can manipulate images (a manipulator). So our objective is to create an image and pass it to the corresponding manipulator.

The best way to provide a general interface for this is to make it available to us through HTML itself.
Just like the drop event from the previous step, we're going to create an event. Like generating a single valid image from a dropped file, this event is specifically tailored to our goal.
(You can do more if you add to it — for example, you could resize the image first before sending it.)

As you can see in this code, we write a method called _getFile(). It loads a File as an Image and emits it as a custom Image event.

  _gotFile(file) {
    const image = new Image();
    const reader = new FileReader();
    reader.onload = (event) => {
      // when the reader is ready
      image.src = event.target.result;
      image.onload = () => {
        // when the image is ready
        const params = {
          detail: image,
          bubbles: true,
        };
        const ev = new CustomEvent('image', params);
        this.dispatchEvent(ev);
      };
    };
    reader.readAsDataURL(file);
  }

When the reader is ready, the result of the drop event target image is stored in the image object's src (path) property.
Once the image is successfully loaded, we define a variable called params with detail and bubbles properties.
To verify that the image was received correctly, we create a new CustomEvent object.
this.dispatchEvent(ev): Returns false if at least one event handler that handled the event called this method, and true otherwise.

2-4. Trying It Out

Open the developer tools and paste the following:

document.querySelector('codelab-dragdrop').addEventListener('image',
    (ev) => console.info('got image', ev.detail));

If an image is dropped onto the designated area, it shows the image information in the console.
This is where the CustomEvent we set up earlier kicks in.

 

3. Connecting Element

3-1. Creating My Second Custom Element

Let's create a second Custom Element called codelab-effects. 
This element will render the image and can apply interesting visual effects to it.

class CodelabEffects extends HTMLElement {
  constructor() {
    super();
    this.root = this.attachShadow({mode: 'open'});
  }
}
customElements.define('codelab-effects', CodelabEffects);

3-2. Creating a Shadow Root

Shadow DOM allows you to add custom HTML that isn't actually part of the page's main DOM.
The method used to enable Shadow DOM is attachShadow().

Depending on the mode of attachShadow(), you can control whether the HTML code inside the element is visible in the developer tools or not.

It's an element that isn't visible in the actual developer tools, and it can typically be accessed using document.querySelector() or getElementById().

Not every Custom Element needs a Shadow Root.
In fact, the <codelab-dragdrop> element performs complex logic for manipulating dropped files without creating a new one. But it's a really powerful API.

You can define a template by adding a few lines of code for the HTML inside the Shadow DOM.

    this.root = this.attachShadow({mode: 'open'});
    this.root.innerHTML = `
<style>
:host {
    background: #fff;
    border: 1px solid black;
    display: inline-block;
}
</style>
<canvas id="canvas" width="512" height="512"></canvas>
<table>
  <tr>
    <td>AMOUNT</td>
    <td><input id="amount" type="range" min="3" max="40" value="10"></td>
  </tr>
</table>
`;

 

Finally, we place this element inside the codelab-dragdrop element. 
Remove the red box from before. 
Now the codelab-effects element helps provide the target image.

<!-- elements will go here -->
<codelab-dragdrop>
  <codelab-effects></codelab-effects>
</codelab-dragdrop>

3-3. Putting It Together

Remember the script we pasted in the developer tools to check the event in the previous step?
Let's use this script to connect the events!

<!-- elements will go here -->
<codelab-dragdrop id="dragdrop">
  <codelab-effects id="effects"></codelab-effects>
</codelab-dragdrop>
<script>
dragdrop.addEventListener('image', (ev) => {
  effects.image = ev.detail;  // set the image that we got in dragdrop
});
</script>

Now the code is set up so that when an image event fires (i.e., when an image is dropped), it sets the image property on the codelab-effects element. Let's now add code to grab the pixel data from the provided image.

 constructor() {
    super();
    this.root = this.attachShadow({mode: 'open'});
    // Leave the root.innerHTML part alone
  }

  // Add this method
  set image(image) {
    const canvas = this.root.getElementById('canvas');

    // resize image to something reasonable
    canvas.width = Math.min(1024, Math.max(256, image.width));
    canvas.height = (image.height * (canvas.width / image.width));

    // clone buffer to get one of same size
    const buf = canvas.cloneNode(true);
    const ctx = buf.getContext('2d');
    ctx.drawImage(image, 0, 0, buf.width, buf.height);
    this.data = ctx.getImageData(0, 0, buf.width, buf.height).data;
    console.info(this.data);
  }

A setter is used when you need to manipulate the value of a class field every time a value is assigned to it. A setter is defined by placing the 
set keyword before the method name. The method name is then used as if it were a class field name.
In other words, a setter isn't called like a regular method — it's used in the format of assigning a value like a property, and the method is invoked upon assignment.

Now if you drag and drop an image here, you can see the pixel data of that image in the console.

3-4. Basic Styling

Rather than just looking at data numbers via console.log(), let's actually grab the data and draw it on the canvas.
We take the image data created inside the set image method and draw it on the canvas.
This codelab isn't about working with canvas or image data — it's about demonstrating Web Components, and you can look forward to some interesting effects.

    // Replace console.info with:
    this.draw();
  }

  // And add this method
  draw() {
    const canvas = this.root.getElementById('canvas');
    canvas.width = canvas.width;  // clear canvas
    const context = canvas.getContext('2d');

    const amount = +this.root.getElementById('amount').value;
    const size = amount * .8;

    for (let y = amount; y < canvas.height; y += amount * 2) {
      for (let x = amount; x < canvas.width; x += amount * 2) {
        const index = ((y * canvas.width) + x) * 4;
        const [r,g,b] = this.data.slice(index, index+3);
        const color = `rgb(${r},${g},${b})`;

        context.beginPath();
        context.arc(x, y, size, 0, 360, false);
        context.fillStyle = color;
        context.fill();
      }
    }
  }

Refresh the page and drag your favorite image onto it — you'll see a pointillism effect appear.

4. Saving Images

4-1. Click To Download

To share or use the pointillism image we just created, you'd have to right-click and download the image.
Instead, we're going to add a link below the canvas so that the image can be downloaded automatically.

    this.root.innerHTML = `
...
<canvas id="canvas" width="512" height="512"></canvas>
<br /><a href="#" id="link">Download</a>
...
`;

Then we set up an event handler using the link id to enable the download.

    // And add this handler
    const link = this.root.getElementById('link');
    link.addEventListener('click', (ev) => {
      link.href = this.root.getElementById('canvas').toDataURL();
      link.download = 'pointify.png';
    });

Download complete!!

 

5. Adding Controls

5-1. Responding to Input

Let's make use of the "AMOUNT" slider! We can use it to control the size of the dots in our pointillism image. But right now, the drawing only happens once when the image is dropped, so let's fix the code.

Let's add a listener inside the constructor of the CodelabEffects class.
As a reminder, this.root is where all the Shadow DOM lives, including the AMOUNT slider.
This will respond to any changes made by Shadow DOM elements and call the draw method.

...
      link.download = 'pointify.png';
    });

    //add these two new listeners
    this.root.addEventListener('input', (ev) => this.draw());
    this.root.addEventListener('change', (ev) => this.draw());

  }

When an input or change event comes in, it re-executes draw().

5-2. Advanced Controls

Let's add controls to the shadow DOM.

    this.root.innerHTML = `
... <!-- add some new <tr>'s at the bottom -->
  <tr>
    <td>SIZE</td>
    <td><input id="size" type="range" min="0" max="4" step="0.01" value="1"></td>
  </tr>
  <tr>
    <td>OPACITY</td>
    <td><input id="opacity" type="range" min="0" max="1" step="0.01" value="1"></td>
  </tr>
  <tr>
    <td>ATTENUATION</td>
    <td><input id="attenuation" type="checkbox"></td>
  </tr>

</table>
`;

The rendering code, draw(), also needs a slight update to accommodate the controls.

draw() {
    const canvas = this.root.getElementById('canvas');
    canvas.width = canvas.width;  // clear canvas
    const context = canvas.getContext('2d');

    const attenuation = this.root.getElementById('attenuation').checked;
    const amount = +this.root.getElementById('amount').value;
    const size = this.root.getElementById('size').value * amount;
    const opacity = this.root.getElementById('opacity').value;

    for (let y = amount; y < canvas.height; y += amount * 2) {
      for (let x = amount; x < canvas.width; x += amount * 2) {
        const index = ((y * canvas.width) + x) * 4;
        const [r,g,b] = this.data.slice(index, index+3);
        const color = `rgba(${r},${g},${b},${opacity})`;

        const weight = 1 - ( this.data[ index ] / 255 );
        const radius = (attenuation ? size * weight : size);

        context.beginPath();
        context.arc(x, y, radius, 0, 360, false);
        context.fillStyle = color;
        context.fill();
      }
    }
  }

size: Controls the base radius of the circles.
opacity: Controls the transparency of the circles.
attenuation: Adjusts the size of each circle based on how dark it is.

5-3. Additional Features

A color filter that tints the entire image
Using different shapes
Randomized placement, etc.

There are so many possibilities with this component element!

There are also various Web Component element libraries like Polymer.
These libraries provide higher-level abstractions rather than the low-level component elements we were working with here, where we had to think about each individual rendering detail.

'Develop > Web' 카테고리의 다른 글

Thrift 뽀개기 | Cracking Thrift  (0) 2023.02.23
web & server - DSC Ewha 세션 | web & server - DSC Ewha Session  (0) 2019.10.15

댓글

Comments

Develop/Web

web & server - DSC Ewha 세션 | web & server - DSC Ewha Session

DSC Ewha에서 진행하는 미니 세미나에서 개발을 처음시작하는 멤버분들께 요청 응답 구조 및 서버의 개념을 설명했습니다.그리고 제가 알고있는 개발 프레임워크 및 스택들에 대해서 간단히 설명을 드렸습니다. Web and server from 민정 김At a mini seminar held by DSC Ewha, I explained the concept of request-response architecture and servers to members who were just getting started with development.I also gave a brief overview of the development frameworks and stacks that I'm familiar with...

web & server - DSC Ewha 세션 | web & server - DSC Ewha Session

728x90

DSC Ewha에서 진행하는 미니 세미나에서 개발을 처음시작하는 멤버분들께 요청 응답 구조 및 서버의 개념을 설명했습니다.

그리고 제가 알고있는 개발 프레임워크 및 스택들에 대해서 간단히 설명을 드렸습니다.

 

At a mini seminar held by DSC Ewha, I explained the concept of request-response architecture and servers to members who were just getting started with development.

I also gave a brief overview of the development frameworks and stacks that I'm familiar with.

 

댓글

Comments