Apple will allow alternative browser engines for iPhone and iPad users (iOS/iPadOS) in Japan.
https://developer.apple.com/support/alternative-browser-engines-jp/
Apple should allow alt engine for the rest of the world too. No point holding it back.
@hongminhee@hackers.pub · 1006 following · 713 followers
Hi, I'm who's behind Fedify, Hollo, BotKit, and this website, Hackers' Pub! My main account is at
@hongminhee洪 民憙 (Hong Minhee)
.
Fedify, Hollo, BotKit, 그리고 보고 계신 이 사이트 Hackers' Pub을 만들고 있습니다. 제 메인 계정은:
@hongminhee洪 民憙 (Hong Minhee)
.
Fedify、Hollo、BotKit、そしてこのサイト、Hackers' Pubを作っています。私のメインアカウントは「
@hongminhee洪 民憙 (Hong Minhee)
」に。
Apple will allow alternative browser engines for iPhone and iPad users (iOS/iPadOS) in Japan.
https://developer.apple.com/support/alternative-browser-engines-jp/
Apple should allow alt engine for the rest of the world too. No point holding it back.
洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
It's 2 AM. Something is wrong in production. Users are complaining, but you're not sure what's happening—your only clues are a handful of console.log statements you sprinkled around during development. Half of them say things like “here” or “this works.” The other half dump entire objects that scroll off the screen. Good luck.
We've all been there. And yet, setting up “proper” logging often feels like overkill. Traditional logging libraries like winston or Pino come with their own learning curves, configuration formats, and assumptions about how you'll deploy your app. If you're working with edge functions or trying to keep your bundle small, adding a logging library can feel like bringing a sledgehammer to hang a picture frame.
I'm a fan of the “just enough” approach—more than raw console.log, but without the weight of a full-blown logging framework. We'll start from console.log(), understand its real limitations (not the exaggerated ones), and work toward a setup that's actually useful. I'll be using LogTape for the examples—it's a zero-dependency logging library that works across Node.js, Deno, Bun, and edge functions, and stays out of your way when you don't need it.
The console object is JavaScript's great equalizer. It's built-in, it works everywhere, and it requires zero setup. You even get basic severity levels: console.debug(), console.info(), console.warn(), and console.error(). In browser DevTools and some terminal environments, these show up with different colors or icons.
console.debug("Connecting to database...");
console.info("Server started on port 3000");
console.warn("Cache miss for user 123");
console.error("Failed to process payment");
For small scripts or quick debugging, this is perfectly fine. But once your application grows beyond a few files, the cracks start to show:
No filtering without code changes. Want to hide debug messages in production? You'll need to wrap every console.debug() call in a conditional, or find-and-replace them all. There's no way to say “show me only warnings and above” at runtime.
Everything goes to the console. What if you want to write logs to a file? Send errors to Sentry? Stream logs to CloudWatch? You'd have to replace every console.* call with something else—and hope you didn't miss any.
No context about where logs come from. When your app has dozens of modules, a log message like “Connection failed” doesn't tell you much. Was it the database? The cache? A third-party API? You end up prefixing every message manually: console.error("[database] Connection failed").
No structured data. Modern log analysis tools work best with structured data (JSON). But console.log("User logged in", { userId: 123 }) just prints User logged in { userId: 123 } as a string—not very useful for querying later.
Libraries pollute your logs. If you're using a library that logs with console.*, those messages show up whether you want them or not. And if you're writing a library, your users might not appreciate unsolicited log messages.
Before diving into code, let's think about what would actually solve the problems above. Not a wish list of features, but the practical stuff that makes a difference when you're debugging at 2 AM or trying to understand why requests are slow.
A logging system should let you categorize messages by severity—trace, debug, info, warning, error, fatal—and then filter them based on what you need. During development, you want to see everything. In production, maybe just warnings and above. The key is being able to change this without touching your code.
When your app grows beyond a single file, you need to know where logs are coming from. A good logging system lets you tag logs with categories like ["my-app", "database"] or ["my-app", "auth", "oauth"]. Even better, it lets you set different log levels for different categories—maybe you want debug logs from the database module but only warnings from everything else.
“Sink” is just a fancy word for “where logs go.” You might want logs to go to the console during development, to files in production, and to an external service like Sentry or CloudWatch for errors. A good logging system lets you configure multiple sinks and route different logs to different destinations.
Instead of logging strings, you log objects with properties. This makes logs machine-readable and queryable:
// Instead of this:
logger.info("User 123 logged in from 192.168.1.1");
// You do this:
logger.info("User logged in", { userId: 123, ip: "192.168.1.1" });
Now you can search for all logs where userId === 123 or filter by IP address.
In a web server, you often want all logs from a single request to share a common identifier (like a request ID). This makes it possible to trace a request's journey through your entire system.
There are plenty of logging libraries out there. winston has been around forever and has a plugin for everything. Pino is fast and outputs JSON. bunyan, log4js, signale—the list goes on.
So why LogTape? A few reasons stood out to me:
Zero dependencies. Not “few dependencies”—actually zero. In an era where a single npm install can pull in hundreds of packages, this matters for security, bundle size, and not having to wonder why your lockfile just changed.
Works everywhere. The same code runs on Node.js, Deno, Bun, browsers, and edge functions like Cloudflare Workers. No polyfills, no conditional imports, no “this feature only works on Node.”
Doesn't force itself on users. If you're writing a library, you can add logging without your users ever knowing—unless they want to see the logs. This is a surprisingly rare feature.
Let's set it up:
npm add @logtape/logtape # npm
pnpm add @logtape/logtape # pnpm
yarn add @logtape/logtape # Yarn
deno add jsr:@logtape/logtape # Deno
bun add @logtape/logtape # Bun
Configuration happens once, at your application's entry point:
import { configure, getConsoleSink, getLogger } from "@logtape/logtape";
await configure({
sinks: {
console: getConsoleSink(), // Where logs go
},
loggers: [
{ category: ["my-app"], lowestLevel: "debug", sinks: ["console"] }, // What to log
],
});
// Now you can log from anywhere in your app:
const logger = getLogger(["my-app", "server"]);
logger.info`Server started on port 3000`;
logger.debug`Request received: ${{ method: "GET", path: "/api/users" }}`;
Notice a few things:
sinks) and which logs to show (lowestLevel).["my-app", "server"] inherits settings from ["my-app"].Here's a scenario: you're debugging a database issue. You want to see every query, every connection attempt, every retry. But you don't want to wade through thousands of HTTP request logs to find them.
Categories let you solve this. Instead of one global log level, you can set different verbosity for different parts of your application.
await configure({
sinks: {
console: getConsoleSink(),
},
loggers: [
{ category: ["my-app"], lowestLevel: "info", sinks: ["console"] }, // Default: info and above
{ category: ["my-app", "database"], lowestLevel: "debug", sinks: ["console"] }, // DB module: show debug too
],
});
Now when you log from different parts of your app:
// In your database module:
const dbLogger = getLogger(["my-app", "database"]);
dbLogger.debug`Executing query: ${sql}`; // This shows up
// In your HTTP module:
const httpLogger = getLogger(["my-app", "http"]);
httpLogger.debug`Received request`; // This is filtered out (below "info")
httpLogger.info`GET /api/users 200`; // This shows up
If you're using libraries that also use LogTape, you can control their logs separately:
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: ["my-app"], lowestLevel: "debug", sinks: ["console"] },
// Only show warnings and above from some-library
{ category: ["some-library"], lowestLevel: "warning", sinks: ["console"] },
],
});
Sometimes you want a catch-all configuration. The root logger (empty category []) catches everything:
await configure({
sinks: { console: getConsoleSink() },
loggers: [
// Catch all logs at info level
{ category: [], lowestLevel: "info", sinks: ["console"] },
// But show debug for your app
{ category: ["my-app"], lowestLevel: "debug", sinks: ["console"] },
],
});
LogTape has six log levels. Choosing the right one isn't just about severity—it's about who needs to see the message and when.
| Level | When to use it |
|---|---|
trace |
Very detailed diagnostic info. Loop iterations, function entry/exit. Usually only enabled when hunting a specific bug. |
debug |
Information useful during development. Variable values, state changes, flow control decisions. |
info |
Normal operational messages. “Server started,” “User logged in,” “Job completed.” |
warning |
Something unexpected happened, but the app can continue. Deprecated API usage, retry attempts, missing optional config. |
error |
Something failed. An operation couldn't complete, but the app is still running. |
fatal |
The app is about to crash or is in an unrecoverable state. |
const logger = getLogger(["my-app"]);
logger.trace`Entering processUser function`;
logger.debug`Processing user ${{ userId: 123 }}`;
logger.info`User successfully created`;
logger.warn`Rate limit approaching: ${980}/1000 requests`;
logger.error`Failed to save user: ${error.message}`;
logger.fatal`Database connection lost, shutting down`;
A good rule of thumb: in production, you typically run at info or warning level. During development or when debugging, you drop down to debug or trace.
At some point, you'll want to search your logs. “Show me all errors from the payment service in the last hour.” “Find all requests from user 12345.” “What's the average response time for the /api/users endpoint?”
If your logs are plain text strings, these queries are painful. You end up writing regexes, hoping the log format is consistent, and cursing past-you for not thinking ahead.
Structured logging means attaching data to your logs as key-value pairs, not just embedding them in strings. This makes logs machine-readable and queryable.
LogTape supports two syntaxes for this:
const userId = 123;
const action = "login";
logger.info`User ${userId} performed ${action}`;
logger.info("User performed action", {
userId: 123,
action: "login",
ip: "192.168.1.1",
timestamp: new Date().toISOString(),
});
You can reference properties in your message using placeholders:
logger.info("User {userId} logged in from {ip}", {
userId: 123,
ip: "192.168.1.1",
});
// Output: User 123 logged in from 192.168.1.1
LogTape supports dot notation and array indexing in placeholders:
logger.info("Order {order.id} placed by {order.customer.name}", {
order: {
id: "ORD-001",
customer: { name: "Alice", email: "alice@example.com" },
},
});
logger.info("First item: {items[0].name}", {
items: [{ name: "Widget", price: 9.99 }],
});
For production, you often want logs as JSON (one object per line). LogTape has a built-in formatter for this:
import { configure, getConsoleSink, jsonLinesFormatter } from "@logtape/logtape";
await configure({
sinks: {
console: getConsoleSink({ formatter: jsonLinesFormatter }),
},
loggers: [
{ category: [], lowestLevel: "info", sinks: ["console"] },
],
});
Output:
{"@timestamp":"2026-01-15T10:30:00.000Z","level":"INFO","message":"User logged in","logger":"my-app","properties":{"userId":123}}
So far we've been sending everything to the console. That's fine for development, but in production you'll likely want logs to go elsewhere—or to multiple places at once.
Think about it: console output disappears when the process restarts. If your server crashes at 3 AM, you want those logs to be somewhere persistent. And when an error occurs, you might want it to show up in your error tracking service immediately, not just sit in a log file waiting for someone to grep through it.
This is where sinks come in. A sink is just a function that receives log records and does something with them. LogTape comes with several built-in sinks, and creating your own is trivial.
The simplest sink—outputs to the console:
import { getConsoleSink } from "@logtape/logtape";
const consoleSink = getConsoleSink();
For writing logs to files, install the @logtape/file package:
npm add @logtape/file
import { getFileSink, getRotatingFileSink } from "@logtape/file";
// Simple file sink
const fileSink = getFileSink("app.log");
// Rotating file sink (rotates when file reaches 10MB, keeps 5 old files)
const rotatingFileSink = getRotatingFileSink("app.log", {
maxSize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
});
Why rotating files? Without rotation, your log file grows indefinitely until it fills up the disk. With rotation, old logs are automatically archived and eventually deleted, keeping disk usage under control. This is especially important for long-running servers.
For production systems, you often want logs to go to specialized services that provide search, alerting, and visualization. LogTape has packages for popular services:
// OpenTelemetry (for observability platforms like Jaeger, Honeycomb, Datadog)
import { getOpenTelemetrySink } from "@logtape/otel";
// Sentry (for error tracking with stack traces and context)
import { getSentrySink } from "@logtape/sentry";
// AWS CloudWatch Logs (for AWS-native log aggregation)
import { getCloudWatchLogsSink } from "@logtape/cloudwatch-logs";
The OpenTelemetry sink is particularly useful if you're already using OpenTelemetry for tracing—your logs will automatically correlate with your traces, making debugging distributed systems much easier.
Here's where things get interesting. You can send different logs to different destinations based on their level or category:
await configure({
sinks: {
console: getConsoleSink(),
file: getFileSink("app.log"),
errors: getSentrySink(),
},
loggers: [
{ category: [], lowestLevel: "info", sinks: ["console", "file"] }, // Everything to console + file
{ category: [], lowestLevel: "error", sinks: ["errors"] }, // Errors also go to Sentry
],
});
Notice that a log record can go to multiple sinks. An error log in this configuration goes to the console, the file, and Sentry. This lets you have comprehensive local logs while also getting immediate alerts for critical issues.
Sometimes you need to send logs somewhere that doesn't have a pre-built sink. Maybe you have an internal logging service, or you want to send logs to a Slack channel, or store them in a database.
A sink is just a function that takes a LogRecord. That's it:
import type { Sink } from "@logtape/logtape";
const slackSink: Sink = (record) => {
// Only send errors and fatals to Slack
if (record.level === "error" || record.level === "fatal") {
fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `[${record.level.toUpperCase()}] ${record.message.join("")}`,
}),
});
}
};
The simplicity of sink functions means you can integrate LogTape with virtually any logging backend in just a few lines of code.
Here's a scenario you've probably encountered: a user reports an error, you check the logs, and you find a sea of interleaved messages from dozens of concurrent requests. Which log lines belong to the user's request? Good luck figuring that out.
This is where request tracing comes in. The idea is simple: assign a unique identifier to each request, and include that identifier in every log message produced while handling that request. Now you can filter your logs by request ID and see exactly what happened, in order, for that specific request.
LogTape supports this through contexts—a way to attach properties to log messages without passing them around explicitly.
The simplest approach is to create a logger with attached properties using .with():
function handleRequest(req: Request) {
const requestId = crypto.randomUUID();
const logger = getLogger(["my-app", "http"]).with({ requestId });
logger.info`Request received`; // Includes requestId automatically
processRequest(req, logger);
logger.info`Request completed`; // Also includes requestId
}
This works well when you're passing the logger around explicitly. But what about code that's deeper in your call stack? What about code in libraries that don't know about your logger instance?
This is where implicit contexts shine. Using withContext(), you can set properties that automatically appear in all log messages within a callback—even in nested function calls, async operations, and third-party libraries (as long as they use LogTape).
First, enable implicit contexts in your configuration:
import { configure, getConsoleSink } from "@logtape/logtape";
import { AsyncLocalStorage } from "node:async_hooks";
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: ["my-app"], lowestLevel: "debug", sinks: ["console"] },
],
contextLocalStorage: new AsyncLocalStorage(),
});
Then use withContext() in your request handler:
import { withContext, getLogger } from "@logtape/logtape";
function handleRequest(req: Request) {
const requestId = crypto.randomUUID();
return withContext({ requestId }, async () => {
// Every log message in this callback includes requestId—automatically
const logger = getLogger(["my-app"]);
logger.info`Processing request`;
await validateInput(req); // Logs here include requestId
await processBusinessLogic(req); // Logs here too
await saveToDatabase(req); // And here
logger.info`Request complete`;
});
}
The magic is that validateInput, processBusinessLogic, and saveToDatabase don't need to know anything about the request ID. They just call getLogger() and log normally, and the request ID appears in their logs automatically. This works even across async boundaries—the context follows the execution flow, not the call stack.
This is incredibly powerful for debugging. When something goes wrong, you can search for the request ID and see every log message from every module that was involved in handling that request.
Setting up request tracing manually can be tedious. LogTape has dedicated packages for popular frameworks that handle this automatically:
// Express
import { expressLogger } from "@logtape/express";
app.use(expressLogger());
// Fastify
import { getLogTapeFastifyLogger } from "@logtape/fastify";
const app = Fastify({ loggerInstance: getLogTapeFastifyLogger() });
// Hono
import { honoLogger } from "@logtape/hono";
app.use(honoLogger());
// Koa
import { koaLogger } from "@logtape/koa";
app.use(koaLogger());
These middlewares automatically generate request IDs, set up implicit contexts, and log request/response information. You get comprehensive request logging with a single line of code.
If you've ever used a library that spams your console with unwanted log messages, you know how annoying it can be. And if you've ever tried to add logging to your own library, you've faced a dilemma: should you use console.log() and annoy your users? Require them to install and configure a specific logging library? Or just... not log anything?
LogTape solves this with its library-first design. Libraries can add as much logging as they want, and it costs their users nothing unless they explicitly opt in.
The rule is simple: use getLogger() to log, but never call configure(). Configuration is the application's responsibility, not the library's.
// my-library/src/database.ts
import { getLogger } from "@logtape/logtape";
const logger = getLogger(["my-library", "database"]);
export function connect(url: string) {
logger.debug`Connecting to ${url}`;
// ... connection logic ...
logger.info`Connected successfully`;
}
What happens when someone uses your library?
If they haven't configured LogTape, nothing happens. The log calls are essentially no-ops—no output, no errors, no performance impact. Your library works exactly as if the logging code wasn't there.
If they have configured LogTape, they get full control. They can see your library's debug logs if they're troubleshooting an issue, or silence them entirely if they're not interested. They decide, not you.
This is fundamentally different from using console.log() in a library. With console.log(), your users have no choice—they see your logs whether they want to or not. With LogTape, you give them the power to decide.
You configure LogTape once in your entry point. This single configuration controls logging for your entire application, including any libraries that use LogTape:
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: ["my-app"], lowestLevel: "debug", sinks: ["console"] }, // Your app: verbose
{ category: ["my-library"], lowestLevel: "warning", sinks: ["console"] }, // Library: quiet
{ category: ["noisy-library"], lowestLevel: "fatal", sinks: [] }, // That one library: silent
],
});
This separation of concerns—libraries log, applications configure—makes for a much healthier ecosystem. Library authors can add detailed logging for debugging without worrying about annoying their users. Application developers can tune logging to their needs without digging through library code.
If your application already uses winston, Pino, or another logging library, you don't have to migrate everything at once. LogTape provides adapters that route LogTape logs to your existing logging setup:
import { install } from "@logtape/adaptor-winston";
import winston from "winston";
install(winston.createLogger({ /* your existing config */ }));
This is particularly useful when you want to use a library that uses LogTape, but you're not ready to switch your whole application over. The library's logs will flow through your existing winston (or Pino) configuration, and you can migrate gradually if you choose to.
Development and production have different needs. During development, you want verbose logs, pretty formatting, and immediate feedback. In production, you care about performance, reliability, and not leaking sensitive data. Here are some things to keep in mind.
By default, logging is synchronous—when you call logger.info(), the message is written to the sink before the function returns. This is fine for development, but in a high-throughput production environment, the I/O overhead of writing every log message can add up.
Non-blocking mode buffers log messages and writes them in the background:
const consoleSink = getConsoleSink({ nonBlocking: true });
const fileSink = getFileSink("app.log", { nonBlocking: true });
The tradeoff is that logs might be slightly delayed, and if your process crashes, some buffered logs might be lost. But for most production workloads, the performance benefit is worth it.
Logs have a way of ending up in unexpected places—log aggregation services, debugging sessions, support tickets. If you're logging request data, user information, or API responses, you might accidentally expose sensitive information like passwords, API keys, or personal data.
LogTape's @logtape/redaction package helps you catch these before they become a problem:
import {
redactByPattern,
EMAIL_ADDRESS_PATTERN,
CREDIT_CARD_NUMBER_PATTERN,
type RedactionPattern,
} from "@logtape/redaction";
import { defaultConsoleFormatter, configure, getConsoleSink } from "@logtape/logtape";
const BEARER_TOKEN_PATTERN: RedactionPattern = {
pattern: /Bearer [A-Za-z0-9\-._~+\/]+=*/g,
replacement: "[REDACTED]",
};
const formatter = redactByPattern(defaultConsoleFormatter, [
EMAIL_ADDRESS_PATTERN,
CREDIT_CARD_NUMBER_PATTERN,
BEARER_TOKEN_PATTERN,
]);
await configure({
sinks: {
console: getConsoleSink({ formatter }),
},
// ...
});
With this configuration, email addresses, credit card numbers, and bearer tokens are automatically replaced with [REDACTED] in your log output. The @logtape/redaction package comes with built-in patterns for common sensitive data types, and you can define custom patterns for anything else. It's not foolproof—you should still be mindful of what you log—but it provides a safety net.
See the redaction documentation for more patterns and field-based redaction.
Edge functions (Cloudflare Workers, Vercel Edge Functions, etc.) have a unique constraint: they can be terminated immediately after returning a response. If you have buffered logs that haven't been flushed yet, they'll be lost.
The solution is to explicitly flush logs before returning:
import { configure, dispose } from "@logtape/logtape";
export default {
async fetch(request, env, ctx) {
await configure({ /* ... */ });
// ... handle request ...
ctx.waitUntil(dispose()); // Flush logs before worker terminates
return new Response("OK");
},
};
The dispose() function flushes all buffered logs and cleans up resources. By passing it to ctx.waitUntil(), you ensure the worker stays alive long enough to finish writing logs, even after the response has been sent.
Logging isn't glamorous, but it's one of those things that makes a huge difference when something goes wrong. The setup I've described here—categories for organization, structured data for queryability, contexts for request tracing—isn't complicated, but it's a significant step up from scattered console.log statements.
LogTape isn't the only way to achieve this, but I've found it hits a nice sweet spot: powerful enough for production use, simple enough that you're not fighting the framework, and light enough that you don't feel guilty adding it to a library.
If you want to dig deeper, the LogTape documentation covers advanced topics like custom filters, the “fingers crossed” pattern for buffering debug logs until an error occurs, and more sink options. The GitHub repository is also a good place to report issues or see what's coming next.
Now go add some proper logging to that side project you've been meaning to clean up. Your future 2 AM self will thank you.
대한수학회 수학달력 2026년을 하루에 하나씩 짧게 설명해보는 타래
洪 民憙 (Hong Minhee) replied to the below article:
neo @neo@hackers.pub
新年首推,自制编程语言。
def look_up(name, expr)
if distance = @locals[expr]
@environ.get_at(distance, name)
else
@globals[name]
end
end
def truthy?(object)
return false if object.nil?
return false if object.is_a?(FalseClass)
true
end
def interpret(statements)
@neo 是在用 Ruby 實作新的程式語言嗎?我以前也曾用 Ruby 寫過一個簡單的 Lisp 直譯器,過程真的很有趣。祝您新年快樂!
"Crafting Interpreters" 真的想教会我写自己的编程语言。
洪 民憙 (Hong Minhee) shared the below article:
neo @neo@hackers.pub
新年首推,自制编程语言。
def look_up(name, expr)
if distance = @locals[expr]
@environ.get_at(distance, name)
else
@globals[name]
end
end
def truthy?(object)
return false if object.nil?
return false if object.is_a?(FalseClass)
true
end
def interpret(statements)
洪 民憙 (Hong Minhee) shared the below article:
소피아 @async3619@hackers.pub
X-Frame-Options 의 악몽에서 깨어나세요, 프록시 서버 개발기 X-Frame-Options? 그게 뭔가요? 우리가 아는 몇몇 대형 웹 서비스들(유튜브 등)은 보통의 경우 다른 웹 사이트에서 iframe 요소를 통해 임베딩 되는 것을 거부하지 않습니다. 다만, 몇몇 웹 서비스는 다른 웹 페이지에서 iframe 요소를 통해 표시되길 거부합니다. 제품 정책 및 보안상의 이유로 표시를 거부하는 목적이 있겠습니다.
이러한 니즈를 충족시킬 수 있는 HTTP 헤더가 X-Frame-Options 입니다. 이 헤더의 값을 SAMEORIGIN 내지는 DENY로 설정하면, 직관적인 값에 따라 알맞게 프레임 내 임베딩 가능 여부를 결정할 수 있습니다.

위 이미지에서 볼 수 있듯, 네이버는 X-Frame-Options 헤더를 명시함으로써 그 어떠한 출처에서도 프레임 내에 임베딩 되는 것을 거부한 상황입니다. #

웹 페이지 내에 다른 웹 페이지가 임베딩 되어 '미리보기' 처럼 제공되는 경험을 보신 적이 있으신가요? Omakase AI는 자사 프로덕트의 데모를 위와 같이 제공하고 있습니다. 캡쳐하여 실시간으로 전송되는 영상의 화면 위에, 자사 컨텐츠를 올려두어 실제 적용 시에 어떤 네러티브를 제공할지 데모 형식으로 보여줍니다.
문제는 이 모든 경험이 영상을 통해 진행 된다는 점 입니다. 영상 전송은 필연적으로 지연 시간이 존재할 수 밖에 없습니다. 여러분이 스크롤을 내리고, 클릭을 하는 등의 작은 인터렉션 하나가, 큰 지연 시간 뒤에 처리가 된다고 하면 유저는 답답하고 매끄럽지 않음을 느낄 것이고 이는 곧 이탈로 이어질 가능성이 있습니다. (구글은 비슷한 맥락에서 Core Web Vitals로써 INP를 설명하고 있습니다)
따라서 저는 영상을 보내는 나이브한 방법 이외의 유저의 브라우저에서 외부 웹 서비스를 표시할 좋은 방법을 찾아야 했고, 그것이 바로 프레임 내지는 iframe을 사용하는 방법 이었습니다. 이런 맥락에서 X-Frame-Options를 우회할 필요가 생긴 것 입니다.

우리는 정상적인 방법으로는 제 3자 출처의 요청을 가로채어 응답 데이터 및 헤더를 조작할 수 없음을 잘 알고 있습니다. 여기서 필요한게 중간자 (Man in the Middle) 입니다. 누군가를 클라이언트 - 원격지 서버 사이에 두어, 서로에게 오가는 요청과 응답을 수정하는 작업을 수행하도록 하는 것 입니다. 그렇게 하면, 둘은 각자 수신한 요청과 응답이 모두 원본인지, 수정한 것인지 알 수 있는 방법은 거의 없을 것 입니다.[1]

이 중간자 역할을 하는 프록시 서버를 중간에 두어 요청을 모두 프록시 서버를 거치도록 하는 방법을 이용하는 것 입니다. 간단하게는 X-Frame-Options 응답 헤더의 제거가 있을 것 입니다. 중간자가 X-Frame-Options 헤더를 제거함으로 응답을 수신하는 클라이언트 브라우저의 iframe 요소는 큰 문제 없이 내용을 표시할 수 있게 됩니다.
처음에는 그저 GET Query Parameter로 프록시 서버에게 어떤 원격 URL을 프록시 할 것인지 명시하도록 구현 했습니다. 예를 들면 다음과 같은 형식이 될 수 있겠습니다:
https://example.com/proxy?target=https://www.naver.com/...
보통의 경우에는 별 문제 없이 동작 했습니다만, 재앙은 그다지 먼 곳에 있지 않았습니다. 만약 다음과 같은 코드가 원격지 웹 서비스 코드에 있다고 해봅시다. 아래 코드는 무신사 웹 페이지의 빌드된 소스코드 입니다:
import {E as k0} from "./vendor/react-error-boundary.js";
import {b as o0, d as Wt} from "./vendor/react-router.js";
import {d as F0} from "./vendor/dayjs.js";
import {L as Qt} from "./vendor/lottie.js";
import "./vendor/scheduler.js";
import "./vendor/prop-types.js";
import "./vendor/react-fast-compare.js";
import "./vendor/invariant.js";
import "./vendor/shallowequal.js";
import "./vendor/@remix-run.js";
import "./vendor/tslib.js";
import "./vendor/@emotion.js";
import "./vendor/stylis.js";
import "./vendor/framer-motion.js";
import "./vendor/motion-utils.js";
import "./vendor/motion-dom.js";
무신사는 내부적으로 ESM을 사용해서, import 구문을 통해 필요한 에셋을 불러오는 코드를 사용중에 있습니다. 문제는 여기서 발생합니다. import 의 대상이 되는 소스코드가 상대 경로를 따르게 되어 아래와 같은 결과를 초래하게 됩니다.
https://example.com/proxy?target=https://www.naver.com/...
위와 같은 URL을 표시하고 있는 `iframe` 요소에서,
`import "./vendor/framer-motion.js"` 구문을 만난다면..
https://example.com/proxy/vendor/framer-motion.js 를 요청하게 됨.
이를 해결하기 위해, 프록시 된 대상 URL에 대한 개념의 도입이 필요 했습니다. 상대 경로 진입에도 안정적으로 작동할 수 있는 새로운 방식의 접근이 필요 했습니다. 저는
를 생각해 냈어야 했고, 그 결과는 이렇습니다. https://section.blog.naver.com/BlogHome.naver?directoryNo=0¤tPage=1&groupId=0 를 예시로 들면, 프록시화 (Proxified) 된 URL은 다음과 같은 것 입니다.
https://example.com/proxy/section/blog/naver/com/_/BlogHome.naver?...
URL hostname의 . 구분자를 /로 치환하고, 이후의 모든 pathname, search 등은 모두 _ 구분자 뒤로 넘김으로서 URL의 원형을 유지할 수 있게 됩니다. 추가적으로 상대 경로 접근에도 안전한 URL을 만들 수 있습니다.
우리는 상대 경로 문제를 해결하기 위해 URL을 프록시화 하는 방법을 사용했고, 이는 제대로 동작하는 듯 해보였습니다. 악몽은 React, Vue 등의 SPA 웹 앱을 프록시하여 표시하는 데에서 시작 되었습니다.
React, Vue 와 같은 프레임워크들은 History API 및 window.location 객체를 기반으로 한 Routing 기능을 제공하고 있습니다.[2] 이 말은, 결국엔 어떤 프레임워크가 되었든 저수준 빌트인 자바스크립트 API를 사용할 수 밖에 없다는 것을 의미 합니다. 그렇다면 직관적으로 생각 해봤을 때,
window(및globalThis) 객체의location속성의 값을 변경해주면 되지 않겠나?
라고 생각할 수 있습니다. 그러나 이는 불가능 합니다.

어떠한 이유 때문인지는 알 길이 없었지만, 자바스크립트는 그렇게 만만한 존재가 아니었습니다. 다른 좋은 방법을 찾아야 할 필요가 있었고, 결론에 도달하는 데에는 오랜 시간이 걸리지 않았습니다. 그것은 바로 원격지 웹 페이지에서 실행되는 모든 스크립트의 window.location 객체 접근을 감시하면 어떨지에 대한 아이디어 였습니다.
번들된 소스코드의 경우 대체적으로 다음과 같은 형식을 가지게 됩니다:
const l = window.location;
/* ... */ l.pathname /* ... */
여기서 우리는 변수 l이 window.location의 별칭인지 소스코드만 분석해서는 알기 매우 어렵습니다. 따라서, babel을 사용해서 소스코드를 AST로 분석하고, 모든 프로퍼티 접근을 특정 함수 호출로 변환하면, '특정 함수'에서 모든 것을 처리할 수 있으니 좋을 것 같다는 생각이 있었고, 실행에 옮겼습니다:
const l = window.location;
l.pathname;
// 위 코드는 아래와 같이 변환됨
const l = __internal_get__(window, 'location');
__internal_get__(l, 'pathname');
__internal_get__ 함수 내부에서 첫번째 인자가 window.location 과 동일한 인스턴스를 가지고 있는지 비교하거나, 두번째 인자인 프로퍼티 키를 비교해서 href 등의 값이라면, 원하는 값을 반환하도록 후킹 함수를 만들 수 있겠습니다.
function __internal_get__(owner, propertyKey) {
if (owner === window.location) {
return {
get href() { /* proxified 된 url을 기반으로 Router를 속이는 URL을 반환하는 로직 */ }
}
}
// ...
}
글에 열거한 내용 이외에도 정말 많은 기술이 사용 되었는데, 아주 재밌는 경험 이었습니다. 혹여나 이러한 비슷한 기능을 하는 기능을 개발할 일이 있으시다면, 도움이 됐으면 좋겠습니다.
여기서 거의 라는 표현을 사용한 이유는, 제 짧은 식견에서 보자면 비슷한 맥락에서 사용하는 기법으로 integrity 속성이 있을 수 있겠습니다. ↩︎
예를 들면, location.pathname을 읽어 현재 Route가 어떤 Route인지 감지하는 등의 동작이 있겠음. ↩︎
洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
So you need to send emails from your JavaScript application. Email remains one of the most essential features in web apps—welcome emails, password resets, notifications—but the ecosystem is fragmented. Nodemailer doesn't work on edge functions. Each provider has its own SDK. And if you're using Deno or Bun, good luck finding libraries that actually work.
This guide covers how to send emails across modern JavaScript runtimes using Upyo, a cross-runtime email library.
If you just want working code, here's the quickest path to sending an email:
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
const transport = new SmtpTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: "your-email@gmail.com",
pass: "your-app-password", // Not your regular password!
},
});
const message = createMessage({
from: "your-email@gmail.com",
to: "recipient@example.com",
subject: "Hello from my app!",
content: { text: "This is my first email." },
});
const receipt = await transport.send(message);
if (receipt.successful) {
console.log("Sent:", receipt.messageId);
} else {
console.log("Failed:", receipt.errorMessages);
}
Install with:
npm add @upyo/core @upyo/smtp
That's it. This exact code works on Node.js, Deno, and Bun. But if you want to understand what's happening and explore more powerful options, read on.
Let's start with the most accessible option: Gmail's SMTP server. It's free, requires no additional accounts, and works great for development and low-volume production use.
Gmail doesn't allow you to use your regular password for SMTP. You need to create an app-specific password:
Choose your runtime and package manager:
Node.js
npm add @upyo/core @upyo/smtp
# or: pnpm add @upyo/core @upyo/smtp
# or: yarn add @upyo/core @upyo/smtp
Deno
deno add jsr:@upyo/core jsr:@upyo/smtp
Bun
bun add @upyo/core @upyo/smtp
The same code works across all three runtimes—that's the beauty of Upyo.
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
// Create the transport (reuse this for multiple emails)
const transport = new SmtpTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: "your-email@gmail.com",
pass: "abcd efgh ijkl mnop", // Your app password
},
});
// Create and send a message
const message = createMessage({
from: "your-email@gmail.com",
to: "recipient@example.com",
subject: "Welcome to my app!",
content: {
text: "Thanks for signing up. We're excited to have you!",
html: "<h1>Welcome!</h1><p>Thanks for signing up. We're excited to have you!</p>",
},
});
const receipt = await transport.send(message);
if (receipt.successful) {
console.log("Email sent successfully! Message ID:", receipt.messageId);
} else {
console.error("Failed to send email:", receipt.errorMessages.join(", "));
}
// Don't forget to close connections when done
await transport.closeAllConnections();
Let me highlight a few important details:
secure: true with port 465: This establishes a TLS-encrypted connection from the start. Gmail requires encryption, so this combination is essential.text and html content: Always provide both. Some email clients don't render HTML, and spam filters look more favorably on emails with plain text alternatives.receipt pattern: Upyo uses discriminated unions for type-safe error handling. When receipt.successful is true, you get messageId. When it's false, you get errorMessages. This makes it impossible to forget error handling.await using (shown next) to handle this automatically.await using Managing resources manually is error-prone—what if an exception occurs before closeAllConnections() is called? Modern JavaScript (ES2024) solves this with explicit resource management.
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
// Transport is automatically disposed when it goes out of scope
await using transport = new SmtpTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: {
user: "your-email@gmail.com",
pass: "your-app-password",
},
});
const message = createMessage({
from: "your-email@gmail.com",
to: "recipient@example.com",
subject: "Hello!",
content: { text: "This email was sent with automatic cleanup!" },
});
await transport.send(message);
// No need to call `closeAllConnections()` - it happens automatically!
The await using keyword tells JavaScript to call the transport's cleanup method when execution leaves this scope—even if an error is thrown. This pattern is similar to Python's with statement or C#'s using block. It's supported in Node.js 22+, Deno, and Bun.
What if your environment doesn't support await using?
For older Node.js versions or environments without ES2024 support, use try/finally to ensure cleanup:
const transport = new SmtpTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: { user: "your-email@gmail.com", pass: "your-app-password" },
});
try {
await transport.send(message);
} finally {
await transport.closeAllConnections();
}
This achieves the same result—cleanup happens whether the send succeeds or throws an error.
Real-world emails often need more than plain text.
Inline images appear directly in the email body rather than as downloadable attachments. The trick is to reference them using a Content-ID (CID) URL scheme.
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
import { readFile } from "node:fs/promises";
await using transport = new SmtpTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: { user: "your-email@gmail.com", pass: "your-app-password" },
});
// Read your logo file
const logoContent = await readFile("./assets/logo.png");
const message = createMessage({
from: "your-email@gmail.com",
to: "customer@example.com",
subject: "Your order confirmation",
content: {
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<img src="cid:company-logo" alt="Company Logo" style="width: 150px;">
<h1>Order Confirmed!</h1>
<p>Thank you for your purchase. Your order #12345 has been confirmed.</p>
</div>
`,
text: "Order Confirmed! Thank you for your purchase. Your order #12345 has been confirmed.",
},
attachments: [
{
filename: "logo.png",
content: logoContent,
contentType: "image/png",
contentId: "company-logo", // Referenced as cid:company-logo in HTML
inline: true,
},
],
});
await transport.send(message);
Key points about inline images:
contentId: This is the identifier you use in the HTML's src="cid:..." attribute. It can be any unique string.inline: true: This tells the email client to display the image within the message body, not as a separate attachment.alt text: Some email clients block images by default, so the alt text ensures your message is still understandable.For regular attachments that recipients can download, use the standard File API. This approach works across all JavaScript runtimes.
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
import { readFile } from "node:fs/promises";
await using transport = new SmtpTransport({
host: "smtp.gmail.com",
port: 465,
secure: true,
auth: { user: "your-email@gmail.com", pass: "your-app-password" },
});
// Read files to attach
const invoicePdf = await readFile("./invoices/invoice-2024-001.pdf");
const reportXlsx = await readFile("./reports/monthly-report.xlsx");
const message = createMessage({
from: "billing@yourcompany.com",
to: "client@example.com",
cc: "accounting@yourcompany.com",
subject: "Invoice #2024-001",
content: {
text: "Please find your invoice and monthly report attached.",
},
attachments: [
new File([invoicePdf], "invoice-2024-001.pdf", { type: "application/pdf" }),
new File([reportXlsx], "monthly-report.xlsx", {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}),
],
priority: "high", // Sets email priority headers
});
await transport.send(message);
A few notes on attachments:
type helps email clients display the right icon and open the file with the appropriate application.priority: "high": This sets the X-Priority header, which some email clients use to highlight important messages. Use it sparingly—overuse can trigger spam filters.Email supports several recipient types, each with different visibility rules:
import { createMessage } from "@upyo/core";
const message = createMessage({
from: { name: "Support Team", address: "support@yourcompany.com" },
to: [
"primary-recipient@example.com",
{ name: "John Smith", address: "john@example.com" },
],
cc: "manager@yourcompany.com",
bcc: ["archive@yourcompany.com", "compliance@yourcompany.com"],
replyTo: "no-reply@yourcompany.com",
subject: "Your support ticket has been updated",
content: { text: "We've responded to your ticket #5678." },
});
Understanding recipient types:
to: Primary recipients. Everyone can see who else is in this field.cc (Carbon Copy): Secondary recipients. Visible to all recipients—use for people who should be informed but aren't the primary audience.bcc (Blind Carbon Copy): Hidden recipients. No one can see BCC addresses—useful for archiving or compliance without revealing internal processes.replyTo: Where replies should go. Useful when sending from a no-reply address but wanting responses to reach a real inbox.You can specify addresses as simple strings ("email@example.com") or as objects with name and address properties for display names.
Gmail SMTP is great for getting started, but for production applications, you'll want a dedicated email service provider. Here's why:
The best part? With Upyo, switching providers requires minimal code changes—just swap the transport.
Resend is a newer email service with an excellent developer experience.
npm add @upyo/resend
import { createMessage } from "@upyo/core";
import { ResendTransport } from "@upyo/resend";
const transport = new ResendTransport({
apiKey: process.env.RESEND_API_KEY!,
});
const message = createMessage({
from: "hello@yourdomain.com", // Must be verified in Resend
to: "user@example.com",
subject: "Welcome aboard!",
content: {
text: "Thanks for joining us!",
html: "<h1>Welcome!</h1><p>Thanks for joining us!</p>",
},
tags: ["onboarding", "welcome"], // For analytics
});
const receipt = await transport.send(message);
if (receipt.successful) {
console.log("Sent via Resend:", receipt.messageId);
}
Notice how similar this looks to the SMTP example? The only differences are the import and the transport configuration. Your message creation and sending logic stays exactly the same—that's Upyo's transport abstraction at work.
SendGrid is a popular choice for high-volume senders, offering advanced analytics, template management, and a generous free tier.
SendGrid is a popular choice for high-volume senders.
npm add @upyo/sendgrid
import { createMessage } from "@upyo/core";
import { SendGridTransport } from "@upyo/sendgrid";
const transport = new SendGridTransport({
apiKey: process.env.SENDGRID_API_KEY!,
clickTracking: true,
openTracking: true,
});
const message = createMessage({
from: "notifications@yourdomain.com",
to: "user@example.com",
subject: "Your weekly digest",
content: {
html: "<h1>This Week's Highlights</h1><p>Here's what you missed...</p>",
text: "This Week's Highlights\n\nHere's what you missed...",
},
tags: ["digest", "weekly"],
});
await transport.send(message);
Mailgun offers robust infrastructure with strong EU support—important if you need GDPR-compliant data residency.
npm add @upyo/mailgun
import { createMessage } from "@upyo/core";
import { MailgunTransport } from "@upyo/mailgun";
const transport = new MailgunTransport({
apiKey: process.env.MAILGUN_API_KEY!,
domain: "mg.yourdomain.com",
region: "eu", // or "us"
});
const message = createMessage({
from: "team@yourdomain.com",
to: "user@example.com",
subject: "Important update",
content: { text: "We have some news to share..." },
});
await transport.send(message);
Amazon SES is incredibly affordable—about $0.10 per 1,000 emails. If you're already in the AWS ecosystem, it integrates seamlessly with IAM, CloudWatch, and other services.
npm add @upyo/ses
import { createMessage } from "@upyo/core";
import { SesTransport } from "@upyo/ses";
const transport = new SesTransport({
authentication: {
type: "credentials",
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
region: "us-east-1",
configurationSetName: "my-config-set", // Optional: for tracking
});
const message = createMessage({
from: "alerts@yourdomain.com",
to: "admin@example.com",
subject: "System alert",
content: { text: "CPU usage exceeded 90%" },
priority: "high",
});
await transport.send(message);
Here's where many email solutions fall short. Edge functions (Cloudflare Workers, Vercel Edge, Deno Deploy) run in a restricted environment—they can't open raw TCP connections, which means SMTP is not an option.
You must use an HTTP-based transport like Resend, SendGrid, Mailgun, or Amazon SES. The good news? Your code barely changes.
// src/index.ts
import { createMessage } from "@upyo/core";
import { ResendTransport } from "@upyo/resend";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const transport = new ResendTransport({
apiKey: env.RESEND_API_KEY,
});
const message = createMessage({
from: "noreply@yourdomain.com",
to: "user@example.com",
subject: "Request received",
content: { text: "We got your request and are processing it." },
});
const receipt = await transport.send(message);
if (receipt.successful) {
return new Response(`Email sent: ${receipt.messageId}`);
} else {
return new Response(`Failed: ${receipt.errorMessages.join(", ")}`, {
status: 500,
});
}
},
};
interface Env {
RESEND_API_KEY: string;
}
// app/api/send-email/route.ts
import { createMessage } from "@upyo/core";
import { SendGridTransport } from "@upyo/sendgrid";
export const runtime = "edge";
export async function POST(request: Request) {
const { to, subject, body } = await request.json();
const transport = new SendGridTransport({
apiKey: process.env.SENDGRID_API_KEY!,
});
const message = createMessage({
from: "app@yourdomain.com",
to,
subject,
content: { text: body },
});
const receipt = await transport.send(message);
if (receipt.successful) {
return Response.json({ success: true, messageId: receipt.messageId });
} else {
return Response.json(
{ success: false, errors: receipt.errorMessages },
{ status: 500 }
);
}
}
// main.ts
import { createMessage } from "jsr:@upyo/core";
import { MailgunTransport } from "jsr:@upyo/mailgun";
Deno.serve(async (request: Request) => {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const { to, subject, body } = await request.json();
const transport = new MailgunTransport({
apiKey: Deno.env.get("MAILGUN_API_KEY")!,
domain: Deno.env.get("MAILGUN_DOMAIN")!,
region: "us",
});
const message = createMessage({
from: "noreply@yourdomain.com",
to,
subject,
content: { text: body },
});
const receipt = await transport.send(message);
if (receipt.successful) {
return Response.json({ success: true, messageId: receipt.messageId });
} else {
return Response.json(
{ success: false, errors: receipt.errorMessages },
{ status: 500 }
);
}
});
Ever wonder why some emails land in spam while others don't? Email authentication plays a huge role. DKIM (DomainKeys Identified Mail) is one of the key mechanisms—it lets you digitally sign your emails so recipients can verify they actually came from your domain and weren't tampered with in transit.
Without DKIM:
First, generate a DKIM key pair. You can use OpenSSL:
# Generate a 2048-bit RSA private key
openssl genrsa -out dkim-private.pem 2048
# Extract the public key
openssl rsa -in dkim-private.pem -pubout -out dkim-public.pem
Then configure your SMTP transport:
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
import { readFileSync } from "node:fs";
const transport = new SmtpTransport({
host: "smtp.example.com",
port: 587,
secure: false,
auth: {
user: "user@yourdomain.com",
pass: "password",
},
dkim: {
signatures: [
{
signingDomain: "yourdomain.com",
selector: "mail", // Creates DNS record at mail._domainkey.yourdomain.com
privateKey: readFileSync("./dkim-private.pem", "utf8"),
algorithm: "rsa-sha256", // or "ed25519-sha256" for shorter keys
},
],
},
});
The key configuration options:
signingDomain: Must match your email's "From" domainselector: An arbitrary name that becomes part of your DNS record (e.g., mail creates a record at mail._domainkey.yourdomain.com)algorithm: RSA-SHA256 is widely supported; Ed25519-SHA256 offers shorter keys (see below)Add a TXT record to your domain's DNS:
mail._domainkey (or mail._domainkey.yourdomain.com depending on your DNS provider)v=DKIM1; k=rsa; p=YOUR_PUBLIC_KEY_HEREExtract the public key value (remove headers, footers, and newlines from the .pem file):
cat dkim-public.pem | grep -v "^-" | tr -d '\n'
RSA-2048 keys are long—about 400 characters for the public key. This can be problematic because DNS TXT records have size limits, and some DNS providers struggle with long records.
Ed25519 provides equivalent security with much shorter keys (around 44 characters). If your email infrastructure supports it, Ed25519 is the modern choice.
# Generate Ed25519 key pair
openssl genpkey -algorithm ed25519 -out dkim-ed25519-private.pem
openssl pkey -in dkim-ed25519-private.pem -pubout -out dkim-ed25519-public.pem
const transport = new SmtpTransport({
// ... other config
dkim: {
signatures: [
{
signingDomain: "yourdomain.com",
selector: "mail2025",
privateKey: readFileSync("./dkim-ed25519-private.pem", "utf8"),
algorithm: "ed25519-sha256",
},
],
},
});
When you need to send emails to many recipients—newsletters, notifications, marketing campaigns—you have two approaches:
send() // ❌ Don't do this for bulk sending
for (const subscriber of subscribers) {
await transport.send(createMessage({
from: "newsletter@example.com",
to: subscriber.email,
subject: "Weekly update",
content: { text: "..." },
}));
}
This works, but it's inefficient:
send() call waits for the previous one to completesendMany() The sendMany() method is designed for bulk operations:
import { createMessage } from "@upyo/core";
import { ResendTransport } from "@upyo/resend";
const transport = new ResendTransport({
apiKey: process.env.RESEND_API_KEY!,
});
const subscribers = [
{ email: "alice@example.com", name: "Alice" },
{ email: "bob@example.com", name: "Bob" },
{ email: "charlie@example.com", name: "Charlie" },
// ... potentially thousands more
];
// Create personalized messages
const messages = subscribers.map((subscriber) =>
createMessage({
from: "newsletter@yourdomain.com",
to: subscriber.email,
subject: "Your weekly digest",
content: {
html: `<h1>Hi ${subscriber.name}!</h1><p>Here's what's new this week...</p>`,
text: `Hi ${subscriber.name}!\n\nHere's what's new this week...`,
},
tags: ["newsletter", "weekly"],
})
);
// Send all messages efficiently
let successCount = 0;
let failureCount = 0;
for await (const receipt of transport.sendMany(messages)) {
if (receipt.successful) {
successCount++;
} else {
failureCount++;
console.error("Failed:", receipt.errorMessages.join(", "));
}
}
console.log(`Sent: ${successCount}, Failed: ${failureCount}`);
Why sendMany() is better:
const totalMessages = messages.length;
let processed = 0;
for await (const receipt of transport.sendMany(messages)) {
processed++;
if (processed % 100 === 0) {
console.log(`Progress: ${processed}/${totalMessages} (${Math.round((processed / totalMessages) * 100)}%)`);
}
if (!receipt.successful) {
console.error(`Message ${processed} failed:`, receipt.errorMessages);
}
}
console.log("Batch complete!");
send() vs sendMany() | Scenario | Use |
|---|---|
| Single transactional email (welcome, password reset) | send() |
| A few emails (under 10) | send() in a loop is fine |
| Newsletters, bulk notifications | sendMany() |
| Batch processing from a queue | sendMany() |
Upyo includes a MockTransport for testing:
import { createMessage } from "@upyo/core";
import { MockTransport } from "@upyo/mock";
import assert from "node:assert";
import { describe, it, beforeEach } from "node:test";
describe("Email functionality", () => {
let transport: MockTransport;
beforeEach(() => {
transport = new MockTransport();
});
it("should send welcome email after registration", async () => {
// Your application code would call this
const message = createMessage({
from: "welcome@yourapp.com",
to: "newuser@example.com",
subject: "Welcome to our app!",
content: { text: "Thanks for signing up!" },
});
const receipt = await transport.send(message);
// Assertions
assert.strictEqual(receipt.successful, true);
assert.strictEqual(transport.getSentMessagesCount(), 1);
const sentMessage = transport.getLastSentMessage();
assert.strictEqual(sentMessage?.subject, "Welcome to our app!");
assert.strictEqual(sentMessage?.recipients[0].address, "newuser@example.com");
});
it("should handle email failures gracefully", async () => {
// Simulate a failure
transport.setNextResponse({
successful: false,
errorMessages: ["Invalid recipient address"],
});
const message = createMessage({
from: "test@yourapp.com",
to: "invalid-email",
subject: "Test",
content: { text: "Test" },
});
const receipt = await transport.send(message);
assert.strictEqual(receipt.successful, false);
assert.ok(receipt.errorMessages.includes("Invalid recipient address"));
});
});
The key testing methods:
getSentMessagesCount(): How many emails were “sent”getLastSentMessage(): The most recent messagegetSentMessages(): All messages as an arraysetNextResponse(): Force the next send to succeed or fail with specific errorsimport { MockTransport } from "@upyo/mock";
// Simulate network delays
const slowTransport = new MockTransport({
delay: 500, // 500ms delay per email
});
// Simulate random failures (10% failure rate)
const unreliableTransport = new MockTransport({
failureRate: 0.1,
});
// Simulate variable latency
const realisticTransport = new MockTransport({
randomDelayRange: { min: 100, max: 500 },
});
import { MockTransport } from "@upyo/mock";
const transport = new MockTransport();
// Start your async operation that sends emails
startUserRegistration("newuser@example.com");
// Wait for the expected emails to be sent
await transport.waitForMessageCount(2, 5000); // Wait for 2 emails, 5s timeout
// Or wait for a specific email
const welcomeEmail = await transport.waitForMessage(
(msg) => msg.subject.includes("Welcome"),
3000
);
console.log("Welcome email was sent:", welcomeEmail.subject);
PoolTransport What happens if your email provider goes down? For mission-critical applications, you need redundancy. PoolTransport combines multiple providers with automatic failover—if one fails, it tries the next.
import { PoolTransport } from "@upyo/pool";
import { ResendTransport } from "@upyo/resend";
import { SendGridTransport } from "@upyo/sendgrid";
import { MailgunTransport } from "@upyo/mailgun";
import { createMessage } from "@upyo/core";
// Create multiple transports
const resend = new ResendTransport({ apiKey: process.env.RESEND_API_KEY! });
const sendgrid = new SendGridTransport({ apiKey: process.env.SENDGRID_API_KEY! });
const mailgun = new MailgunTransport({
apiKey: process.env.MAILGUN_API_KEY!,
domain: "mg.yourdomain.com",
});
// Combine them with priority-based failover
const transport = new PoolTransport({
strategy: "priority",
transports: [
{ transport: resend, priority: 100 }, // Try first
{ transport: sendgrid, priority: 50 }, // Fallback
{ transport: mailgun, priority: 10 }, // Last resort
],
maxRetries: 3,
});
const message = createMessage({
from: "critical@yourdomain.com",
to: "admin@example.com",
subject: "Critical alert",
content: { text: "This email will try multiple providers if needed." },
});
const receipt = await transport.send(message);
// Automatically tries Resend first, then SendGrid, then Mailgun if others fail
The priority values determine the order—higher numbers are tried first. If Resend fails (network error, rate limit, etc.), the pool automatically retries with SendGrid, then Mailgun.
For more advanced routing strategies (weighted distribution, content-based routing), see the pool transport documentation.
In production, you'll want to track email metrics: send rates, failure rates, latency. Upyo integrates with OpenTelemetry:
import { createOpenTelemetryTransport } from "@upyo/opentelemetry";
import { SmtpTransport } from "@upyo/smtp";
const baseTransport = new SmtpTransport({
host: "smtp.example.com",
port: 587,
auth: { user: "user", pass: "password" },
});
const transport = createOpenTelemetryTransport(baseTransport, {
serviceName: "email-service",
tracing: { enabled: true },
metrics: { enabled: true },
});
// Now all email operations generate traces and metrics automatically
await transport.send(message);
This gives you:
See the OpenTelemetry documentation for details.
| Scenario | Recommended Transport |
|---|---|
| Development/testing | Gmail SMTP or MockTransport |
| Small production app | Resend or SendGrid |
| High volume (100k+/month) | Amazon SES |
| Edge functions | Resend, SendGrid, or Mailgun |
| Self-hosted infrastructure | SMTP with DKIM |
| Mission-critical | PoolTransport with failover |
| EU data residency | Mailgun (EU region) or self-hosted |
This guide covered the most popular transports, but Upyo also supports:
And you can always create a custom transport for any email service not yet supported.
Have questions or feedback? Feel free to open an issue.
What's been your biggest pain point when sending emails from JavaScript? Let me know in the comments—I'm curious what challenges others have run into.
Upyo (pronounced /oo-pyo/) comes from the Korean word 郵票, meaning “postage stamp.”
洪 民憙 (Hong Minhee) shared the below article:
Jaeyeol Lee @kodingwarrior@hackers.pub
이번 분기는 그렇게 기대는 많이 하지는 않았는데, 이것저것 벌려놓은 일들은 있었어서 이거라도 수습은 해야겠다라는 마음가짐은 했던 것 같다. 벌려놓은 일이 많았던 만큼 역시나 업보를 청산하느라 정신이 없는 나날의 연속이었고, 그러면서도 나름 만족스러운 하루를 보내고 있었다. 그러면서도 여러가지 좋은 소식도 생겼다. 기대했던 것들을 상회하는 좋은 기회를 누리고 있어서 요즘은 매일마다 도파민이 넘치고 있는 삶을 살고 있다. 자세한 얘기는...... 후술하도록 하겠다.
이번에도 업보청산하느라 정신없는 나날을 보내고 있었고, 그러면서도 중요한 이벤트도 몇 가지 일어났다.
크게 보자면 뭔가 좀 적은 것 같긴 한데, 좋은 일들이 가득한 나날이었다. 지금 당장은 회사 쪽 일에 전념하고 싶기도 하고, 향후에 근황 공유를 하더라도 좀 심심할 수 있겠다는 생각은 들고 있다. 한 4년 전 쯤, 리걸테크 분야 회사에서 일했던 것과 비슷한 심정을 느끼고 있는데, 지금 회사에 있는 매 순간순간이 성장하고 있다는 느낌이 들고 있고, 긍정적인 의미로 한계에 도전하게 되는 일들이 많다. 회사의 성장이 곧 나의 성장이라는 확신이 들게 하는 환경이고, 회사 일 자체가 정말 재밌기도 해서 스스로가 워커홀릭이 되어가는게 느껴진다.
참...... 인생 살다가도 모르는 일이다. 3년 내내 어디에다가 말하기도 어려운 힘든 나날을 보내기만 했는데, 문득 좋은 기회가 찾아와서 이렇게 행복하게 살아도 되는건가 싶은 생각을 늘상 하고 있다. 뭐, 지금은 즐겨야지 싶다.
그리고, 어쩌다보니 좋은 기회가 생겨서 책을 쓰게 되었는데 조만간 소식을 공유할 수 있으면 좋겠다.
말 그대로 오늘내일하는 삶을 살아왔기 때문에, 회고라는 것도 딱히 생각도 없었고, (있는 그대로 말하면 어지간하면 경악할만한) 열악한 환경에서 살아왔기 때문에 더 나은 미래를 가정한다는 것 자체가 성립이 되지 않았는데, "이제는 진짜 바뀔 수 있다" 라는 확신이 들고 있다. 거의 3-4년을 무기력하고 우울한 삶, 악순환의 고리 속에서 살아왔는데 이제는 좀 달라졌다.
올해에 PyCon JP에 참여한 이후로, 해외 컨퍼런스에 좀 더 많이 참여해보고 싶은 생각이 들었다. PyCon JP에서도 좋은 인연들을 알아갈 수 있었고, 좋은 기회를 얻을뻔도 했었다. 심지어, FastAPI 메인테이너인 tiangolo님과도 같이 사진을 찍을 수 있었다. 이런 맛으로 해외 컨퍼런스에 참여하는 것일까? 올해에 참여한 해외 컨퍼런스는 PyCon JP 밖에 없었지만, 내년에는 홍콩에도 가보고 필리핀에도 가보고 PyCon JP는 당연히 참여를 하는거고, 특히 일본에서 열리는 VimConf는 이번에는 반드시 참가하고 말 것이라는 확고한 의지가 있다.
내년에는 그래도, 내가 생각하는 것을 자유롭게 표현할 수 있는 수준으로는 능숙해질 필요는 있어야겠어서 전화영어는 알아보고 있다. 지금 일하고 있는 회사가 현지 직원을 채용하고 있기도 해서, 이제는 영어로 회화하는건 피할 수가 없는 현실이 되었다. 그리고.... 돈 모아서 PyCon US 갈 생각도 이미 하고 있다.
Python 생태계에 언젠가는 기여를 해야겠다고 생각만 해왔던 것 같다. 내가 애정을 가져왔고 지금도 애정을 가지는 언어 생태계에서 밥벌이를 시작한 만큼, 이제는 좀 기여할 수 있는 여지도 명분도 충분히 생겼다. 여러 생태계를 관찰하면서 느낀 점이 있는데, 개발 커뮤니티 생태계가 성숙해지려면 여러가지 요소가 필요하다. 그나마 직관적으로 와닿는 예시를 들자면.. (1) 좋은 일자리, (2) 보고 배울 수 있는 멘토, (3) 질적인 네트워킹, (4) 포용적인 분위기, (5) 상호간 영감을 주면서 시너지를 낼 수 있는 환경 정도 되겠다. 그렇지 않은 환경을 수차례 관찰을 해온 적이 있기 때문에 더욱 실감하고 있는 부분이기도 하다.
커뮤니티를 직접 빌드하는건 이미 벌려놓은게 있으니 지금 하는거라도 잘해야겠다 치더라도, 이미 이렇게 자리를 잡게 된 이상 (내가 하고 있는 일을 더욱 잘해서) Python 생태계의 양질의 일자리를 더 확보하는데 기여를 해야겠고, 어딜 내놔도 부끄러운 사람이 되지 않기 위해 더욱 분발해서 Python 생태계의 보고 배울 수 있는 멘토가 되고 싶고, 필요하다면 여기저기 발표도 해보고 싶다. 2026년 목표는 해외 Python 관련 행사에서 영어로 발표해본다? 정도 될 것 같다.
개발자로서 기술만 잘한다고 해서 좋은 결과를 만들 수 있는 건 아니다. 주변 지인분들의 표현을 빌리자면, 개발자도 어떻게 보면 회사의 일원이다. 스스로는 회사의 일원으로서, 특히 어느 정도 연차가 있는 사람으로서, 회사의 비즈니스적인 성장에 기여할 의무는 어느 정도 있다고 생각하는 편이다.
현재 회사에서 일하면서 AI 도구를 활용하거나 워크플로우를 개선하는 과정을 통해, 기술이 어떻게 비즈니스 임팩트를 만드는지 배워가고 있다. 앞으로는 "이 기능이 사용자에게 어떤 가치를 줄까" "이 기술 선택이 비용과 시간 면에서 타당할까"라는 질문을 더 자주 던질 수 있는 엔지니어가 되고 싶다. 좋은 제품은 좋은 코드와 좋은 비즈니스 센스의 조화에서 나온다.
서른이 넘은지 시간이 어느 정도는 지난 지금 생각해보면, 스스로를 돌아볼 기회가 없었다. 그냥 말 그대로 미래라는게 없었다고 생각되었기 때문이다. 좋은 동료와 일하는 경험의 부재, 진지하게 제품 개발에 임할 수 있는 기회의 부재, 대외활동은 적극적으로 해오긴 했지만 결과적으로는 본업의 불만족으로 인한 우울감 때문에 일상을 제대로 유지하기도 어려웠다. 인생이 이따위인데 뭔 회고인가? 라는 꼬인 생각도 했었던 것 같다. 이제는 좀 확실하게 달라졌다.
진심박치기 모드일 때는 다르다고 확신은 하고 있었고, 이젠 진심박치기 모드일 수 있는 기회가 왔다. 해볼 수 있는 것들은 다 해보고 싶고, 본업에 충실한 나로서 제대로 인정받고 싶다. 본업으로 같이 일하고 싶은 동료로서, 사람 대 사람으로서 안심할 수 있는 사람으로서. 내가 인정하는 내가 인정받고 싶은 사람들과 함께 하는 시간이 많아지다보니, 사고의 흐름이 자연스럽게 변하게 된 것 같다.
이번엔 진짜 최선을 다해서 살아볼까싶다.
strcpy도 사용 금지
------------------------------
- cURL 프로젝트가 기존에 strncpy()를 제거한 데 이어, 이제 *strcpy()도 코드베이스에서 완전히 금지* 함
- strcpy()는 API가 단순하지만 *버퍼 크기 검증이 분리될 위험* 이 있어, 장기 유지보수 시 안전하지 않음
- 이를 대신해 *curlx_strcopy()* 라는 새 함수가 도입되어, 대상 버퍼 크기와 문자열 …
------------------------------
https://news.hada.io/topic?id=25474&utm_source=googlechat&utm_medium=bot&utm_campaign=1834
2026년 병오년 새해를 맞아 식탁보 1.16.0 버전을 출시했습니다. 이번 버전에서는 폴더 마운트 기능, 그리고 백그라운드 비동기 다운로드를 구현하여 이전보다 최대 30~40% 이상 빨라진 환경 구축 속도를 달성했습니다.
코딩 AI 어시스턴트의 도움을 받아 계속해서 빠른 출시와 적극적인 기능 반영을 이어 나가도록 하겠습니다. 많은 공유와 후원을 계속 부탁드리겠습니다!
#식탁보 #인터넷뱅킹 #NPKI #보안 #플러그인 #공동인증서
https://github.com/yourtablecloth/TableCloth/releases/tag/v1.16.0
2026年에도 새해 福 많이 받으세요!
연말을 맞이하여 주요 저장소들의 AGENTS.md/CLAUDE.md 문서 업데이트 중…
VCS와 패키지 매니저가 통합되어야 한단 얘기를 했었는데, shadcn의 인기가 그런 방향에 대한 지지를 간접적으로 보여주고 있다. shadcn은 UI 라이브러리를 만들어봤자 어차피 고쳐쓰는 경우가 많아서 나왔다. 문제는 기존의 npm 패키지 같은 것들은 받는건 쉬운데 그다음에 고치는게 열불터지는 것이다.
오늘은 고등학교 때 만들다 말았던 라이브러리를 얼추 마무리 지었다. Claude랑 같이 스펙 문서 만들고, 이를 바탕으로 작업하도록 했다. 당시에 하라는 FAT 덤프 파싱은 안 하고 Python에서 편하게 파싱하고 싶어서 만들기 시작했던 라이브러리고 취업하게 되서 이후로 안 봤었는데, Claude Code 덕분에 이제는 보내줄(?) 수 있을 것 같다.
근데 당시에 파서 콤비네이터를 몰랐어서 그랬고 지금은 파서 콤비네이터를 쓸 것 같다. 그리고 오늘 작업하면서 보게 된 건데 비슷한(?) 느낌으로 construct[1] 라는 라이브러리의 존재도 알게 되었다.
이제 Python을 잘 안 쓰고, 원래 시작점이었던 포렌식도 안 하니까 쓸 일은 없겠지만 그래도 당시 2019년 Hacktoberfest 시기에 필드 추가 기여도 받아보고 좋은 기억의 라이브러리였다.
https://github.com/moreal/pystructs/
@hongminhee洪 民憙 (Hong Minhee) OpenCode 는 어떤점이 좋으셨어요?
@jLEE Jaeyoung OpenCode가 특별히 Claude Code보다 좋은 점은 아직 크게 못 느낀 것 같아요. 아, LSP를 지원하다는 것 정도…? 근데 Claude Code도 최근에 LSP 지원이 들어왔다고 듣긴 해서 (LSP를 쓴다는 느낌을 받은 적은 없지만요), 이 부분은 좀 애매하네요. 다만 OpenCode가 오픈 소스인데다 확장성이 좋아서 그 부분을 기대하고 있어요. 아, 그리고 세션 중간에 모델(아예 다른 업체의 모델로도)을 바꿀 수 있는 것도 좋은 것 같습니다.
Released Vertana 0.1.0—agentic #translation for #TypeScript/#JavaScript.
Instead of just passing text to an #LLM, it autonomously gathers context from linked pages and references to produce translations that actually understand what they're #translating.
알고리즘을 각 잡고 공부해 본 적이 없습니다. 기본부터 천천히 다져가며 공부하고 싶은데, 좋은 책, 강의 등이 있을까요?
@hongminhee洪 民憙 (Hong Minhee) openrouter(AI 모델 여러개 쓸 수 있는 인터페이스 제공하는 곳)랑 연결해서, AI 모델 여러개로 에이전트 여러개 만들어서 각자 역할분담하게 하는 전략도 있더라고요.
저도 돈 많으면 시도해보고 싶습니다...
@akastoot악하 그런 게 OpenCode만 가지고도 되는 것 같더라고요!
평소에는 거의 Claude Code만 쓰는데, 오늘은 일부러 OpenCode를 써봤다. OpenCode에서 Claude 4.5 Opus도 써 보고, Gemini 3 Pro도 써 보고, GPT-5.2도 써 봤다. 일단 “말귀”를 잘 알아 듣는다는 점에서는 Claude 4.5 Opus와 GPT-5.2가 괜찮았던 것 같고, Gemini 3 Pro는 여러 측면에서 내 기대와는 좀 다르게 돌아갔던 것 같다. 그리고 문서 작업을 시켜보면 알 수 있는데, Gemini 3 Pro는 글을 상대적으로 못 쓴다. 이래저래 Gemini 3 Pro는 앞으로도 안 쓰게 될 듯.
OpenCode 자체는 잘 만들었다고 느꼈다. Claude Max 플랜을 그대로 쓸 수 있어서, 당분간 Claude Code 대신 메인 에이전트로 사용해 볼 예정.
今年の10月から地域の市民向けシビックテック講座に参加してるのですが、そこでたまたま挙がったテーマが個人的にとても良く、だんだん魅了されています。
ちなみにテーマはを消滅危機言語である沖縄語(沖縄方言)を残したい」で、中学生の頃になりたかった言語学者の夢を大変久々に思い出したのでした。
講座修了後も継続して取り組みたいなと思い始めたので、その気持ちに従って図書館通いの日々が始まりました。
言語を扱うので、LLMを調べたり弄ったりする機会もできて、なんだか毎日楽しいよ。
#코스모슬라이드 업로드 페이지 그냥 수제 프론트 디자인 해야 하나... 이래서 풀 스택이 어렵구나.
AOC2025 DAY 5 무식하게 하려다가 맥북 뻗어서 머리 써서 풀었다... 진작 머리 쓸걸..
Karpathy의 프로그래밍 관련 발언: "이렇게까지 뒤처진 느낌은 처음이다"
------------------------------
- Andrej Karpathy가 현재의 프로그래밍 환경에서 *자신이 크게 뒤처지고 있다고* 강하게 느낀다고 밝힘
- 프로그래머가 직접 작성하는 코드 비중은 줄어들고, 이미 존재하는 도구와 시스템을 *어떻게 연결하고 조합하느냐* 가 핵심 역량으로 이동 중
- 지난 1년간 등장한 도구들을 제대로 엮기만 해도 *생산성이…
------------------------------
https://news.hada.io/topic?id=25434&utm_source=googlechat&utm_medium=bot&utm_campaign=1834
Unity의 Mono 문제: 왜 당신의 C# 코드는 기대보다 느리게 실행되는가
------------------------------
- Unity가 사용하는 *Mono 런타임* 은 최신 .NET 대비 현저히 느린 실행 속도를 보이며, 동일한 C# 코드가 *최대 15배까지* 차이 나는 사례가 있음
- 실제 게임 코드에서 *Mono 기반 Unity 실행은 100초* , 동일 코드의 .NET 실행은 *38초* 로 측정되어, 디버깅과 테스트 효율에도 큰 영향을 줌
- *Release 모드…
------------------------------
https://news.hada.io/topic?id=25421&utm_source=googlechat&utm_medium=bot&utm_campaign=1834
洪 民憙 (Hong Minhee) shared the below article:
고남현 @gnh1201@hackers.pub
ActivityPub 서버에 공유되는 YouTube 링크는 종종 사용자들 사이에서 개인정보 보호 측면의 우려 사항으로 언급됩니다. 이는 공유된 URL에 포함된 si 파라미터나, YouTube 웹사이트 자체에 내장된 다양한 추적 기술 등 방문자를 추적하는 데 사용될 수 있는 여러 기술적 메커니즘 때문입니다.
현실적으로 볼 때, ActivityPub 프로토콜을 구현하는 프로젝트들이 이 문제에 대해 기본 제공 해결책을 제시할 가능성은 낮습니다. 이는 YouTube라는 특정 서비스에 국한된 문제가 아니라, 보다 광범위한 웹 추적 문제에 해당하기 때문입니다.
그럼에도 불구하고, 서버 관리자는 서버 차원에서 이러한 우려를 완화하기 위한 실질적인 조치를 취할 수 있습니다.
YouTube로 직접 연결하는 대신, 개인정보 보호에 더 우호적인 대체 프론트엔드 사용을 권장할 수 있습니다.
이러한 프론트엔드들은 영상 접근성을 유지하면서도 추적을 줄이거나 제거하는 데 도움을 줍니다.
sub_filter를 사용한 링크 재작성 대체 프론트엔드를 설정한 이후에는, Nginx의 sub_filter 기능을 사용하여 YouTube 링크를 투명하게 재작성할 수 있습니다. 이를 통해 사용자가 원본 YouTube URL에 직접 접근하는 것을 방지하고, 대신 대체 프론트엔드를 통해 영상을 보도록 유도할 수 있습니다.
예시 설정은 다음과 같습니다.
sub_filter 'www.youtube.com/' 'dnt-yt.catswords.net/';
sub_filter 'youtube.com/' 'dnt-yt.catswords.net/';
sub_filter 'www.youtu.be/' 'dnt-yt.catswords.net/';
sub_filter 'youtu.be/' 'dnt-yt.catswords.net/';
youtube.com 또는 youtu.be로 연결되는 링크가 일관되게 대체 프론트엔드 주소로 변경되는 것이 확인되면, 설정은 완료된 것입니다.
이 접근 방식을 실제로 적용한 사례는 아래 링크에서 확인할 수 있습니다.
https://catswords.social/@gnh1201/115801692643125819
BGE-M3, MarkItDown, 그리고 마크다운 구조 파서를 이용해 시맨틱 청킹을 수행하고, 그 결과를 Parquet 파일에 저장하는 aipack 프레임워크의 첫 버전을 릴리스합니다. 모델과 데이터베이스에 종속되지 않는 중립적 상태를 유지하여 언제든 재사용할 수 있는 파일 포맷을 기반으로 RAG를 구현하고, MCP 서버까지 구동할 수 있도록 설계했습니다.
aipack의 지향점은 NPU나 GPU에 의존하지 않는 RAG를 구현함과 동시에, 향후 다양한 RAG 구조로 확장하기 용이한 환경을 만드는 데 방점이 찍혀 있습니다. "고품질의 Parquet 파일을 만들어낼 수 있다면 무엇이든 할 수 있다"는 전제 아래, 업계에서 흔히 쓰이는 RAG 파이프라인을 디커플링(Decoupling)해본 실험적 프로젝트입니다.
프로젝트에 대한 피드백과 후기, 평가를 공유해 주시면 감사하겠습니다. 또한, 지속 가능한 오픈소스 활동을 위해 후원을 더해 주신다면 큰 힘이 됩니다.
GitHub: https://github.com/rkttu/aipack
Fedify에 기여하다보면 진짜 민희님이 괜히 STF 받으신게 아니구나... 싶어지는 코드들이 종종 있음
걍 눈 앞이 캄캄해지고 아찔해져서 눈 질끈 감게 됨
#코스모슬라이드 업로드 기능을 뜯어고치고 있다. 일단 GPT한테 바이브 시켰더니 적당히 잘 작동...하네. 겉모습은.
Upyo 만들 때도 상당히 바이브 코딩 농도가 높았는데, 지금 만들고 있는 LLM 기반 번역 라이브러리는 그보다도 더 바이크 코딩 농도를 높여서 작업하고 있다. 초기 세팅 및 인터페이스 선언 외에는 내가 직접 짠 코드가 거의 없을 정도. 그래도 어떻게든 돌아가는 게 만들어지는 걸 보니 LLM 성능도 많이 좋아진 것 같다. (코딩 에이전트들의 노하우도 깊어진 것도 한몫 하겠지만.)
@mitsuhikoArmin Ronacher by keeping my skills sharp? I don't think so
@aburkaaburka 🫣 You're judging before you've tried, and that's the disservice. You're assuming your skills will dull if you use an agent, and you're treating that assumption as a conclusion. That's the mistake. Try it first. See what actually happens. Then adjust your thinking based on experience, not fear.
I'm using them for months now, and in no way is it dulling my skills. I haven't learned as much as a programmer in years personally.
If you are a programmer and an AI hold-out, and you have some time off during Christmas: gift yourself a 100 USD subscription to Claude Code and … try it. But really try it. Take a week if you can afford it and dive in. It will change your opinion on these tools.
And I'm saying this also because I saw multiple people now who I knew learned throughout the year what AI agents are and it didn't click, until they took the time over Christmas to really dive in.
If you are a programmer and an AI hold-out, and you have some time off during Christmas: gift yourself a 100 USD subscription to Claude Code and … try it. But really try it. Take a week if you can afford it and dive in. It will change your opinion on these tools.
洪 民憙 (Hong Minhee) shared the below article:
Juntai Park @arkjun@hackers.pub
최근 인프라 구성과 서비스 운영 전반에서 (늦었지만…) Terraform과 Kubernetes를 본격적으로 사용해 보았고, 생각보다 경험이 꽤 좋아서 기록 겸 공유해 둔다.
이걸 왜 이제 썼지. 진작 써볼 걸. (feat. 관리할 서버가 많아질수록 체감이 큼)
내 경우는 NCP(Naver Cloud Platform) 를 사용했는데, 지원하는 리소스 범위가 제한적이라 일부는 여전히 웹 콘솔에서 수작업이 필요했다.
그럼에도 불구하고, Terraform을 도입한 만족도는 꽤 높았다.
yaml파일로 관리할 수 있는 점이 매우 편리이번에 느낀 또 하나의 큰 포인트는 AI의 존재감이었다.
결과적으로,
요약하자면,
부록) K8S, 다음에도 바로 쓸 것인가?
정리하면
식탁보 1.15.0 버전을 출시했습니다. 1년여만의 대규모 업데이트로, .NET 10 적용과 함께 커뮤니티에서 불편 사항으로 여겨졌던 Windows Sandbox의 vGPU 기본 사용 문제, 언어 표시 문제, 그리고 인스톨러 간소화 등 성능과 기능 간소화는 물론, 코드의 분량을 대폭 간소화했습니다.
추후 TableCloth3 프로젝트에서 개발 중인 Avalonia 기반 프론트엔드로 쉽게 전환할 수 있도록 땅 다지기 작업도 같이 진행해두었고 계속 업데이트해나갈 예정입니다. 그리고 이번 업데이트부터 ARM64 빌드도 정식으로 제공됩니다.
꾸준한 관심과 성원에 늘 감사드립니다.
Upyo 만들 때도 상당히 바이브 코딩 농도가 높았는데, 지금 만들고 있는 LLM 기반 번역 라이브러리는 그보다도 더 바이크 코딩 농도를 높여서 작업하고 있다. 초기 세팅 및 인터페이스 선언 외에는 내가 직접 짠 코드가 거의 없을 정도. 그래도 어떻게든 돌아가는 게 만들어지는 걸 보니 LLM 성능도 많이 좋아진 것 같다. (코딩 에이전트들의 노하우도 깊어진 것도 한몫 하겠지만.)
I finally managed to complete a working RFC9421 implementation... I didn't realize that Fedify's RFC9421 public key retrieval didn't include assertionMethod...
❯ witr ghostty
Target : ghostty
Process : ghostty (pid 36529)
User : mck
Service : launch job demand
Command : /Applications/Ghostty.app/Contents/MacOS/ghostty
Started : just now (Mon 2025-12-29 10:08:55 +00:00)
Why It Exists :
launchd (pid 1) → ghostty (pid 36529)
Source : application.com.mitchellh.ghostty.34914134.34914139 (launchd)
Type : Launch Agent
Working Dir : /
Open Files : 100 of 256 (39%)
Locks : /Users/mck/Library/Caches/com.mitchellh.ghostty/sentry/8c48f586-5c27-49c8-71a1-7727494394fc.run.lock
Warnings :
• Process is running from a suspicious working directory: /
• Service name and process name do not match
최근 보안의 화두는 제로트러스트지만, 정작 가장 민감한 공동인증서 파일은 여전히 NPKI라는 고전적인 디렉터리 구조 속에 노출되어 있습니다.
OS 수준의 암호화 기술인 BitLocker나 VBS의 이점을 전혀 활용하지 못하는 현 상황을 개선해보고자, Windows 인증서 저장소를 백엔드로 활용하는 방식을 고민 중입니다. macOS의 Keychain 시스템처럼 인증서를 시스템 보안 영역 내부로 끌어들임으로써, 파일 탈취 위험을 획기적으로 낮추는 것이 목표입니다.
인프라 자체를 바꿀 순 없어도, 엔드포인트 단에서 '방어의 밀도'를 높이는 유의미한 시도가 될 것 같습니다. :-D
Claude Code Subagent를 가만 살펴봤는데, 내가 일하는 방식 그리고 다른 사람에게 위임할 수 있는 방식을 잘 정의하면 그럭저럭 쓸만한 것 같음. 생각을 글로 잘 정리하는 버릇을 들여야겠다.
(오프라인에서 했던 얘기를 온라인에서도 하기)
@hongminhee洪 民憙 (Hong Minhee) 님 블로그는 국한문혼용으로 보면 세로쓰기 가로스크롤로 바뀌는 게 꽤나 운치가 있다고 생각해요..... 한자를 못 읽는 건 아쉽지만
@eatch잇창명 EatChangmyeong💕🐱 감사합니다. 🥰
(오프라인에서 했던 얘기를 온라인에서도 하기)
@hongminhee洪 民憙 (Hong Minhee) 님 블로그는 국한문혼용으로 보면 세로쓰기 가로스크롤로 바뀌는 게 꽤나 운치가 있다고 생각해요..... 한자를 못 읽는 건 아쉽지만
FOSDEM 2026 Social Web Speakers
I have been trying to create a list for #fosdem26 and realized that (ironically) most of the people in the socialweb track ... does not have a fediverse account listed there.
I am also at fault, btw, so shame to me.
If you know someone who is presenting at #fosdem26 please send them my way. I will update this thread with the list of confirmed speakers.
The Fosdem 26 social web track List:
@pfefferleMatthias Pfefferle
@evanEvan Prodromou @evanprodromou
@haubleshannah aubry
@mapacheMaho 🦝🍻
@dariusDarius Kazemi
@bjoernstaBjörn Staschen
@django
@resieguenRebecca Sieber
@openforfutureOpen For Future | Italia
@iusondemandValentino Spataro
@cwebberChristine Lemmer-Webber
@tsyesikaJessica Tallon
@zzepposs
@melaniebartos
@Pepijn
@FloppyJames Smith 💾
@tobias
@mayel
@ivan
@hongminhee@hackers.pub洪 民憙 (Hong Minhee)
@samviesamvie ⁂
@benpateBen Pate 🤘🏻
@neiman
@hongminhee@hollo.social洪 民憙 (Hong Minhee) 
@filippodb
filippodb ⁂
@magostinelliMichele Agostinelli
@publicspaces
@cubicgardenIan Forrester | @cubicgarden
@samviesamvie ⁂
@bonfire
@FediVariety
@vishnee
Non Social Web Track presenters:
Social Web Track schedule:
https://fosdem.org/2026/schedule/track/social-web/
Boosts are also appreciated!
P.S. Special thanks to
@liaizonwakest ⁂,
@andypiper,
@michaelMichael Foster,
@toon
asdf + direnv 조합을 mise로 바꾸고 나서, 초반에는 환경 변수 제어가 쉽고 다양한 점이 좋았다면 시간이 갈수록 tasks 기능이 기존 프로젝트까지 mise로 전환하게 만든다.
[tasks."dev:db"]
dir = "./data"
run = "./docker-run-postgres.sh --no-tty"
[tasks."dev:cms"]
dir = "./cms"
run = """
until nc -z localhost 5432; do
echo "Waiting for database to be ready..."
sleep 1
done
corepack yarn dev || [ $? -eq 129 ] || [ $? -eq 130 ]
"""
[tasks.dev]
depends = ["dev:db", "dev:cms"]
pyodide를 사용해서 python으로 된 앱을 정적 웹사이트로 서빙 https://khris.github.io/woodcut/
LLM 기반 번역 라이브러리를 하나 간단하게 만드려고 하는데, 이름 투표 부탁드립니다!
I'm working on a new JavaScript/TypeScript library for natural language translation powered by LLMs. I want a name that feels elegant, memorable, and reflects the essence of translation.
I've narrowed it down to four candidates from different linguistic roots. Which one do you think fits bets?
Xindaya (信達雅): Derived from Yan Fu (嚴復)'s Three Pillars of Translation—faithfulness (信), expressiveness (達), and elegance (雅).
Vertana (वर्तन): Means transformation, turning, or process. It evokes the fluid and sacred process of transforming meaning from one language to another.
Glosso (γλῶσσα): The root for tongue or language. It's the origin of terms like glosssary and polyglot.
Fanyi (飜譯): The direct and minimal term for translation. It's punchy and honors the long-standing tradition of translation in East Asia.
GLM-4.7의 성능이 그렇게나 좋다고 들어서 요금제를 보니 상당히 파격적인 가격이라 조금 시도해 봤다. 우선 LogTape에 있던 이슈 하나를 수행하게 했고, 혹시 몰라서 Claude Code에서 Claude 4.5 Opus로 PLAN.md 계획 파일을 꽤 꼼꼼하게 만들게 한 뒤, 그걸 참고하게 했다. 그럼에도 불구하고:
export되는 API에 대해서는 JSDoc 주석을 써야 한다는 당연한 절차를 수행하지 않음 (코드베이스의 다른 코드는 다 그렇게 되어 있는데도 눈치가 없음)Deno.env 같은 특정 런타임에 의존적인 API를 씀 (코드베이스의 다른 코드는 그런 API 안 쓰고 있음)소문난 잔치에 먹을 게 없다더니, 역시나 벤치마크만 보고 모델을 골라서는 안 되겠다는 교훈만 재확인 한 것 같다.